- 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
318 lines
13 KiB
HTML
318 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Whitelist Manager - Firefrost Gaming</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
fire: '#FF4500',
|
|
frost: '#00CED1',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="bg-gray-900 text-gray-100">
|
|
<div class="container mx-auto px-4 py-8">
|
|
<!-- Header -->
|
|
<header class="mb-8">
|
|
<h1 class="text-4xl font-bold mb-2">
|
|
<span class="text-fire">🔥</span>
|
|
Whitelist Manager
|
|
<span class="text-frost">❄️</span>
|
|
</h1>
|
|
<p class="text-gray-400">Firefrost Gaming - Server Whitelist Management</p>
|
|
</header>
|
|
|
|
<!-- Player Management Section -->
|
|
<div class="bg-gray-800 rounded-lg p-6 mb-8">
|
|
<h2 class="text-2xl font-bold mb-4">Player Management</h2>
|
|
|
|
<!-- Single Player Operations -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium mb-2">Player Username</label>
|
|
<input type="text" id="playerName"
|
|
class="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 focus:outline-none focus:border-frost"
|
|
placeholder="Enter Minecraft username">
|
|
</div>
|
|
|
|
<!-- Bulk Operations -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium mb-2">Bulk Operations (one username per line)</label>
|
|
<textarea id="bulkPlayers" rows="4"
|
|
class="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 focus:outline-none focus:border-frost"
|
|
placeholder="username1 username2 username3"></textarea>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex gap-4 mb-6">
|
|
<button onclick="addPlayer()"
|
|
class="bg-green-600 hover:bg-green-700 px-6 py-2 rounded font-medium transition">
|
|
Add to Whitelist
|
|
</button>
|
|
<button onclick="removePlayer()"
|
|
class="bg-red-600 hover:bg-red-700 px-6 py-2 rounded font-medium transition">
|
|
Remove from Whitelist
|
|
</button>
|
|
<button onclick="bulkAdd()"
|
|
class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded font-medium transition">
|
|
Bulk Add
|
|
</button>
|
|
<button onclick="bulkRemove()"
|
|
class="bg-orange-600 hover:bg-orange-700 px-6 py-2 rounded font-medium transition">
|
|
Bulk Remove
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Server Selection -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium mb-2">Select Servers</label>
|
|
<div class="flex gap-4 mb-4">
|
|
<button onclick="selectAllServers()"
|
|
class="text-frost hover:text-frost-light underline">
|
|
Select All
|
|
</button>
|
|
<button onclick="selectNone()"
|
|
class="text-frost hover:text-frost-light underline">
|
|
Select None
|
|
</button>
|
|
<button onclick="selectTX()"
|
|
class="text-fire hover:text-fire-light underline">
|
|
TX1 Only
|
|
</button>
|
|
<button onclick="selectNC()"
|
|
class="text-frost hover:text-frost-light underline">
|
|
NC1 Only
|
|
</button>
|
|
</div>
|
|
<div id="serverList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
<!-- Server checkboxes will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Server Status Section -->
|
|
<div class="bg-gray-800 rounded-lg p-6">
|
|
<h2 class="text-2xl font-bold mb-4">Server Status</h2>
|
|
<div id="serverStatus" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<!-- Server status cards will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Section -->
|
|
<div id="results" class="mt-8 hidden">
|
|
<div class="bg-gray-800 rounded-lg p-6">
|
|
<h2 class="text-2xl font-bold mb-4">Results</h2>
|
|
<div id="resultsContent"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const servers = {{ servers | tojson }};
|
|
|
|
// Initialize the page
|
|
function init() {
|
|
populateServerList();
|
|
populateServerStatus();
|
|
}
|
|
|
|
function populateServerList() {
|
|
const serverList = document.getElementById('serverList');
|
|
servers.forEach(server => {
|
|
const node = server.name.includes('TX') || ['Reclamation', 'Stoneblock 4', 'Society: Sunlit Valley', 'Vanilla 1.21.11', 'All The Mons'].includes(server.name) ? 'TX1' : 'NC1';
|
|
const nodeColor = node === 'TX1' ? 'text-fire' : 'text-frost';
|
|
|
|
const div = document.createElement('div');
|
|
div.className = 'flex items-center gap-2';
|
|
div.innerHTML = `
|
|
<input type="checkbox" id="server-${server.uuid}" value="${server.uuid}"
|
|
class="w-4 h-4 rounded border-gray-600 bg-gray-700 checked:bg-frost">
|
|
<label for="server-${server.uuid}" class="cursor-pointer">
|
|
<span class="${nodeColor} font-medium">[${node}]</span> ${server.name}
|
|
</label>
|
|
`;
|
|
serverList.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function populateServerStatus() {
|
|
const statusDiv = document.getElementById('serverStatus');
|
|
servers.forEach(server => {
|
|
const statusColor = server.status === 'running' ? 'text-green-500' :
|
|
server.status === 'offline' ? 'text-red-500' : 'text-yellow-500';
|
|
const statusText = server.status || 'unknown';
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'bg-gray-700 rounded p-4';
|
|
card.innerHTML = `
|
|
<h3 class="font-bold mb-2">${server.name}</h3>
|
|
<p class="${statusColor}">● ${statusText.toUpperCase()}</p>
|
|
`;
|
|
statusDiv.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function getSelectedServers() {
|
|
const checkboxes = document.querySelectorAll('#serverList input[type="checkbox"]:checked');
|
|
return Array.from(checkboxes).map(cb => cb.value);
|
|
}
|
|
|
|
function selectAllServers() {
|
|
document.querySelectorAll('#serverList input[type="checkbox"]').forEach(cb => cb.checked = true);
|
|
}
|
|
|
|
function selectNone() {
|
|
document.querySelectorAll('#serverList input[type="checkbox"]').forEach(cb => cb.checked = false);
|
|
}
|
|
|
|
function selectTX() {
|
|
const txServers = ['1eb33479-a6bc-4e8f-b64d-d1e4bfa0a8b4', 'a0efbfe8-4b97-4a90-869d-ffe6d3072bd5',
|
|
'9310d0a6-62a6-4fe6-82c4-eb483dc68876', '3bed1bda-f648-4630-801a-fe9f2e3d3f27',
|
|
'668a5220-7e72-4379-9165-bdbb84bc9806'];
|
|
document.querySelectorAll('#serverList input[type="checkbox"]').forEach(cb => {
|
|
cb.checked = txServers.includes(cb.value);
|
|
});
|
|
}
|
|
|
|
function selectNC() {
|
|
const ncServers = ['124f9060-58a7-457a-b2cf-b4024fce2951', 'a14201d2-83b2-44e6-ae48-e6c4cbc56f24',
|
|
'82e63949-8fbf-4a44-b32a-53324e8492bf', '2f85d4ef-aa49-4dd6-b448-beb3fca1db12',
|
|
'09a95f38-9f8c-404a-9557-3a7c44258223'];
|
|
document.querySelectorAll('#serverList input[type="checkbox"]').forEach(cb => {
|
|
cb.checked = ncServers.includes(cb.value);
|
|
});
|
|
}
|
|
|
|
async function addPlayer() {
|
|
const player = document.getElementById('playerName').value.trim();
|
|
if (!player) {
|
|
showResults([{success: false, message: 'Please enter a player name'}]);
|
|
return;
|
|
}
|
|
|
|
const selectedServers = getSelectedServers();
|
|
if (selectedServers.length === 0) {
|
|
showResults([{success: false, message: 'Please select at least one server'}]);
|
|
return;
|
|
}
|
|
|
|
const response = await fetch('/api/whitelist/add', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({player, servers: selectedServers})
|
|
});
|
|
|
|
const data = await response.json();
|
|
showResults(data.results || [data]);
|
|
}
|
|
|
|
async function removePlayer() {
|
|
const player = document.getElementById('playerName').value.trim();
|
|
if (!player) {
|
|
showResults([{success: false, message: 'Please enter a player name'}]);
|
|
return;
|
|
}
|
|
|
|
const selectedServers = getSelectedServers();
|
|
if (selectedServers.length === 0) {
|
|
showResults([{success: false, message: 'Please select at least one server'}]);
|
|
return;
|
|
}
|
|
|
|
const response = await fetch('/api/whitelist/remove', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({player, servers: selectedServers})
|
|
});
|
|
|
|
const data = await response.json();
|
|
showResults(data.results || [data]);
|
|
}
|
|
|
|
async function bulkAdd() {
|
|
const playersText = document.getElementById('bulkPlayers').value.trim();
|
|
if (!playersText) {
|
|
showResults([{success: false, message: 'Please enter player names'}]);
|
|
return;
|
|
}
|
|
|
|
const players = playersText.split('\n').map(p => p.trim()).filter(p => p);
|
|
const selectedServers = getSelectedServers();
|
|
|
|
if (selectedServers.length === 0) {
|
|
showResults([{success: false, message: 'Please select at least one server'}]);
|
|
return;
|
|
}
|
|
|
|
const response = await fetch('/api/whitelist/bulk', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({operation: 'add', players, servers: selectedServers})
|
|
});
|
|
|
|
const data = await response.json();
|
|
showResults(data.results || [data]);
|
|
}
|
|
|
|
async function bulkRemove() {
|
|
const playersText = document.getElementById('bulkPlayers').value.trim();
|
|
if (!playersText) {
|
|
showResults([{success: false, message: 'Please enter player names'}]);
|
|
return;
|
|
}
|
|
|
|
const players = playersText.split('\n').map(p => p.trim()).filter(p => p);
|
|
const selectedServers = getSelectedServers();
|
|
|
|
if (selectedServers.length === 0) {
|
|
showResults([{success: false, message: 'Please select at least one server'}]);
|
|
return;
|
|
}
|
|
|
|
const response = await fetch('/api/whitelist/bulk', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({operation: 'remove', players, servers: selectedServers})
|
|
});
|
|
|
|
const data = await response.json();
|
|
showResults(data.results || [data]);
|
|
}
|
|
|
|
function showResults(results) {
|
|
const resultsDiv = document.getElementById('results');
|
|
const resultsContent = document.getElementById('resultsContent');
|
|
|
|
resultsContent.innerHTML = '';
|
|
results.forEach(result => {
|
|
const color = result.success ? 'text-green-500' : 'text-red-500';
|
|
const icon = result.success ? '✓' : '✗';
|
|
|
|
const div = document.createElement('div');
|
|
div.className = `p-3 mb-2 rounded ${result.success ? 'bg-green-900/20' : 'bg-red-900/20'}`;
|
|
div.innerHTML = `
|
|
<span class="${color} font-bold">${icon}</span>
|
|
${result.player ? `<strong>${result.player}</strong> on ` : ''}
|
|
<strong>${result.server || 'Server'}</strong>:
|
|
${result.message}
|
|
`;
|
|
resultsContent.appendChild(div);
|
|
});
|
|
|
|
resultsDiv.classList.remove('hidden');
|
|
setTimeout(() => resultsDiv.scrollIntoView({behavior: 'smooth'}), 100);
|
|
}
|
|
|
|
// Initialize on page load
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|