mirror of
https://github.com/a-sync/game-server-watcher.git
synced 2026-07-22 05:41:43 -04:00
Merge branch 'master' into cloud
This commit is contained in:
34
dist/server.js
vendored
34
dist/server.js
vendored
@@ -23,20 +23,25 @@ let loop;
|
||||
if (DBG)
|
||||
console.log('DBG: %j %j', (new Date()), req.url);
|
||||
const reqUrl = new url_1.URL(req.url || '', 'http://localhost');
|
||||
const reqPath = reqUrl.pathname.split('/');
|
||||
if (reqUrl.pathname === '/') {
|
||||
const p = reqUrl.pathname === '/' ? 'index.html' : reqUrl.pathname.slice(1);
|
||||
if (['index.html', 'main.css', 'main.js'].includes(p)) {
|
||||
if (SECRET !== '') {
|
||||
let ct = 'text/html';
|
||||
if (p === 'main.css')
|
||||
ct = 'text/css';
|
||||
else if (p === 'main.js')
|
||||
ct = 'text/javascript';
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html',
|
||||
'Content-Type': ct,
|
||||
'Cache-Control': 'max-age=' + String(CACHE_MAX_AGE)
|
||||
});
|
||||
fs_1.default.createReadStream('./index.html').pipe(res);
|
||||
fs_1.default.createReadStream('./public/' + p).pipe(res);
|
||||
}
|
||||
else {
|
||||
res.end('Configure the `SECRET` env var to enable the web UI!');
|
||||
}
|
||||
}
|
||||
else if (reqUrl.pathname === '/ping') {
|
||||
else if (p === 'ping') {
|
||||
if (DBG)
|
||||
console.log('ping');
|
||||
res.end('pong');
|
||||
@@ -45,15 +50,16 @@ let loop;
|
||||
let status = 200;
|
||||
let re = {};
|
||||
if (validateBearerToken(String(req.headers['x-btoken']))) {
|
||||
const reqPath = p.split('/');
|
||||
try {
|
||||
if (reqPath[1] === 'features') {
|
||||
if (reqPath[0] === 'features') {
|
||||
re.features = {
|
||||
steam: FEET_STEAM,
|
||||
discord: FEET_DISCORD,
|
||||
telegram: FEET_TELEGRAM
|
||||
};
|
||||
}
|
||||
else if (reqPath[1] === 'config') {
|
||||
else if (reqPath[0] === 'config') {
|
||||
if (req.method === 'GET') {
|
||||
re.config = await (0, watcher_1.readConfig)();
|
||||
}
|
||||
@@ -77,9 +83,9 @@ let loop;
|
||||
re.error = 'Invalid Request';
|
||||
}
|
||||
}
|
||||
else if (reqPath[1] === 'flush' && ['servers', 'discord', 'telegram'].includes(reqPath[2])) {
|
||||
await restart(reqPath[2]);
|
||||
re.message = '🗑️ ' + reqPath[2].slice(0, 1).toUpperCase() + reqPath[2].slice(1) + ' data flushed.';
|
||||
else if (reqPath[0] === 'flush' && ['servers', 'discord', 'telegram'].includes(reqPath[1])) {
|
||||
await restart(reqPath[1]);
|
||||
re.message = '🗑️ ' + reqPath[1].slice(0, 1).toUpperCase() + reqPath[1].slice(1) + ' data flushed.';
|
||||
}
|
||||
else {
|
||||
status = 400;
|
||||
@@ -105,8 +111,9 @@ let loop;
|
||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||
res.end('<html><head></head><body>404 💢</body></html>');
|
||||
}
|
||||
}).listen(APP_PORT);
|
||||
console.log('Web service started %s:%s', APP_HOST, APP_PORT);
|
||||
}).listen(APP_PORT, APP_HOST, () => {
|
||||
console.log('Web service started %s:%s', APP_HOST, APP_PORT);
|
||||
});
|
||||
(0, watcher_1.main)().then(l => {
|
||||
loop = l;
|
||||
});
|
||||
@@ -136,7 +143,8 @@ function validateBearerToken(btoken) {
|
||||
if (salt.length > 24
|
||||
&& /^\d{13}$/.test(valid)
|
||||
&& /^[a-f0-9]{128}$/.test(hash)
|
||||
&& Date.now() < Number(valid)) {
|
||||
&& Date.now() < Number(valid)
|
||||
&& Number(valid) - Date.now() < 3600000 * 12) {
|
||||
return hash === crypto_1.default.createHash('sha512').update(salt + valid + SECRET).digest('hex');
|
||||
}
|
||||
return false;
|
||||
|
||||
816
index.html
816
index.html
@@ -1,816 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>GSW Control Panel</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://i.imgur.com/2Ok3pxv.png" rel="icon" type="image/png">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/@forevolve/bootstrap-dark@latest/dist/css/toggle-bootstrap-dark.min.css" rel="stylesheet"> -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Marvel&family=Roboto&display=swap" rel="stylesheet">
|
||||
<link href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@latest/dist/css/select2.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/css/jsoneditor.min.css" rel="stylesheet">
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.slim.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/node-forge@1.3.1/dist/forge.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@latest/dist/purify.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mustache@latest/mustache.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@latest/dist/js/select2.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #000;
|
||||
background-position: top center;
|
||||
transition: background-image 2s ease-in-out 0.5s;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-image: url('https://i.imgur.com/lTly23N.png');
|
||||
}
|
||||
|
||||
body>nav {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.navbar-brand span {
|
||||
color: #fff;
|
||||
font-family: 'Marvel', Helvetica, Arial, sans-serif;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 0 4px #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
zoom: 2;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#protected-section>div>div {
|
||||
background-color: rgba(245, 250, 255, 0.75);
|
||||
}
|
||||
|
||||
#config-form h3.card_title>label {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
#config-form h3>label {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#config-form div[data-schemaid="root"]>div.card-body>div>div {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
#config-form div[data-schemaid="root"]>div.card-body>div>div:last-child {
|
||||
margin-bottom: inherit;
|
||||
}
|
||||
|
||||
#config-form button {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#config-form input[type=checkbox] {
|
||||
vertical-align: middle;
|
||||
zoom: 2;
|
||||
cursor: pointer;
|
||||
margin-right: 0.25rem !important;
|
||||
}
|
||||
|
||||
#config-form .btn-group {
|
||||
display: inline-flex !important;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
#config-form .btn-group button {
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
#config-form .btn-group.je-object__controls {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-add {
|
||||
background-color: darkgreen;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-delete {
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
footer>a>svg {
|
||||
fill: #c6cbd1;
|
||||
}
|
||||
|
||||
#save-config {
|
||||
background-color: rgba(15, 20, 25, 0.9);
|
||||
}
|
||||
|
||||
#save-config ._saving_btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#save-config ._btn ._saving_btn {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
#save-config ._btn ._save_btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#bye>h3 {
|
||||
text-shadow: 0 0 6px #fff;
|
||||
}
|
||||
|
||||
#bye>div>a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
#bye kbd {
|
||||
box-shadow: 0 0.125rem 0.25rem rgb(255, 255, 255, 0.75) !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav id="top-menu" class="navbar navbar-expand-lg navbar-dark invisible">
|
||||
<span class="navbar-brand mb-0 h1">
|
||||
<img src="https://i.imgur.com/2Ok3pxv.png" width="30" height="30" class="d-inline-block align-middle mr-sm-1 mr-md-2 mr-lg-3" alt="">
|
||||
<span>GSW Control Panel</span>
|
||||
</span>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a id="top-menu-configuration" class="nav-link active" href="#configuration">Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a id="top-menu-flush-data" class="nav-link" href="#flush-data">Flush data</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0">
|
||||
<button id="logout" class="btn btn-outline-danger" type="submit">Logout</button>
|
||||
</form>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
<section id="protected-section" class="row d-none">
|
||||
<div class="col-12">
|
||||
<div id="configuration" class="p-4 m-2 bg-light border border-white rounded-lg shadow-lg">
|
||||
<form method="post" id="config-form"></form>
|
||||
</div>
|
||||
<div id="flush-data" class="p-4 m-2 bg-light border border-white rounded-lg d-none shadow-lg">
|
||||
<div id="flush-data-container">
|
||||
<h3 class="card-title">Flush data</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-lg btn-danger" data-api="flush/servers">Flush
|
||||
game servers data</button>
|
||||
<button type="button" class="btn btn-lg btn-primary" data-api="flush/discord">Flush
|
||||
discord data</button>
|
||||
<button type="button" class="btn btn-lg btn-info" data-api="flush/telegram">Flush
|
||||
telegram data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="unprotected-section">
|
||||
<div class="col-12 text-center sticky-top">
|
||||
<div id="spinner" class="spinner-border m-2 justify-content-center text-primary mx-auto"></div>
|
||||
</div>
|
||||
<div id="bye" class="p-4 m-2 d-none">
|
||||
<h3 class="text-light text-center">
|
||||
Refresh the page to login...
|
||||
</h3>
|
||||
<div class="text-center">
|
||||
<a href=""><kbd class="bg-light text-dark d-inline-block">F5</kbd></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="toast-containers" class="container fixed-bottom mb-1 mb-lg-2 mb-xl-3">
|
||||
<div id="notif-container" class="col-12 mx-auto col-lg-10 col-xl-8">
|
||||
</div>
|
||||
<div id="action-container" class="col-12 mx-auto col-lg-10 col-xl-8">
|
||||
<div id="save-config" class="alert rounded d-none animate__animated animate__fadeInUp animate__faster shadow" role="alert">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex-inline text-light">Careful — you have unsaved changes!</div>
|
||||
<form class="d-flex-inline mr-2 mr-lg-0">
|
||||
<button id="save-config-reset" class="btn btn-sm text-light" type="reset">Reset</button>
|
||||
<button id="save-config-submit" class="btn btn-sm btn-success" type="submit">
|
||||
<span class="_save_btn">Save</span>
|
||||
<span class="spinner-grow spinner-grow-sm _saving_btn" role="status" aria-hidden="true"></span>
|
||||
<span class="_saving_btn">Saving...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mx-auto text-center py-2 d-none">
|
||||
<a href="https://github.com/a-sync/game-server-watcher"><svg width="16" height="16" viewBox="0 0 16 16" version="1.1" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z">
|
||||
</path>
|
||||
</svg></a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const id = s => document.getElementById(s);
|
||||
|
||||
let configEditor;
|
||||
$(async () => {
|
||||
const gswFeatures = await fetchApi('GET', 'features');
|
||||
if (gswFeatures) {
|
||||
await setRandomBg().catch(() => { });
|
||||
setInterval(setRandomBg, 60000);
|
||||
|
||||
let unsavedChanges = false;
|
||||
const confirmText = 'You have unsaved changes! Are you sure you want to ';
|
||||
$(window).on('beforeunload', e => {
|
||||
if (unsavedChanges) return confirmText + 'navigate away?';
|
||||
});
|
||||
$('#logout').click(e => {
|
||||
e.preventDefault();
|
||||
if (unsavedChanges && !confirm(confirmText + 'log out?')) return;
|
||||
logout();
|
||||
});
|
||||
|
||||
activateMenu();
|
||||
$('body > nav > ul > li > a').click(e => {
|
||||
e.preventDefault();
|
||||
activateMenu(e.currentTarget);
|
||||
});
|
||||
|
||||
$('#top-menu').removeClass('invisible');
|
||||
$('body > footer').removeClass('d-none');
|
||||
|
||||
// Configuration
|
||||
const gswConfig = await fetchApi('GET', 'config');
|
||||
if (gswConfig && gswConfig.config) {
|
||||
configEditor = new JSONEditor(id('config-form'), {
|
||||
"theme": "bootstrap4",
|
||||
"iconlib": "fontawesome5",
|
||||
"object_layout": "normal",
|
||||
"template": "mustache",
|
||||
"show_errors": "always",
|
||||
"required_by_default": 0,
|
||||
"no_additional_properties": 1,
|
||||
"display_required_only": 0,
|
||||
"remove_empty_properties": 1,
|
||||
"keep_oneof_values": 0,
|
||||
"ajax": 0,
|
||||
"ajaxCredentials": 0,
|
||||
"show_opt_in": 1,
|
||||
"disable_edit_json": 0,
|
||||
"disable_collapse": 1,
|
||||
"disable_properties": 1,
|
||||
"disable_array_add": 0,
|
||||
"disable_array_reorder": 1,
|
||||
"disable_array_delete": 0,
|
||||
"enable_array_copy": 1,
|
||||
"array_controls_top": 0,
|
||||
"disable_array_delete_all_rows": 1,
|
||||
"disable_array_delete_last_row": 1,
|
||||
"prompt_before_delete": 1,
|
||||
"schema": {
|
||||
"title": "Configuration",
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"format": "tabs-top",
|
||||
"items": {
|
||||
"title": "Game Server",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"host",
|
||||
"port"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Name",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Gamedig type",
|
||||
"description": "Look for the <i>GameDig Type ID</i> in the <a href=\"https://github.com/a-sync/node8-gamedig#games-list\">games list</a>.",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"appId": {
|
||||
"title": "Steam App ID",
|
||||
"description": "Look for the <i>AppID</i> in the <a href=\"https://steamdb.info/apps/\">apps list</a>.",
|
||||
"type": "integer",
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"title": "Host name or IP",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"port": {
|
||||
"title": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"updateIntervalMinutes": {
|
||||
"title": "Update interval (minutes) [NOT IMPLEMENTED]",
|
||||
"type": "integer",
|
||||
"default": 5,
|
||||
"minimum": 1,
|
||||
"maximum": 60,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"graphHistoryHours": {
|
||||
"title": "Graph history time span (hours)",
|
||||
"type": "integer",
|
||||
"default": 12,
|
||||
"minimum": 1,
|
||||
"maximum": 24,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"timezoneOffset": {
|
||||
"title": "Time zone offset of the server",
|
||||
"default": 0,
|
||||
"type": "number",
|
||||
"minimum": -12,
|
||||
"maximum": 14,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 6,
|
||||
"grid_break": true
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"type": "array",
|
||||
"title": "Discord channels",
|
||||
"description": "The simplest way to get a discord channel ID is to enable developer mode in settings, then right clicking on the channel name will give you the <b>Copy ID</b> option.",
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Channel",
|
||||
"required": [
|
||||
"channelId"
|
||||
],
|
||||
"properties": {
|
||||
"channelId": {
|
||||
"title": "Channel ID",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "table",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"type": "array",
|
||||
"title": "Telegram chats",
|
||||
"description": "The simplest way to get a telegram chat ID is to invite <a target=\"_blank\" href=\"https://telegram.me/getidsbot\">@getidsbot</a> and use <code>/start</code> to make it post (among other data) the chat ID.",
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Chat",
|
||||
"required": [
|
||||
"chatId"
|
||||
],
|
||||
"properties": {
|
||||
"chatId": {
|
||||
"type": "string",
|
||||
"title": "Chat ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "table",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "grid",
|
||||
"headerTemplate": "{{#self.name}}{{self.name}}{{/self.name}}{{^self.name}}{{self.type}}:{{self.host}}:{{self.port}}{{/self.name}}"
|
||||
}
|
||||
},
|
||||
startval: gswConfig.config
|
||||
});
|
||||
|
||||
configEditor.on('ready', () => {
|
||||
addExportButton();
|
||||
$('#config-form').submit(e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
$('#spinner').addClass('d-none');
|
||||
$('#protected-section').removeClass('d-none');
|
||||
|
||||
let editorCurrVal = configEditor.getValue();
|
||||
let formCurrVal = actualFormValues();
|
||||
$('#config-form input, #config-form textarea').keyup(() => {
|
||||
unsavedChanges = checkForChanges(formCurrVal);
|
||||
});
|
||||
configEditor.on('change', () => {
|
||||
unsavedChanges = checkForChanges(formCurrVal);
|
||||
});
|
||||
|
||||
$('#save-config-submit').click(async e => {
|
||||
e.preventDefault();
|
||||
if (!configEditor.isEnabled()) return;
|
||||
configEditor.disable();
|
||||
$(e.currentTarget).addClass('_btn');
|
||||
|
||||
const uerr = configEditor.validate();
|
||||
if (uerr.length) {
|
||||
notif('warning', 'Validation errors', uerr.map(err => err.path + ' ' + err.message).join('<br>'), undefined, 3 + uerr.length);
|
||||
} else {
|
||||
const res = await fetchApi('POST', 'config', configEditor.getValue());
|
||||
if (res && res.message) {
|
||||
editorCurrVal = configEditor.getValue();
|
||||
formCurrVal = actualFormValues();
|
||||
unsavedChanges = false;
|
||||
dismissSaveConfig();
|
||||
notif('success', '✔️ Changes saved successfully.', undefined, undefined, 3);
|
||||
} else {
|
||||
notif('danger', '⚠️ Error while saving config.', (res ? res.error : undefined), undefined, 3);
|
||||
}
|
||||
}
|
||||
|
||||
$(e.currentTarget).removeClass('_btn');
|
||||
configEditor.enable();
|
||||
});
|
||||
|
||||
$('#save-config-reset').click(e => {
|
||||
e.preventDefault();
|
||||
if (!configEditor.isEnabled()) return;
|
||||
configEditor.setValue(editorCurrVal, true);
|
||||
unsavedChanges = false;
|
||||
dismissSaveConfig();
|
||||
if (formCurrVal !== actualFormValues()) {
|
||||
document.location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
notif('danger', '⚠️ Error while loading config.');
|
||||
}
|
||||
|
||||
// Flush data
|
||||
$('button[data-api]').click(async e => {
|
||||
e.preventDefault();
|
||||
const endpoint = e.currentTarget.dataset.api;
|
||||
|
||||
if (confirm('Are you sure you want to call `' + endpoint + '`?')) {
|
||||
const res = await fetchApi('GET', endpoint);
|
||||
if (res && res.message) {
|
||||
notif('success', res.message, undefined, undefined, 3);
|
||||
} else {
|
||||
notif('danger', (res ? res.error : undefined), undefined, undefined, 3);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function dismissSaveConfig() {
|
||||
$('#save-config').addClass('animate__fadeOutDown');
|
||||
setTimeout(() => {
|
||||
if ($('#save-config').hasClass('animate__fadeOutDown')) {
|
||||
$('#save-config').addClass('d-none');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// NOTE: sadly configEditor.getValue() can not be trusted when the
|
||||
// change event originates from a keypress event
|
||||
function actualFormValues() {
|
||||
const formItems = $('#config-form .tab-pane:not([style*="display: none"]) input:enabled, #config-form .tab-pane:not([style*="display: none"]) textarea:enabled, #config-form .tab-pane:not([style*="display: none"]) select:enabled').toArray();
|
||||
return JSON.stringify(formItems.map(e => {
|
||||
if (e.type === 'checkbox') {
|
||||
return e.checked ? e.value : undefined;
|
||||
}
|
||||
return e.value;
|
||||
}));
|
||||
}
|
||||
|
||||
function checkForChanges(formCurrVal) {
|
||||
if (formCurrVal !== actualFormValues()) {
|
||||
$('#save-config').removeClass('d-none');
|
||||
$('#save-config').removeClass('animate__fadeOutDown');
|
||||
return true;
|
||||
} else {
|
||||
dismissSaveConfig();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function addExportButton() {
|
||||
const button_holder = configEditor.root.theme.getHeaderButtonHolder();
|
||||
const exportBtn = configEditor.root.getButton('Export', 'save', 'Export As JSON File');
|
||||
button_holder.appendChild(exportBtn);
|
||||
configEditor.root.header.parentNode.insertBefore(button_holder, configEditor.root.header.nextSibling);
|
||||
exportBtn.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
const dt = (new Date()).toISOString().slice(0, 19).replace(/\D/g, '');
|
||||
const filename = dt + '-gsw.config.json';
|
||||
const blob = new Blob([JSON.stringify(configEditor.getValue(), null, 2)], {
|
||||
type: 'application/json;charset=utf-8'
|
||||
});
|
||||
|
||||
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
|
||||
window.navigator.msSaveOrOpenBlob(blob, filename);
|
||||
} else {
|
||||
const a = document.createElement('a');
|
||||
a.download = filename;
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.dataset.downloadurl = ['text/plain', a.download, a.href].join(':');
|
||||
|
||||
a.dispatchEvent(new MouseEvent('click', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': false
|
||||
}));
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
$(window).off('beforeunload');
|
||||
$('#save-config').addClass('d-none');
|
||||
$('#top-menu').addClass('invisible');
|
||||
$('#protected-section').addClass('d-none');
|
||||
$('#spinner').addClass('d-none');
|
||||
$('#bye').removeClass('d-none');
|
||||
$('body > footer').addClass('d-none');
|
||||
if (configEditor) configEditor.destroy();
|
||||
window.localStorage.removeItem('btoken');
|
||||
}
|
||||
|
||||
function notif(type, html1, html2, html3, dismissable) {
|
||||
let closeTimeout = 0;
|
||||
if (typeof dismissable === 'number') {
|
||||
closeTimeout = dismissable;
|
||||
dismissable = true;
|
||||
}
|
||||
|
||||
const el = $('<div/>', { class: 'alert alert-dismissible fade show shadow alert-' + type, role: 'alert' });
|
||||
el.attr('role', 'alert');
|
||||
if (html2) {
|
||||
const h4 = $('<h4/>', { class: 'alert-heading', html: html1 });
|
||||
el.append(h4);
|
||||
|
||||
const p1 = $('<p/>', { html: html2 });
|
||||
el.append(p1);
|
||||
if (html3) {
|
||||
el.append($('<hr/>'));
|
||||
const p2 = $('<p/>', { class: 'mb-0', html: html3 });
|
||||
el.append(p2);
|
||||
}
|
||||
} else {
|
||||
el.html(html1);
|
||||
}
|
||||
|
||||
if (dismissable !== false) {
|
||||
const button = $('<button/>', { class: 'close', type: 'button', 'data-dismiss': 'alert', 'aria-label': 'Close' });
|
||||
|
||||
const span = $('<span/>', { 'aria-hidden': 'true', html: '×' });
|
||||
button.append(span);
|
||||
el.append(button);
|
||||
}
|
||||
|
||||
el.alert();
|
||||
|
||||
if (closeTimeout > 0) {
|
||||
const a = $(el);
|
||||
const timer = setTimeout(a => {
|
||||
a.alert('close');
|
||||
}, 1000 * closeTimeout, a);
|
||||
a.on('close.bs.alert', function () {
|
||||
clearTimeout(timer);
|
||||
})
|
||||
}
|
||||
|
||||
$('#notif-container').append(el);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function activateMenu(target) {
|
||||
if (!target) {
|
||||
target = id('top-menu-configuration');
|
||||
if (document.location.hash.length > 1) {
|
||||
const hashTargetMenu = id('top-menu-' + document.location.hash.slice(1));
|
||||
if (hashTargetMenu) {
|
||||
target = hashTargetMenu;
|
||||
}
|
||||
} else if (window.localStorage.getItem('active.menu')) {
|
||||
const stateTargetMenu = id('top-menu-' + window.localStorage.getItem('active.menu').slice(1));
|
||||
if (stateTargetMenu) {
|
||||
target = stateTargetMenu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const menu = target.getAttribute('href');
|
||||
const menuItems = document.querySelectorAll('#top-menu > ul > li > a');
|
||||
for (const a of menuItems) {
|
||||
const at = a.getAttribute('href');
|
||||
if (at === menu) {
|
||||
$(at).removeClass('d-none');
|
||||
$(a).addClass('active');
|
||||
|
||||
window.history.replaceState(null, null, document.location.pathname + menu);
|
||||
window.localStorage.setItem('active.menu', menu);
|
||||
|
||||
const titleArr = document.title.split(' #');
|
||||
if (titleArr.length < 2) {
|
||||
titleArr.push(menu.slice(1));
|
||||
} else {
|
||||
titleArr[titleArr.length - 1] = menu.slice(1);
|
||||
}
|
||||
|
||||
document.title = titleArr.join(' #');
|
||||
} else {
|
||||
$(at).addClass('d-none');
|
||||
$(a).removeClass('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchApi(method, endpoint, body) {
|
||||
let re = null;
|
||||
let errMsg = '';
|
||||
const retries = 2;
|
||||
try {
|
||||
for (let i = 1; i <= retries; i++) {
|
||||
errMsg = '';
|
||||
const btoken = await getBearerToken();
|
||||
if (btoken) {
|
||||
const res = await fetch('/' + endpoint, {
|
||||
method,
|
||||
body: (body ? JSON.stringify(body) : undefined),
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'x-btoken': btoken,
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
re = false;
|
||||
window.localStorage.removeItem('btoken');
|
||||
errMsg = 'Invalid token.';
|
||||
} else if (res.status === 200) {
|
||||
re = await res.json();
|
||||
break;
|
||||
} else errMsg = 'Unexpected API status.';
|
||||
} else {
|
||||
re = false;
|
||||
errMsg = 'Authentication error.';
|
||||
}
|
||||
if (errMsg !== '') console.error(errMsg + ' method, endpoint', method, endpoint);
|
||||
}
|
||||
} catch (err) {
|
||||
re = null;
|
||||
errMsg = 'Unexpected error.';
|
||||
console.error('fetchApi: method, endpoint, err, re', method, endpoint, err, re);
|
||||
}
|
||||
|
||||
if (!re) {
|
||||
logout();
|
||||
notif('danger', '⚠️ ' + errMsg, 3);
|
||||
}
|
||||
|
||||
return re;
|
||||
}
|
||||
|
||||
async function getBearerToken() {
|
||||
const bt = window.localStorage.getItem('btoken');
|
||||
if (bt) {
|
||||
const salt = bt.slice(0, bt.length - 141);
|
||||
const valid = bt.slice(-141, -128);
|
||||
const hash = bt.slice(-128);
|
||||
if (salt.length > 24
|
||||
&& /^\d{13}$/.test(valid)
|
||||
&& /^[0-9a-f]{128}$/.test(hash)
|
||||
&& Date.now() < Number(valid)) {
|
||||
return bt;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use modalDialog
|
||||
// if (typeof window.showModalDialog === 'function') {} else
|
||||
if (typeof HTMLDialogElement === 'function') {
|
||||
const ans = await prompt('Admin secret');
|
||||
|
||||
if (ans) window.localStorage.setItem('btoken', await generateBearerToken(ans));
|
||||
else window.localStorage.removeItem('btoken');
|
||||
} else {
|
||||
notif('primary', '⚠️ Close the tab to re-enable dialog boxes.');
|
||||
}
|
||||
|
||||
return window.localStorage.getItem('btoken');
|
||||
}
|
||||
|
||||
async function generateBearerToken(secret) {
|
||||
const valid = String(Date.now() + 3600000 * 12); // Note: valid for 12 hours
|
||||
const salt = Array.from({ length: 3 }).map(() => Math.random().toString(36).slice(2)).join('');
|
||||
let hash;
|
||||
if (crypto.subtle) {
|
||||
const buff = await crypto.subtle.digest('SHA-512', new TextEncoder('utf-8').encode(salt + valid + secret));
|
||||
hash = Array.from(new Uint8Array(buff)).map(x => ('00' + x.toString(16)).slice(-2)).join('');
|
||||
} else {
|
||||
const md = forge.md.sha512.create();
|
||||
md.update(salt + valid + secret);
|
||||
hash = md.digest().toHex();
|
||||
}
|
||||
|
||||
return salt + valid + hash;
|
||||
}
|
||||
|
||||
function setRandomBg() {
|
||||
const bgs = [
|
||||
'https://i.imgur.com/bDzhwG5.png',
|
||||
'https://i.imgur.com/rA8JXuI.png',
|
||||
'https://i.imgur.com/pstAPIw.png',
|
||||
'https://i.imgur.com/gQD3xfo.png',
|
||||
'https://i.imgur.com/iKTfM8z.png'
|
||||
];
|
||||
|
||||
const bgImg = new Image();
|
||||
bgImg.src = bgs[Math.floor(Math.random() * bgs.length)];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
bgImg.onload = () => {
|
||||
document.body.style.backgroundAttachment = 'scroll';
|
||||
document.body.style.backgroundRepeat = 'repeat';
|
||||
document.body.style.backgroundSize = 'auto';
|
||||
document.body.style.backgroundImage = 'url(' + bgImg.src + ')';
|
||||
return resolve(bgImg);
|
||||
};
|
||||
|
||||
bgImg.onerror = () => {
|
||||
return reject(bgImg);
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
125
public/index.html
Normal file
125
public/index.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>GSW Control Panel</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://i.imgur.com/2Ok3pxv.png" rel="icon" type="image/png">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/@forevolve/bootstrap-dark@latest/dist/css/toggle-bootstrap-dark.min.css" rel="stylesheet"> -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Marvel&family=Roboto&display=swap" rel="stylesheet">
|
||||
<link href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@latest/dist/css/select2.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/css/jsoneditor.min.css" rel="stylesheet">
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.slim.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/node-forge@1.3.1/dist/forge.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@latest/dist/purify.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mustache@latest/mustache.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@latest/dist/js/select2.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script>
|
||||
|
||||
<link href="main.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav id="top-menu" class="navbar navbar-expand-lg navbar-dark invisible">
|
||||
<span class="navbar-brand mb-0 h1">
|
||||
<img src="https://i.imgur.com/2Ok3pxv.png" width="30" height="30" class="d-inline-block align-middle mr-sm-1 mr-md-2 mr-lg-3" alt="">
|
||||
<span>GSW Control Panel</span>
|
||||
</span>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a id="top-menu-configuration" class="nav-link active" href="#configuration">Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a id="top-menu-flush-data" class="nav-link" href="#flush-data">Flush data</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0">
|
||||
<button id="logout" class="btn btn-outline-danger" type="submit">Logout</button>
|
||||
</form>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
<section id="protected-section" class="row d-none">
|
||||
<div class="col-12">
|
||||
<div id="configuration" class="p-4 m-2 bg-light border border-white rounded-lg shadow-lg">
|
||||
<form method="post" id="config-form"></form>
|
||||
</div>
|
||||
<div id="flush-data" class="p-4 m-2 bg-light border border-white rounded-lg d-none shadow-lg">
|
||||
<div id="flush-data-container">
|
||||
<h3 class="card-title">Flush data</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-lg btn-danger" data-api="flush/servers">Flush
|
||||
game servers data</button>
|
||||
<button type="button" class="btn btn-lg btn-primary" data-api="flush/discord">Flush
|
||||
discord data</button>
|
||||
<button type="button" class="btn btn-lg btn-info" data-api="flush/telegram">Flush
|
||||
telegram data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="unprotected-section">
|
||||
<div class="col-12 text-center sticky-top">
|
||||
<div id="spinner" class="spinner-border m-2 justify-content-center text-primary mx-auto"></div>
|
||||
</div>
|
||||
<div id="bye" class="p-4 m-2 d-none">
|
||||
<h3 class="text-light text-center">
|
||||
Refresh the page to login...
|
||||
</h3>
|
||||
<div class="text-center">
|
||||
<a href=""><kbd class="bg-light text-dark d-inline-block">F5</kbd></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="toast-containers" class="container fixed-bottom mb-1 mb-lg-2 mb-xl-3">
|
||||
<div id="notif-container" class="col-12 mx-auto col-lg-10 col-xl-8">
|
||||
</div>
|
||||
<div id="action-container" class="col-12 mx-auto col-lg-10 col-xl-8">
|
||||
<div id="save-config" class="alert rounded d-none animate__animated animate__fadeInUp animate__faster shadow" role="alert">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex-inline text-light">Careful — you have unsaved changes!</div>
|
||||
<form class="d-flex-inline mr-2 mr-lg-0">
|
||||
<button id="save-config-reset" class="btn btn-sm text-light" type="reset">Reset</button>
|
||||
<button id="save-config-submit" class="btn btn-sm btn-success" type="submit">
|
||||
<span class="_save_btn"> Save </span>
|
||||
<span class="spinner-grow spinner-grow-sm _saving_btn" role="status" aria-hidden="true"></span>
|
||||
<span class="_saving_btn">Saving...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mx-auto text-center py-2 d-none">
|
||||
<a href="https://github.com/a-sync/game-server-watcher"><svg width="16" height="16" viewBox="0 0 16 16" version="1.1" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z">
|
||||
</path>
|
||||
</svg></a>
|
||||
</footer>
|
||||
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
127
public/main.css
Normal file
127
public/main.css
Normal file
@@ -0,0 +1,127 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #000;
|
||||
background-position: top center;
|
||||
transition: background-image 2s ease-in-out 0.5s;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-image: url('https://i.imgur.com/lTly23N.png');
|
||||
}
|
||||
|
||||
body>nav {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.navbar-brand span {
|
||||
color: #fff;
|
||||
font-family: 'Marvel', Helvetica, Arial, sans-serif;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 0 4px #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
zoom: 2;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#protected-section>div>div {
|
||||
background-color: rgba(245, 250, 255, 0.75);
|
||||
}
|
||||
|
||||
#config-form h3.card_title>label {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
#config-form h3>label {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#config-form div[data-schemaid="root"]>div.card-body>div>div {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
#config-form div[data-schemaid="root"]>div.card-body>div>div:last-child {
|
||||
margin-bottom: inherit;
|
||||
}
|
||||
|
||||
#config-form button {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#config-form input[type=checkbox] {
|
||||
vertical-align: middle;
|
||||
zoom: 2;
|
||||
cursor: pointer;
|
||||
margin-right: 0.25rem !important;
|
||||
}
|
||||
|
||||
#config-form .btn-group {
|
||||
display: inline-flex !important;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
#config-form .btn-group button {
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
#config-form .btn-group.je-object__controls {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-add {
|
||||
background-color: darkgreen;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-delete {
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
footer>a>svg {
|
||||
fill: #c6cbd1;
|
||||
}
|
||||
|
||||
#save-config {
|
||||
background-color: rgba(15, 20, 25, 0.9);
|
||||
}
|
||||
|
||||
#save-config ._saving_btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#save-config ._btn ._saving_btn {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
#save-config ._btn ._save_btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#bye>h3 {
|
||||
text-shadow: 0 0 6px #fff;
|
||||
}
|
||||
|
||||
#bye>div>a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#bye kbd {
|
||||
box-shadow: 0 0.125rem 0.25rem rgb(255, 255, 255, 0.75) !important;
|
||||
}
|
||||
566
public/main.js
Normal file
566
public/main.js
Normal file
@@ -0,0 +1,566 @@
|
||||
const id = s => document.getElementById(s);
|
||||
|
||||
let configEditor;
|
||||
$(async () => {
|
||||
const gswFeatures = await fetchApi('GET', 'features');
|
||||
if (gswFeatures) {
|
||||
await setRandomBg().catch(() => { });
|
||||
setInterval(setRandomBg, 60000);
|
||||
|
||||
let unsavedChanges = false;
|
||||
const confirmText = 'You have unsaved changes! Are you sure you want to ';
|
||||
$(window).on('beforeunload', e => {
|
||||
if (unsavedChanges) return confirmText + 'navigate away?';
|
||||
});
|
||||
$('#logout').click(e => {
|
||||
e.preventDefault();
|
||||
if (unsavedChanges && !confirm(confirmText + 'log out?')) return;
|
||||
logout();
|
||||
});
|
||||
|
||||
activateMenu();
|
||||
$('body > nav > ul > li > a').click(e => {
|
||||
e.preventDefault();
|
||||
activateMenu(e.currentTarget);
|
||||
});
|
||||
|
||||
$('#top-menu').removeClass('invisible');
|
||||
$('body > footer').removeClass('d-none');
|
||||
|
||||
// Configuration
|
||||
const gswConfig = await fetchApi('GET', 'config');
|
||||
if (gswConfig && gswConfig.config) {
|
||||
configEditor = new JSONEditor(id('config-form'), {
|
||||
"theme": "bootstrap4",
|
||||
"iconlib": "fontawesome5",
|
||||
"object_layout": "normal",
|
||||
"template": "mustache",
|
||||
"show_errors": "always",
|
||||
"required_by_default": 0,
|
||||
"no_additional_properties": 1,
|
||||
"display_required_only": 0,
|
||||
"remove_empty_properties": 1,
|
||||
"keep_oneof_values": 0,
|
||||
"ajax": 0,
|
||||
"ajaxCredentials": 0,
|
||||
"show_opt_in": 1,
|
||||
"disable_edit_json": 0,
|
||||
"disable_collapse": 1,
|
||||
"disable_properties": 1,
|
||||
"disable_array_add": 0,
|
||||
"disable_array_reorder": 1,
|
||||
"disable_array_delete": 0,
|
||||
"enable_array_copy": 1,
|
||||
"array_controls_top": 0,
|
||||
"disable_array_delete_all_rows": 1,
|
||||
"disable_array_delete_last_row": 1,
|
||||
"prompt_before_delete": 1,
|
||||
"schema": {
|
||||
"title": "Configuration",
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"format": "tabs-top",
|
||||
"items": {
|
||||
"title": "Game Server",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"host",
|
||||
"port"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Name",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Gamedig type",
|
||||
"description": "Look for the <i>GameDig Type ID</i> in the <a href=\"https://github.com/a-sync/node8-gamedig#games-list\">games list</a>.",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"appId": {
|
||||
"title": "Steam App ID",
|
||||
"description": "Look for the <i>AppID</i> in the <a href=\"https://steamdb.info/apps/\">apps list</a>.",
|
||||
"type": "integer",
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"title": "Host name or IP",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"port": {
|
||||
"title": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"updateIntervalMinutes": {
|
||||
"title": "Update interval (minutes) [NOT IMPLEMENTED]",
|
||||
"type": "integer",
|
||||
"default": 5,
|
||||
"minimum": 1,
|
||||
"maximum": 60,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"graphHistoryHours": {
|
||||
"title": "Graph history time span (hours)",
|
||||
"type": "integer",
|
||||
"default": 12,
|
||||
"minimum": 1,
|
||||
"maximum": 24,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"timezoneOffset": {
|
||||
"title": "Time zone offset of the server",
|
||||
"default": 0,
|
||||
"type": "number",
|
||||
"minimum": -12,
|
||||
"maximum": 14,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 6,
|
||||
"grid_break": true
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"type": "array",
|
||||
"title": "Discord channels",
|
||||
"description": "The simplest way to get a discord channel ID is to enable developer mode in settings, then right clicking on the channel name will give you the <b>Copy ID</b> option.",
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Channel",
|
||||
"required": [
|
||||
"channelId"
|
||||
],
|
||||
"properties": {
|
||||
"channelId": {
|
||||
"title": "Channel ID",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "table",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"type": "array",
|
||||
"title": "Telegram chats",
|
||||
"description": "The simplest way to get a telegram chat ID is to invite <a target=\"_blank\" href=\"https://telegram.me/getidsbot\">@getidsbot</a> and use <code>/start</code> to make it post (among other data) the chat ID.",
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Chat",
|
||||
"required": [
|
||||
"chatId"
|
||||
],
|
||||
"properties": {
|
||||
"chatId": {
|
||||
"type": "string",
|
||||
"title": "Chat ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "table",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "grid",
|
||||
"headerTemplate": "{{#self.name}}{{self.name}}{{/self.name}}{{^self.name}}{{self.type}}:{{self.host}}:{{self.port}}{{/self.name}}"
|
||||
}
|
||||
},
|
||||
startval: gswConfig.config
|
||||
});
|
||||
|
||||
configEditor.on('ready', () => {
|
||||
addExportButton();
|
||||
$('#config-form').submit(e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
$('#spinner').addClass('d-none');
|
||||
$('#protected-section').removeClass('d-none');
|
||||
|
||||
let editorCurrVal = configEditor.getValue();
|
||||
let formCurrVal = actualFormValues();
|
||||
$('#config-form input, #config-form textarea').keyup(() => {
|
||||
unsavedChanges = checkForChanges(formCurrVal);
|
||||
});
|
||||
configEditor.on('change', () => {
|
||||
unsavedChanges = checkForChanges(formCurrVal);
|
||||
});
|
||||
|
||||
$('#save-config-submit').click(async e => {
|
||||
e.preventDefault();
|
||||
if (!configEditor.isEnabled()) return;
|
||||
configEditor.disable();
|
||||
$(e.currentTarget).addClass('_btn');
|
||||
|
||||
const uerr = configEditor.validate();
|
||||
if (uerr.length) {
|
||||
notif('warning', 'Validation errors', uerr.map(err => err.path + ' ' + err.message).join('<br>'), undefined, 3 + uerr.length);
|
||||
} else {
|
||||
const res = await fetchApi('POST', 'config', configEditor.getValue());
|
||||
if (res && res.message) {
|
||||
editorCurrVal = configEditor.getValue();
|
||||
formCurrVal = actualFormValues();
|
||||
unsavedChanges = false;
|
||||
dismissSaveConfig();
|
||||
notif('success', '✔️ Changes saved successfully.', undefined, undefined, 3);
|
||||
} else {
|
||||
notif('danger', '⚠️ Error while saving config.', (res ? res.error : undefined), undefined, 3);
|
||||
}
|
||||
}
|
||||
|
||||
$(e.currentTarget).removeClass('_btn');
|
||||
configEditor.enable();
|
||||
});
|
||||
|
||||
$('#save-config-reset').click(e => {
|
||||
e.preventDefault();
|
||||
if (!configEditor.isEnabled()) return;
|
||||
configEditor.setValue(editorCurrVal, true);
|
||||
unsavedChanges = false;
|
||||
dismissSaveConfig();
|
||||
if (formCurrVal !== actualFormValues()) {
|
||||
document.location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
notif('danger', '⚠️ Error while loading config.');
|
||||
}
|
||||
|
||||
// Flush data
|
||||
$('button[data-api]').click(async e => {
|
||||
e.preventDefault();
|
||||
const endpoint = e.currentTarget.dataset.api;
|
||||
|
||||
if (confirm('Are you sure you want to call `' + endpoint + '`?')) {
|
||||
const res = await fetchApi('GET', endpoint);
|
||||
if (res && res.message) {
|
||||
notif('success', res.message, undefined, undefined, 3);
|
||||
} else {
|
||||
notif('danger', (res ? res.error : undefined), undefined, undefined, 3);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function dismissSaveConfig() {
|
||||
$('#save-config').addClass('animate__fadeOutDown');
|
||||
setTimeout(() => {
|
||||
if ($('#save-config').hasClass('animate__fadeOutDown')) {
|
||||
$('#save-config').addClass('d-none');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// NOTE: sadly configEditor.getValue() can not be trusted when the
|
||||
// change event originates from a keypress event
|
||||
function actualFormValues() {
|
||||
const formItems = $('#config-form .tab-pane:not([style*="display: none"]) input:enabled, #config-form .tab-pane:not([style*="display: none"]) textarea:enabled, #config-form .tab-pane:not([style*="display: none"]) select:enabled').toArray();
|
||||
return JSON.stringify(formItems.map(e => {
|
||||
if (e.type === 'checkbox') {
|
||||
return e.checked ? e.value : undefined;
|
||||
}
|
||||
return e.value;
|
||||
}));
|
||||
}
|
||||
|
||||
function checkForChanges(formCurrVal) {
|
||||
if (formCurrVal !== actualFormValues()) {
|
||||
$('#save-config').removeClass('d-none');
|
||||
$('#save-config').removeClass('animate__fadeOutDown');
|
||||
return true;
|
||||
} else {
|
||||
dismissSaveConfig();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function addExportButton() {
|
||||
const button_holder = configEditor.root.theme.getHeaderButtonHolder();
|
||||
const exportBtn = configEditor.root.getButton('Export', 'save', 'Export As JSON File');
|
||||
button_holder.appendChild(exportBtn);
|
||||
configEditor.root.header.parentNode.insertBefore(button_holder, configEditor.root.header.nextSibling);
|
||||
|
||||
exportBtn.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
const dt = (new Date()).toISOString().slice(0, 19).replace(/\D/g, '');
|
||||
const filename = dt + '-gsw.config.json';
|
||||
const blob = new Blob([JSON.stringify(configEditor.getValue(), null, 2)], {
|
||||
type: 'application/json;charset=utf-8'
|
||||
});
|
||||
|
||||
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
|
||||
window.navigator.msSaveOrOpenBlob(blob, filename);
|
||||
} else {
|
||||
const a = document.createElement('a');
|
||||
a.download = filename;
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.dataset.downloadurl = ['text/plain', a.download, a.href].join(':');
|
||||
|
||||
a.dispatchEvent(new MouseEvent('click', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': false
|
||||
}));
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
$(window).off('beforeunload');
|
||||
$('#save-config').addClass('d-none');
|
||||
$('#top-menu').addClass('invisible');
|
||||
$('#protected-section').addClass('d-none');
|
||||
$('#spinner').addClass('d-none');
|
||||
$('#bye').removeClass('d-none');
|
||||
$('body > footer').addClass('d-none');
|
||||
if (configEditor) configEditor.destroy();
|
||||
window.localStorage.removeItem('btoken');
|
||||
}
|
||||
|
||||
function notif(type, html1, html2, html3, dismissable) {
|
||||
let closeTimeout = 0;
|
||||
if (typeof dismissable === 'number') {
|
||||
closeTimeout = dismissable;
|
||||
dismissable = true;
|
||||
}
|
||||
|
||||
const el = $('<div/>', { class: 'alert alert-dismissible fade show shadow alert-' + type, role: 'alert' });
|
||||
el.attr('role', 'alert');
|
||||
if (html2) {
|
||||
const h4 = $('<h4/>', { class: 'alert-heading', html: html1 });
|
||||
el.append(h4);
|
||||
|
||||
const p1 = $('<p/>', { html: html2 });
|
||||
el.append(p1);
|
||||
if (html3) {
|
||||
el.append($('<hr/>'));
|
||||
const p2 = $('<p/>', { class: 'mb-0', html: html3 });
|
||||
el.append(p2);
|
||||
}
|
||||
} else {
|
||||
el.html(html1);
|
||||
}
|
||||
|
||||
if (dismissable !== false) {
|
||||
const button = $('<button/>', { class: 'close', type: 'button', 'data-dismiss': 'alert', 'aria-label': 'Close' });
|
||||
|
||||
const span = $('<span/>', { 'aria-hidden': 'true', html: '×' });
|
||||
button.append(span);
|
||||
el.append(button);
|
||||
}
|
||||
|
||||
el.alert();
|
||||
|
||||
if (closeTimeout > 0) {
|
||||
const a = $(el);
|
||||
const timer = setTimeout(a => {
|
||||
a.alert('close');
|
||||
}, 1000 * closeTimeout, a);
|
||||
a.on('close.bs.alert', function () {
|
||||
clearTimeout(timer);
|
||||
})
|
||||
}
|
||||
|
||||
$('#notif-container').append(el);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function activateMenu(target) {
|
||||
if (!target) {
|
||||
target = id('top-menu-configuration');
|
||||
if (document.location.hash.length > 1) {
|
||||
const hashTargetMenu = id('top-menu-' + document.location.hash.slice(1));
|
||||
if (hashTargetMenu) {
|
||||
target = hashTargetMenu;
|
||||
}
|
||||
} else if (window.localStorage.getItem('active.menu')) {
|
||||
const stateTargetMenu = id('top-menu-' + window.localStorage.getItem('active.menu').slice(1));
|
||||
if (stateTargetMenu) {
|
||||
target = stateTargetMenu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const menu = target.getAttribute('href');
|
||||
const menuItems = document.querySelectorAll('#top-menu > ul > li > a');
|
||||
for (const a of menuItems) {
|
||||
const at = a.getAttribute('href');
|
||||
if (at === menu) {
|
||||
$(at).removeClass('d-none');
|
||||
$(a).addClass('active');
|
||||
|
||||
window.history.replaceState(null, null, document.location.pathname + menu);
|
||||
window.localStorage.setItem('active.menu', menu);
|
||||
|
||||
const titleArr = document.title.split(' #');
|
||||
if (titleArr.length < 2) {
|
||||
titleArr.push(menu.slice(1));
|
||||
} else {
|
||||
titleArr[titleArr.length - 1] = menu.slice(1);
|
||||
}
|
||||
|
||||
document.title = titleArr.join(' #');
|
||||
} else {
|
||||
$(at).addClass('d-none');
|
||||
$(a).removeClass('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchApi(method, endpoint, body) {
|
||||
let re = null;
|
||||
let errMsg = '';
|
||||
const retries = 2;
|
||||
|
||||
try {
|
||||
for (let i = 1; i <= retries; i++) {
|
||||
errMsg = '';
|
||||
const btoken = await getBearerToken();
|
||||
if (btoken) {
|
||||
const res = await fetch('/' + endpoint, {
|
||||
method,
|
||||
body: (body ? JSON.stringify(body) : undefined),
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'x-btoken': btoken,
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
re = false;
|
||||
window.localStorage.removeItem('btoken');
|
||||
errMsg = 'Invalid token.';
|
||||
} else if (res.status === 200) {
|
||||
re = await res.json();
|
||||
break;
|
||||
} else errMsg = 'Unexpected API status.';
|
||||
} else {
|
||||
re = false;
|
||||
errMsg = 'Authentication error.';
|
||||
}
|
||||
if (errMsg !== '') console.error(errMsg + ' method, endpoint', method, endpoint);
|
||||
}
|
||||
} catch (err) {
|
||||
re = null;
|
||||
errMsg = 'Unexpected error.';
|
||||
console.error('fetchApi: method, endpoint, err, re', method, endpoint, err, re);
|
||||
}
|
||||
|
||||
if (!re) {
|
||||
logout();
|
||||
notif('danger', '⚠️ ' + errMsg, 3);
|
||||
}
|
||||
|
||||
return re;
|
||||
}
|
||||
|
||||
async function getBearerToken() {
|
||||
const bt = window.localStorage.getItem('btoken');
|
||||
if (bt) {
|
||||
const salt = bt.slice(0, bt.length - 141);
|
||||
const valid = bt.slice(-141, -128);
|
||||
const hash = bt.slice(-128);
|
||||
if (salt.length > 24
|
||||
&& /^\d{13}$/.test(valid)
|
||||
&& /^[0-9a-f]{128}$/.test(hash)
|
||||
&& Date.now() < Number(valid)
|
||||
&& Number(valid) - Date.now() < 3600000 * 12) {
|
||||
return bt;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use modalDialog
|
||||
// if (typeof window.showModalDialog === 'function') {} else
|
||||
if (typeof HTMLDialogElement === 'function') {
|
||||
const ans = await prompt('Admin secret');
|
||||
|
||||
if (ans) window.localStorage.setItem('btoken', await generateBearerToken(ans));
|
||||
else window.localStorage.removeItem('btoken');
|
||||
} else {
|
||||
notif('primary', '⚠️ Close the tab to re-enable dialog boxes.');
|
||||
}
|
||||
|
||||
return window.localStorage.getItem('btoken');
|
||||
}
|
||||
|
||||
async function generateBearerToken(secret) {
|
||||
const valid = String(Date.now() + 3600000 * 12);
|
||||
const salt = Array.from({ length: 3 }).map(() => Math.random().toString(36).slice(2)).join('');
|
||||
|
||||
let hash;
|
||||
if (crypto.subtle) {
|
||||
const buff = await crypto.subtle.digest('SHA-512', new TextEncoder('utf-8').encode(salt + valid + secret));
|
||||
hash = Array.from(new Uint8Array(buff)).map(x => ('00' + x.toString(16)).slice(-2)).join('');
|
||||
} else {
|
||||
const md = forge.md.sha512.create();
|
||||
md.update(salt + valid + secret);
|
||||
hash = md.digest().toHex();
|
||||
}
|
||||
|
||||
return salt + valid + hash;
|
||||
}
|
||||
|
||||
function setRandomBg() {
|
||||
const bgs = [
|
||||
'https://i.imgur.com/bDzhwG5.png',
|
||||
'https://i.imgur.com/rA8JXuI.png',
|
||||
'https://i.imgur.com/pstAPIw.png',
|
||||
'https://i.imgur.com/gQD3xfo.png',
|
||||
'https://i.imgur.com/iKTfM8z.png'
|
||||
];
|
||||
|
||||
const bgImg = new Image();
|
||||
bgImg.src = bgs[Math.floor(Math.random() * bgs.length)];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
bgImg.onload = () => {
|
||||
document.body.style.backgroundAttachment = 'scroll';
|
||||
document.body.style.backgroundRepeat = 'repeat';
|
||||
document.body.style.backgroundSize = 'auto';
|
||||
document.body.style.backgroundImage = 'url(' + bgImg.src + ')';
|
||||
return resolve(bgImg);
|
||||
};
|
||||
|
||||
bgImg.onerror = () => {
|
||||
return reject(bgImg);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -32,20 +32,24 @@ interface ApiResponse {
|
||||
|
||||
createServer(async (req, res) => {
|
||||
if (DBG) console.log('DBG: %j %j', (new Date()), req.url);
|
||||
|
||||
const reqUrl = new URL(req.url || '', 'http://localhost');
|
||||
const reqPath = reqUrl.pathname.split('/');
|
||||
if (reqUrl.pathname === '/') {
|
||||
const p = reqUrl.pathname === '/' ? 'index.html' : reqUrl.pathname.slice(1);
|
||||
|
||||
if (['index.html', 'main.css', 'main.js'].includes(p)) {
|
||||
if (SECRET !== '') {
|
||||
let ct = 'text/html';
|
||||
if (p === 'main.css') ct = 'text/css';
|
||||
else if (p === 'main.js') ct = 'text/javascript';
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html',
|
||||
'Content-Type': ct,
|
||||
'Cache-Control': 'max-age=' + String(CACHE_MAX_AGE)
|
||||
});
|
||||
fs.createReadStream('./index.html').pipe(res);
|
||||
fs.createReadStream('./public/' + p).pipe(res);
|
||||
} else {
|
||||
res.end('Configure the `SECRET` env var to enable the web UI!');
|
||||
}
|
||||
} else if (reqUrl.pathname === '/ping') {
|
||||
} else if (p === 'ping') {
|
||||
if (DBG) console.log('ping');
|
||||
res.end('pong');
|
||||
} else if (SECRET !== '' && req.headers['x-btoken']) {
|
||||
@@ -53,14 +57,15 @@ createServer(async (req, res) => {
|
||||
let re: ApiResponse = {};
|
||||
|
||||
if (validateBearerToken(String(req.headers['x-btoken']))) {
|
||||
const reqPath = p.split('/');
|
||||
try {
|
||||
if (reqPath[1] === 'features') {
|
||||
if (reqPath[0] === 'features') {
|
||||
re.features = {
|
||||
steam: FEET_STEAM,
|
||||
discord: FEET_DISCORD,
|
||||
telegram: FEET_TELEGRAM
|
||||
};
|
||||
} else if (reqPath[1] === 'config') {
|
||||
} else if (reqPath[0] === 'config') {
|
||||
if (req.method === 'GET') {
|
||||
re.config = await readConfig();
|
||||
} else if (req.method === 'POST') {
|
||||
@@ -83,9 +88,9 @@ createServer(async (req, res) => {
|
||||
status = 400;
|
||||
re.error = 'Invalid Request';
|
||||
}
|
||||
} else if (reqPath[1] === 'flush' && ['servers', 'discord', 'telegram'].includes(reqPath[2])) {
|
||||
await restart(reqPath[2]);
|
||||
re.message = '🗑️ ' + reqPath[2].slice(0, 1).toUpperCase() + reqPath[2].slice(1) + ' data flushed.';
|
||||
} else if (reqPath[0] === 'flush' && ['servers', 'discord', 'telegram'].includes(reqPath[1])) {
|
||||
await restart(reqPath[1]);
|
||||
re.message = '🗑️ ' + reqPath[1].slice(0, 1).toUpperCase() + reqPath[1].slice(1) + ' data flushed.';
|
||||
} else {
|
||||
status = 400;
|
||||
re.error = 'Invalid Request';
|
||||
@@ -109,9 +114,9 @@ createServer(async (req, res) => {
|
||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||
res.end('<html><head></head><body>404 💢</body></html>');
|
||||
}
|
||||
}).listen(APP_PORT);
|
||||
|
||||
console.log('Web service started %s:%s', APP_HOST, APP_PORT);
|
||||
}).listen(APP_PORT, APP_HOST, () => {
|
||||
console.log('Web service started %s:%s', APP_HOST, APP_PORT);
|
||||
});
|
||||
|
||||
main().then(l => {
|
||||
loop = l;
|
||||
@@ -143,7 +148,8 @@ function validateBearerToken(btoken: string) {
|
||||
if (salt.length > 24
|
||||
&& /^\d{13}$/.test(valid)
|
||||
&& /^[a-f0-9]{128}$/.test(hash)
|
||||
&& Date.now() < Number(valid)) {
|
||||
&& Date.now() < Number(valid)
|
||||
&& Number(valid) - Date.now() < 3600000 * 12) {
|
||||
return hash === crypto.createHash('sha512').update(salt + valid + SECRET).digest('hex');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user