diff --git a/services/arbiter-3.0/migrations/139_seed_server_config.js b/services/arbiter-3.0/migrations/139_seed_server_config.js index ad004a6..04bbe79 100644 --- a/services/arbiter-3.0/migrations/139_seed_server_config.js +++ b/services/arbiter-3.0/migrations/139_seed_server_config.js @@ -1,7 +1,7 @@ /** * Seed script for server_config table. - * Fetches server identifiers from Pterodactyl API and populates - * short_name mappings for all known servers. + * Uses known server identifiers from Chronicler #87 audit. + * No Pterodactyl API call needed — identifiers are hardcoded. * * Usage: node migrations/139_seed_server_config.js */ @@ -16,74 +16,54 @@ const pool = new Pool({ port: process.env.DB_PORT || 5432 }); -// Known server mappings — short_name values confirmed from Discord audit +// Full server data — identifiers, subdomains, IPs, ports from Chronicler #87 audit const SERVER_MAP = [ - { name: 'All the Mods 10: To the Sky', short_name: 'atm10-tts', node: 'NC1' }, - { name: 'All the Mons', short_name: 'all-the-mons', node: 'NC1' }, - { name: 'Mythcraft 5', short_name: 'mythcraft-5', node: 'NC1' }, - { name: 'All of Create (Creative)', short_name: 'all-of-create', node: 'NC1' }, - { name: 'DeceasedCraft', short_name: 'deceasedcraft', node: 'NC1' }, - { name: "Sneak's Pirate Pack", short_name: 'sneaks-pirate-pack', node: 'NC1' }, - { name: 'Otherworld [Dungeons & Dragons]', short_name: 'otherworld', node: 'NC1' }, - { name: 'Farm Crossing 6', short_name: 'farm-crossing-6', node: 'NC1' }, - { name: 'Homestead - A Cozy Survival Experience', short_name: 'homestead', node: 'NC1' }, - { name: 'Stoneblock 4', short_name: 'stoneblock-4', node: 'TX1' }, - { name: 'Society: Sunlit Valley', short_name: 'society-sunlit-valley', node: 'TX1' }, - { name: 'Submerged 2', short_name: 'submerged-2', node: 'TX1' }, - { name: 'Beyond Depth', short_name: 'beyond-depth', node: 'TX1' }, - { name: 'Beyond Ascension', short_name: 'beyond-ascension', node: 'TX1' }, - { name: 'Cottage Witch', short_name: 'cottage-witch', node: 'TX1' }, - { name: 'All The Mons (Private) - TX', short_name: 'all-the-mons-private', node: 'TX1' }, - { name: "Wold's Vaults", short_name: 'wolds-vaults', node: 'TX1' } + { id: 'f408e832', name: 'All the Mods 10: To the Sky', short_name: 'atm10-tts', subdomain: 'atm10tts', ip: '216.239.104.130', port: 25565, node: 'NC1' }, + { id: 'c4bc5892', name: 'All the Mons', short_name: 'all-the-mons', subdomain: 'atmons', ip: '216.239.104.130', port: 25566, node: 'NC1' }, + { id: 'b90ced3c', name: 'Mythcraft 5', short_name: 'mythcraft-5', subdomain: 'mythcraft5', ip: '216.239.104.130', port: 25567, node: 'NC1' }, + { id: 'e1c6ff8d', name: 'All of Create (Creative)', short_name: 'all-of-create', subdomain: 'aocc', ip: '216.239.104.130', port: 25568, node: 'NC1' }, + { id: '82e63949', name: 'All The Mods 10', short_name: 'atm10', subdomain: 'atm10', ip: '216.239.104.130', port: 25569, node: 'NC1' }, + { id: 'd4790f45', name: 'Otherworld [Dungeons & Dragons]', short_name: 'otherworld', subdomain: 'otherworld', ip: '216.239.104.130', port: 25570, node: 'NC1' }, + { id: '8950fa1e', name: 'DeceasedCraft', short_name: 'deceasedcraft', subdomain: 'deceasedcraft', ip: '216.239.104.130', port: 25571, node: 'NC1' }, + { id: '7c9c2dc0', name: "Sneak's Pirate Pack", short_name: 'sneaks-pirate-pack', subdomain: 'sneakspiratpack', ip: '216.239.104.130', port: 25572, node: 'NC1' }, + { id: '25b23f6e', name: 'Farm Crossing 6', short_name: 'farm-crossing-6', subdomain: 'farmcrossing6', ip: '216.239.104.130', port: 25573, node: 'NC1' }, + { id: 'f5befeab', name: 'Homestead - A Cozy Survival Experience', short_name: 'homestead', subdomain: 'homestead', ip: '216.239.104.130', port: 25574, node: 'NC1' }, + { id: 'a0efbfe8', name: 'Stoneblock 4', short_name: 'stoneblock-4', subdomain: 'stoneblock4', ip: '38.68.14.26', port: 25565, node: 'TX1' }, + { id: '9310d0a6', name: 'Society: Sunlit Valley', short_name: 'society-sunlit-valley', subdomain: 'society', ip: '38.68.14.28', port: 25565, node: 'TX1' }, + { id: '576342b8', name: 'Submerged 2', short_name: 'submerged-2', subdomain: 'submerged2', ip: '38.68.14.26', port: 25571, node: 'TX1' }, + { id: 'e95ed4a8', name: 'Beyond Depth', short_name: 'beyond-depth', subdomain: 'beyonddepth', ip: '38.68.14.26', port: 25568, node: 'TX1' }, + { id: '3f842757', name: 'Beyond Ascension', short_name: 'beyond-ascension', subdomain: 'beyondascension', ip: '38.68.14.26', port: 25569, node: 'TX1' }, + { id: '7a9754ad', name: 'Cottage Witch', short_name: 'cottage-witch', subdomain: 'cottagewitch', ip: '38.68.14.26', port: 25572, node: 'TX1' }, + { id: '668a5220', name: 'All The Mons (Private) - TX', short_name: 'all-the-mons-private', subdomain: 'allthemons', ip: '38.68.14.30', port: 25565, node: 'TX1' }, + { id: 'fcbe0a1d', name: "Wold's Vaults", short_name: 'wolds-vaults', subdomain: 'vaults', ip: '38.68.14.26', port: 25570, node: 'TX1' } ]; async function seed() { - console.log('Fetching servers from Pterodactyl API...'); - - const endpoint = `${process.env.PANEL_URL}/api/application/servers`; - const res = await fetch(endpoint, { - headers: { - 'Authorization': `Bearer ${process.env.PANEL_APPLICATION_KEY}`, - 'Accept': 'application/json' - } - }); - - if (!res.ok) { - console.error(`Panel API error: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const data = await res.json(); - const servers = data.data.map(s => s.attributes); - - let matched = 0; - let skipped = 0; - - for (const mapping of SERVER_MAP) { - const ptero = servers.find(s => s.name === mapping.name); - if (!ptero) { - console.warn(` SKIP: "${mapping.name}" — not found in Pterodactyl`); - skipped++; - continue; - } + console.log('Seeding server_config with %d servers...', SERVER_MAP.length); + let count = 0; + for (const s of SERVER_MAP) { await pool.query(` - INSERT INTO server_config (server_identifier, short_name, short_name_locked, display_name, node, pterodactyl_name) - VALUES ($1, $2, true, $3, $4, $5) + INSERT INTO server_config + (server_identifier, short_name, short_name_locked, display_name, pterodactyl_name, subdomain, server_ip, server_port, node) + VALUES ($1, $2, true, $3, $3, $4, $5, $6, $7) ON CONFLICT (server_identifier) DO UPDATE SET short_name = EXCLUDED.short_name, short_name_locked = true, display_name = EXCLUDED.display_name, - node = EXCLUDED.node, pterodactyl_name = EXCLUDED.pterodactyl_name, + subdomain = EXCLUDED.subdomain, + server_ip = EXCLUDED.server_ip, + server_port = EXCLUDED.server_port, + node = EXCLUDED.node, updated_at = NOW() - `, [ptero.identifier, mapping.short_name, mapping.name, mapping.node, mapping.name]); + `, [s.id, s.short_name, s.name, s.subdomain, s.ip, s.port, s.node]); - console.log(` OK: "${mapping.name}" → ${mapping.short_name} (${ptero.identifier})`); - matched++; + console.log(' OK: %s → %s (%s.firefrostgaming.com)', s.name, s.short_name, s.subdomain); + count++; } - console.log(`\nDone. Matched: ${matched}, Skipped: ${skipped}`); + console.log('\nDone. Seeded %d servers.', count); await pool.end(); } diff --git a/services/arbiter-3.0/migrations/139_server_config.sql b/services/arbiter-3.0/migrations/139_server_config.sql index b61db0d..9be73ff 100644 --- a/services/arbiter-3.0/migrations/139_server_config.sql +++ b/services/arbiter-3.0/migrations/139_server_config.sql @@ -7,6 +7,9 @@ CREATE TABLE IF NOT EXISTS server_config ( short_name VARCHAR(64) UNIQUE, short_name_locked BOOLEAN DEFAULT false, display_name VARCHAR(128), + subdomain VARCHAR(64) UNIQUE, + server_ip VARCHAR(45), + server_port INTEGER DEFAULT 25565, restart_enabled BOOLEAN DEFAULT true, restart_offset_minutes INTEGER DEFAULT 0, node VARCHAR(8), diff --git a/services/arbiter-3.0/src/routes/admin/servers.js b/services/arbiter-3.0/src/routes/admin/servers.js index b4df169..4c99813 100644 --- a/services/arbiter-3.0/src/routes/admin/servers.js +++ b/services/arbiter-3.0/src/routes/admin/servers.js @@ -531,4 +531,76 @@ router.post('/:identifier/console', async (req, res) => { } }); +// ─── NEW: SUBDOMAIN PROVISIONING ──────────────────────────── + +const CF_API = 'https://api.cloudflare.com/client/v4'; +const CF_ZONE_ID = '7604c173d802f154035f7e998018c1a9'; + +router.post('/:identifier/provision-subdomain', async (req, res) => { + const { identifier } = req.params; + const { subdomain } = req.body; + + if (!subdomain || !/^[a-z0-9]+$/.test(subdomain)) { + return res.send(`❌ Invalid: lowercase letters and numbers only, no hyphens`); + } + + const config = await getServerConfig(identifier); + if (!config || !config.server_ip) { + return res.send(`❌ No server config or IP`); + } + if (config.subdomain) { + return res.send(`Already provisioned: ${config.subdomain}.firefrostgaming.com`); + } + + const cfToken = process.env.CLOUDFLARE_API_TOKEN; + if (!cfToken) { + return res.send(`❌ CLOUDFLARE_API_TOKEN not set`); + } + + const fqdn = `${subdomain}.firefrostgaming.com`; + + try { + // Create A record + const aRes = await fetch(`${CF_API}/zones/${CF_ZONE_ID}/dns_records`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${cfToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'A', name: fqdn, content: config.server_ip, ttl: 1, proxied: false }) + }); + const aData = await aRes.json(); + if (!aData.success) { + return res.send(`❌ A record: ${aData.errors?.[0]?.message || 'failed'}`); + } + + // Create SRV record + const srvRes = await fetch(`${CF_API}/zones/${CF_ZONE_ID}/dns_records`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${cfToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'SRV', + name: `_minecraft._tcp.${fqdn}`, + data: { priority: 0, weight: 0, port: config.server_port || 25565, target: fqdn }, + ttl: 1 + }) + }); + const srvData = await srvRes.json(); + if (!srvData.success) { + console.warn('[Cloudflare] SRV failed (A record created):', srvData.errors); + } + + // Save to DB + await db.query( + 'UPDATE server_config SET subdomain = $1, updated_at = NOW() WHERE server_identifier = $2', + [subdomain, identifier] + ); + + serverCache.lastFetch = 0; + console.log(`[DNS] Provisioned ${fqdn} → ${config.server_ip}:${config.server_port}`); + + res.send(`✅ ${fqdn} provisioned`); + } catch (error) { + console.error('[provision-subdomain]', error); + res.send(`❌ ${error.message}`); + } +}); + module.exports = router; diff --git a/services/arbiter-3.0/src/views/admin/servers/_server_card.ejs b/services/arbiter-3.0/src/views/admin/servers/_server_card.ejs index 0458350..44cb9b2 100644 --- a/services/arbiter-3.0/src/views/admin/servers/_server_card.ejs +++ b/services/arbiter-3.0/src/views/admin/servers/_server_card.ejs @@ -74,6 +74,35 @@ + +