Initial commit :)

This commit is contained in:
2025-11-20 01:00:38 -08:00
commit 8fee3bf020
16 changed files with 830 additions and 0 deletions

373
public/whitelist/index.php Normal file
View File

@@ -0,0 +1,373 @@
<?php
session_start();
ob_start();
require_once $_SERVER['DOCUMENT_ROOT'] . '/../vendor/autoload.php';
use Dotenv\Dotenv;
use Thedudeguy\Rcon;
use GuzzleHttp\Client;
$dotenv = Dotenv::createImmutable($_SERVER['DOCUMENT_ROOT'] . '/..');
$dotenv->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 "<!doctype html><html><head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta name='generator' content='{$config['SITE_NAME']} " . htmlspecialchars($config['version'] ?? 'unknown') . "'>
<title>".htmlspecialchars($title)."</title>
<link rel='stylesheet' href='/assets/style.css'>
</head><body><div class='container'><h2>".htmlspecialchars($title)."</h2>";
}
function render_footer(){
global $config;
echo '<div class="footer"><p>
This page is currently in <strong title="'.htmlspecialchars($config['version']).'">Beta</strong>. If you have any issues whitelisting, please contact an admin in the Discord server.
</p></div></div></body></html>';
}
function render_error($msg){echo '<p class="error">'.htmlspecialchars($msg).'</p>';}
function render_success($msg){echo '<p class="success">'.htmlspecialchars($msg).'</p>';}
// 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 '<p class="note"><a href="'.htmlspecialchars($_SERVER['PHP_SELF']).'">Return to start</a></p>';
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 '<p class="note"><a href="'.htmlspecialchars($_SERVER['PHP_SELF']).'">Return to main page</a></p>';
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 '<p>You must sign in with Discord to verify you are in our server.</p>';
echo '<a class="btn" href="'.htmlspecialchars($url).'">Sign in with Discord</a>';
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 '<p class="note">Logged in as '.htmlspecialchars($user['username']).' <a href="?action=logout">Sign out</a></p>';
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 '<p class="note">Signed in as '.htmlspecialchars($user['username']).' - <a href="?action=logout">Sign out</a></p>';
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 '<p class="note">Signed in as '.htmlspecialchars($user['username']).' - <a href="?action=logout">Sign out</a></p>';
render_footer(); exit;
}
// page index
render_header('Request Minecraft Whitelist');
echo '<p>Signed in as '.htmlspecialchars($user['username']).' - verified member.</p>';
$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 "<h3>Your whitelisted accounts:</h3><ul id='whitelist'>";
foreach($entries as $e){
$name = htmlspecialchars($e['mc_username']);
$plat = htmlspecialchars($e['platform']);
echo "<li data-name='$name' title='Double-click to remove'>$name ($plat)</li>";
}
echo "</ul>";
}
?>
<form method="post" onsubmit="handleWhitelistSubmit(event)">
<p><input id="mc_user" name="mc_user" type="text" required placeholder="Minecraft username"></p>
<div class="platforms">
<label class="platform"><input type="radio" name="platform" value="java" checked> Java</label>
<label class="platform"><input type="radio" name="platform" value="bedrock"> Bedrock</label>
</div>
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
<p><button class="btn" id="whitelistBtn" type="submit">Request whitelist</button></p>
<p id="waitMsg">This can take a few seconds to work…</p>
<p class="note">
<a href="?action=logout">Sign out</a> |
<a href="#" id="unlinkBtn">Unlink account</a>
</p>
</form>
<script src="js/main.js"></script>
<?php render_footer(); ob_end_flush(); ?>