load(); $config = [ 'SITE_NAME' => $_ENV['SITE_NAME'] ?? 'tgirl.city', 'version' => $_ENV['VERSION'] ?? 'unknown', 'discord_id' => $_ENV['DISCORD_CLIENT_ID'], 'discord_secret' => $_ENV['DISCORD_CLIENT_SECRET'], 'discord_redirect'=> $_ENV['DISCORD_REDIRECT_URI'], 'discord_guild' => $_ENV['DISCORD_GUILD_ID'], 'rcon_host' => $_ENV['RCON_HOST'], 'rcon_port' => (int)$_ENV['RCON_PORT'], 'rcon_pass' => $_ENV['RCON_PASSWORD'], 'rcon_timeout' => (int)($_ENV['RCON_TIMEOUT'] ?? 1), 'webhook_url' => $_ENV['DISCORD_WEBHOOK_URL'] ?? null, 'embed_webhook_name' => $_ENV['EMBED_WEBHOOK_USERNAME'] ?? 'Whitelist Notifier', 'embed_title' => $_ENV['EMBED_TITLE'] ?? 'New Player Whitelisted', 'embed_color' => isset($_ENV['EMBED_COLOR']) ? hexdec($_ENV['EMBED_COLOR']) : 0x00AEEF ]; $db_path = $_SERVER['DOCUMENT_ROOT'] . '/../db/whitelist.db'; if (!file_exists(dirname($db_path))) mkdir(dirname($db_path), 0755, true); // db //TODO: Change from sqlite to mysql, mainly because sqlite does not scale well, nore does it handle concurrency well. Planned deprecation in future. Build 1.1.0 try { $db = new PDO('sqlite:' . $db_path, null, null, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_PERSISTENT => true, ]); $db->exec("CREATE TABLE IF NOT EXISTS whitelist ( id INTEGER PRIMARY KEY AUTOINCREMENT, discord_id TEXT NOT NULL, discord_username TEXT NOT NULL, mc_username TEXT NOT NULL, platform TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )"); } catch (Exception $e) { die('Database error: ' . htmlspecialchars($e->getMessage())); } $http = new Client([ 'timeout' => 5, 'http_errors' => false ]); if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') { header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], true, 301); exit; } function render_header($title='Whitelist'){ global $config; echo "
'.htmlspecialchars($msg).'
';} function render_success($msg){echo ''.htmlspecialchars($msg).'
';} // rcon function rcon_send($host, $port, $pass, $cmd, $timeout = 1) { $rcon = new Rcon($host, $port, $pass, $timeout); if (!$rcon->connect()) { return ['ok' => false, 'err' => 'Unable to connect']; } $response = $rcon->sendCommand($cmd); return [ 'ok' => true, 'out' => $response ?: '' ]; } // OAUTH function http_json($client, $url, $options = []) { try { $res = $client->request($options['method'] ?? 'GET', $url, $options); $body = (string)$res->getBody(); return $body ? json_decode($body, true) : null; } catch (Exception $e) { return null; } } function discord_exchange_code($client, $id, $secret, $redirect, $code) { return http_json($client, 'https://discord.com/api/oauth2/token', [ 'method' => 'POST', 'form_params' => [ 'client_id' => $id, 'client_secret' => $secret, 'grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => $redirect ], 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ]); } function discord_get_user($client, $token) { return http_json($client, 'https://discord.com/api/users/@me', [ 'headers' => [ 'Authorization' => "Bearer $token" ] ]); } function discord_get_guilds($client, $token) { return http_json($client, 'https://discord.com/api/users/@me/guilds', [ 'headers' => [ 'Authorization' => "Bearer $token" ] ]); } // discord webhook function send_discord_webhook($client, $url, $discordUser, $mcUser, $discordId = null) { global $config; $mention = $discordId ? "<@{$discordId}>" : $discordUser; $payload = [ 'username' => $config['embed_webhook_name'], 'embeds' => [[ 'title' => $config['embed_title'], 'color' => $config['embed_color'], 'fields' => [ [ 'name' => 'Discord User', 'value' => $mention, 'inline' => true ], [ 'name' => 'Minecraft Username', 'value' => $mcUser, 'inline' => true ] ], 'timestamp' => date('c') ]] ]; try { $client->post($url, [ 'json' => $payload ]); } catch (Exception $e) { } } if(($_GET['action'] ?? '') === 'logout'){ session_destroy(); header("Location: ".$_SERVER['PHP_SELF']); exit; } if(($_GET['action'] ?? '') === 'unlink'){ if(!isset($_SESSION['access_token'])){ header("Location: ".$_SERVER['PHP_SELF']); exit; } $access_token = $_SESSION['access_token']; $user = $_SESSION['discord_user'] ?? discord_get_user($http, $access_token); if(!$user || !isset($user['id'])){ render_header('Unlink Error'); render_error('Unable to fetch your Discord info.'); render_footer(); exit; } $stmt = $db->prepare("SELECT mc_username, platform FROM whitelist WHERE discord_id = ?"); $stmt->execute([$user['id']]); $entries = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach($entries as $e){ $cmd = ($e['platform']==='bedrock') ? "fwhitelist remove .{$e['mc_username']}" : "whitelist remove {$e['mc_username']}"; rcon_send($config['rcon_host'],$config['rcon_port'],$config['rcon_pass'],$cmd,$config['rcon_timeout']); } $db->prepare("DELETE FROM whitelist WHERE discord_id = ?")->execute([$user['id']]); session_destroy(); render_header('Unlinked'); render_success('Your whitelist entry and database record have been removed.'); echo ''; render_footer(); exit; } if(($_GET['action'] ?? '') === 'remove' && isset($_GET['name'])){ if(!isset($_SESSION['access_token'])){ header("Location: ".$_SERVER['PHP_SELF']); exit; } $access_token = $_SESSION['access_token']; $user = $_SESSION['discord_user'] ?? discord_get_user($http, $access_token); if(!$user || !isset($user['id'])){ render_header('Remove Error'); render_error('Unable to fetch your Discord info.'); render_footer(); exit; } $name = trim($_GET['name']); if($name === ''){ render_header('Error'); render_error('No name provided.'); render_footer(); exit; } $stmt = $db->prepare("SELECT platform FROM whitelist WHERE discord_id = ? AND mc_username = ?"); $stmt->execute([$user['id'], $name]); $entry = $stmt->fetch(PDO::FETCH_ASSOC); if(!$entry){ render_header('Not Found'); render_error('That name is not on your whitelist.'); render_footer(); exit; } $cmd = ($entry['platform']==='bedrock') ? "fwhitelist remove .$name" : "whitelist remove $name"; rcon_send($config['rcon_host'],$config['rcon_port'],$config['rcon_pass'],$cmd,$config['rcon_timeout']); $db->prepare("DELETE FROM whitelist WHERE discord_id = ? AND mc_username = ?")->execute([$user['id'], $name]); render_header('Removed'); render_success("$name has been removed from the whitelist."); echo ''; render_footer(); exit; } if(isset($_GET['code']) && !isset($_SESSION['access_token'])){ global $http; $token = discord_exchange_code($http, $config['discord_id'],$config['discord_secret'],$config['discord_redirect'],$_GET['code'] ); if(!$token || !isset($token['access_token'])){ render_header('Discord sign-in error'); render_error('Failed to get access token.'); render_footer(); exit; } $_SESSION['access_token'] = $token['access_token']; $_SESSION['discord_user'] = discord_get_user($http, $token['access_token']); $_SESSION['discord_guilds'] = discord_get_guilds($http, $token['access_token']); header("Location: ".$_SERVER['PHP_SELF']); exit; } // check auth if(!isset($_SESSION['access_token'])){ render_header('Sign in with Discord'); $url = 'https://discord.com/api/oauth2/authorize?response_type=code&client_id=' .urlencode($config['discord_id']) .'&scope=identify+guilds&redirect_uri=' .urlencode($config['discord_redirect']); echo 'You must sign in with Discord to verify you are in our server.
'; echo 'Sign in with Discord'; render_footer(); exit; } $user = discord_get_user($http, $_SESSION['access_token']); $guilds = $_SESSION['discord_guilds'] ?? discord_get_guilds($http, $_SESSION['access_token']); $in_guild = false; foreach(($guilds ?? []) as $g) if(($g['id'] ?? '') === $config['discord_guild']) $in_guild = true; if(!$in_guild){ render_header('Not in server'); render_error('You are not a member of our Discord server.'); echo 'Logged in as '.htmlspecialchars($user['username']).' Sign out
'; render_footer(); exit; } if(empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16)); $rate_limit_seconds = 20; $allowed_name_pattern = '/^[A-Za-z0-9_\\-]{2,32}$/'; if($_SERVER['REQUEST_METHOD'] === 'POST'){ if(!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')){ render_header('Security error'); render_error('Invalid session token. Try again.'); render_footer(); exit; } if(isset($_SESSION['last_submit']) && (time()-$_SESSION['last_submit']) < $rate_limit_seconds){ render_header('Slow down'); render_error("Please wait {$rate_limit_seconds} seconds between attempts."); render_footer(); exit; } $username = trim($_POST['mc_user'] ?? ''); $platform = $_POST['platform'] ?? 'java'; if(!preg_match($allowed_name_pattern,$username)){ render_header('Invalid username'); render_error('Username must be 2-32 characters (letters, numbers, _, -).'); render_footer(); exit; } if($platform === 'bedrock' && $username[0] !== '.') $username = '.' . $username; $exists = $db->prepare("SELECT 1 FROM whitelist WHERE mc_username=?"); $exists->execute([$username]); if($exists->fetchColumn()){ render_header('Already Whitelisted'); render_success(htmlspecialchars(ltrim($username,'.')).' is already on the whitelist!'); echo 'Signed in as '.htmlspecialchars($user['username']).' - Sign out
'; render_footer(); exit; } $cmd = $platform==='bedrock' ? "fwhitelist add $username" : "whitelist add $username"; $result = rcon_send($config['rcon_host'],$config['rcon_port'],$config['rcon_pass'],$cmd,$config['rcon_timeout']); $_SESSION['last_submit']=time(); render_header('Whitelist Result'); $display = ltrim($username,'.'); if($result['ok']){ $stmt = $db->prepare("INSERT INTO whitelist (discord_id, discord_username, mc_username, platform) VALUES (?, ?, ?, ?)"); $stmt->execute([$user['id'], $user['username'], $display, $platform]); if (!empty($config['webhook_url'])) { send_discord_webhook($http, $config['webhook_url'], $user['username'], $display, $user['id']); } render_success("$display has been successfully added to the whitelist!"); } else { render_error("Error: Unable to confirm $display on the whitelist. Try again or contact an admin."); } echo 'Signed in as '.htmlspecialchars($user['username']).' - Sign out
'; render_footer(); exit; } // page index render_header('Request Minecraft Whitelist'); echo 'Signed in as '.htmlspecialchars($user['username']).' - verified member.
'; $stmt = $db->prepare("SELECT mc_username, platform, timestamp FROM whitelist WHERE discord_id = ? ORDER BY timestamp DESC"); $stmt->execute([$user['id']]); $entries = $stmt->fetchAll(PDO::FETCH_ASSOC); if($entries){ echo "