Initial commit :)
This commit is contained in:
20
.env.example
Normal file
20
.env.example
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
VERSION="1.0.6-beta"
|
||||||
|
|
||||||
|
SITE_NAME="yougirl city"
|
||||||
|
THEME_COLOR="#F5A9B8"
|
||||||
|
|
||||||
|
DISCORD_INVITE_LINK=
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
DISCORD_REDIRECT_URI=
|
||||||
|
DISCORD_GUILD_ID=
|
||||||
|
EMBED_WEBHOOK_USERNAME="yougirl city Notifier"
|
||||||
|
EMBED_TITLE="New Player Whitelisted"
|
||||||
|
EMBED_COLOR=F5A9B8
|
||||||
|
|
||||||
|
|
||||||
|
RCON_HOST=
|
||||||
|
RCON_PORT=
|
||||||
|
RCON_PASSWORD=
|
||||||
|
RCON_TIMEOUT=
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
vendor
|
||||||
|
composer.lock
|
||||||
|
db
|
||||||
|
.env
|
||||||
7
composer.json
Normal file
7
composer.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"vlucas/phpdotenv": "*",
|
||||||
|
"thedudeguy/rcon": "^1.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
public/403.php
Normal file
27
public/403.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable($_SERVER['DOCUMENT_ROOT'] . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>403 Forbidden | <?= $_ENV['SITE_NAME'] ?></title>
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<meta name="theme-color" content="<?= $_ENV['THEME_COLOR'] ?>" />
|
||||||
|
<meta generator='tgirl.city <?= htmlspecialchars($_ENV['VERSION']) ?>'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>403</h1>
|
||||||
|
<p>Hey! You sneaky one, we can have you seeing this</p>
|
||||||
|
<a href="/" class="btn">Back to <?= $_ENV['SITE_NAME'] ?>!</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
public/404.php
Normal file
27
public/404.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable($_SERVER['DOCUMENT_ROOT'] . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>404 Not Found | <?= $_ENV['SITE_NAME'] ?></title>
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<meta name="theme-color" content="<?= $_ENV['THEME_COLOR'] ?>" />
|
||||||
|
<meta generator='tgirl.city <?= htmlspecialchars($_ENV['VERSION']) ?>'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>404</h1>
|
||||||
|
<p>Oops! this page doesn't exist!</p>
|
||||||
|
<a href="/" class="btn">Back to <?= $_ENV['SITE_NAME'] ?>!</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
281
public/assets/style.css
Normal file
281
public/assets/style.css
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
@import url(https://fonts.bunny.net/css2?family=Poppins:wght@400;600&family=Press+Start+2P&display=swap);
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg1: #2a003f;
|
||||||
|
--bg2: #001d2d;
|
||||||
|
--accent1: #ff7ad0;
|
||||||
|
--accent2: #65d6ff;
|
||||||
|
--text: #f4f4f7;
|
||||||
|
--subtext: #bbb;
|
||||||
|
--card-bg: rgba(20, 20, 30, 0.75);
|
||||||
|
--shadow: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset and Base */
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Poppins, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
background: radial-gradient(circle at top left, var(--bg1), transparent 70%),
|
||||||
|
radial-gradient(circle at bottom right, var(--bg2), transparent 70%),
|
||||||
|
#0a0a0f;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
background: var(--card-bg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 32px 20px 28px;
|
||||||
|
box-shadow: 0 8px 32px var(--shadow);
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
animation: fadeIn 0.8s cubic-bezier(0.33, 1, 0.68, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings */
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-family: "Press Start 2P", monospace;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--text);
|
||||||
|
text-shadow: 0 0 8px rgba(255, 122, 208, 0.6);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(20px, 5vw, 26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: clamp(16px, 4vw, 22px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
p {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: clamp(14px, 4vw, 16px);
|
||||||
|
color: var(--subtext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links as buttons */
|
||||||
|
a.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 22px;
|
||||||
|
font-size: clamp(14px, 4vw, 16px);
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(90deg, var(--accent1), var(--accent2));
|
||||||
|
background-size: 200% 200%;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow:
|
||||||
|
0 0 12px rgba(255, 122, 208, 0.4),
|
||||||
|
0 0 18px rgba(101, 214, 255, 0.3);
|
||||||
|
animation: pulse 2.8s ease-in-out infinite, gradientShift 6s ease infinite;
|
||||||
|
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.btn:hover,
|
||||||
|
a.btn:focus {
|
||||||
|
transform: translateY(-2px) scale(1.07);
|
||||||
|
box-shadow:
|
||||||
|
0 0 20px rgba(255, 122, 208, 0.7),
|
||||||
|
0 0 28px rgba(101, 214, 255, 0.6);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
input[type="text"] {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 380px;
|
||||||
|
padding: 13px 15px;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(15, 15, 20, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: var(--accent1);
|
||||||
|
background: rgba(25, 25, 35, 0.9);
|
||||||
|
box-shadow:
|
||||||
|
0 0 10px rgba(255, 122, 208, 0.4),
|
||||||
|
0 0 18px rgba(101, 214, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
button.btn {
|
||||||
|
all: unset;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 28px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, var(--accent1), var(--accent2));
|
||||||
|
box-shadow:
|
||||||
|
0 0 12px rgba(255, 122, 208, 0.3),
|
||||||
|
0 0 20px rgba(101, 214, 255, 0.2),
|
||||||
|
inset 0 0 10px rgba(255, 255, 255, 0.05);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
min-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn:hover {
|
||||||
|
transform: translateY(-1px) scale(1.05);
|
||||||
|
box-shadow:
|
||||||
|
0 0 20px rgba(255, 122, 208, 0.5),
|
||||||
|
0 0 35px rgba(101, 214, 255, 0.4),
|
||||||
|
inset 0 0 15px rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
box-shadow: 0 0 8px rgba(255, 122, 208, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Note */
|
||||||
|
.note {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note a {
|
||||||
|
color: var(--accent2);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease, text-decoration 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note a:hover {
|
||||||
|
color: var(--accent1);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 20px auto;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
background: rgba(15, 15, 25, 0.7);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-left: 4px solid var(--accent1);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow:
|
||||||
|
0 0 10px rgba(255, 122, 208, 0.15),
|
||||||
|
inset 0 0 6px rgba(255, 255, 255, 0.05);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li::before {
|
||||||
|
content: "•";
|
||||||
|
color: var(--accent1);
|
||||||
|
margin-right: 8px;
|
||||||
|
text-shadow: 0 0 6px var(--accent1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platforms {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.container {
|
||||||
|
padding: 24px 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.error{
|
||||||
|
color:var(--accent1);
|
||||||
|
font-weight:600;
|
||||||
|
}
|
||||||
|
.success{
|
||||||
|
color:var(--accent2);
|
||||||
|
font-weight:600;
|
||||||
|
}
|
||||||
|
.footer{
|
||||||
|
margin-top:40px;
|
||||||
|
padding:15px;
|
||||||
|
background:rgba(255,255,255,0.05);
|
||||||
|
border-radius:10px;
|
||||||
|
text-align:center;
|
||||||
|
font-size:14px;
|
||||||
|
color:#bbb
|
||||||
|
}
|
||||||
|
#mc_user{
|
||||||
|
padding:10px;
|
||||||
|
width:80%;
|
||||||
|
border-radius:8px;
|
||||||
|
border:none;
|
||||||
|
background:#111;
|
||||||
|
color:#fff
|
||||||
|
}
|
||||||
|
#waitMsg{
|
||||||
|
display:none;
|
||||||
|
color:#bbb;
|
||||||
|
font-size:14px;
|
||||||
|
}
|
||||||
|
#unlinkBtn{
|
||||||
|
color:#ff7ad0;
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/apple-touch-icon.png
Normal file
BIN
public/images/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
public/images/favicon-96x96.png
Normal file
BIN
public/images/favicon-96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
3
public/images/favicon.svg
Normal file
3
public/images/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 38 KiB |
21
public/images/site.webmanifest
Normal file
21
public/images/site.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "tgirl city",
|
||||||
|
"short_name": "tgirl city",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/images/web-app-manifest-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/images/web-app-manifest-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#F5A9B8",
|
||||||
|
"background_color": "#F5A9B8",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
BIN
public/images/web-app-manifest-192x192.png
Normal file
BIN
public/images/web-app-manifest-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
public/images/web-app-manifest-512x512.png
Normal file
BIN
public/images/web-app-manifest-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
31
public/index.php
Normal file
31
public/index.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable($_SERVER['DOCUMENT_ROOT'] . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title><?= $_ENV['SITE_NAME'] ?></title>
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<meta property="og:title" content="<?= $_ENV['SITE_NAME'] ?>" />
|
||||||
|
<meta property="og:description" content="The cute Discord for transfems" />
|
||||||
|
<meta property="og:url" content="https://tgirl.city" />
|
||||||
|
<meta name="theme-color" content="<?= $_ENV['THEME_COLOR'] ?>" />
|
||||||
|
<meta name="description" content="The cute Discord for transfems" />
|
||||||
|
<meta generator='tgirl.city <?= htmlspecialchars($_ENV['VERSION']) ?>'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1><?= $_ENV['SITE_NAME'] ?></h1>
|
||||||
|
<p>The discord for transfems</p>
|
||||||
|
<a href="<?= $_ENV['DISCORD_INVITE_LINK'] ?>" class="btn" target="_blank">Join Our Discord!</a>
|
||||||
|
<div class="note" title="Fuck off!">(not a fetish server, no chasers allowed!)</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
373
public/whitelist/index.php
Normal file
373
public/whitelist/index.php
Normal 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(); ?>
|
||||||
36
public/whitelist/js/main.js
Normal file
36
public/whitelist/js/main.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const unlinkBtn = document.getElementById("unlinkBtn");
|
||||||
|
if (unlinkBtn) {
|
||||||
|
unlinkBtn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (confirm("Are you sure you want to unlink? This will remove your whitelist and delete your entry from our database.")) {
|
||||||
|
window.location.href = "?action=unlink";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const whitelistForm = document.querySelector("form");
|
||||||
|
if (whitelistForm) {
|
||||||
|
whitelistForm.addEventListener("submit", () => {
|
||||||
|
const btn = document.getElementById("whitelistBtn");
|
||||||
|
const msg = document.getElementById("waitMsg");
|
||||||
|
if (btn && msg) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Please wait…";
|
||||||
|
msg.style.display = "block";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const list = document.getElementById("whitelist");
|
||||||
|
if (list) {
|
||||||
|
list.addEventListener("dblclick", (ev) => {
|
||||||
|
const li = ev.target.closest("li[data-name]");
|
||||||
|
if (!li) return;
|
||||||
|
const name = li.getAttribute("data-name");
|
||||||
|
if (confirm(`Remove ${name} from whitelist?`)) {
|
||||||
|
li.style.opacity = 0.5;
|
||||||
|
li.textContent = `Removing ${name}…`;
|
||||||
|
window.location.href = `?action=remove&name=${encodeURIComponent(name)}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user