diff --git a/dist/server.js b/dist/server.js
index d95736e..29bf067 100644
--- a/dist/server.js
+++ b/dist/server.js
@@ -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('
404 💢');
}
-}).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;
diff --git a/index.html b/index.html
deleted file mode 100644
index b75565a..0000000
--- a/index.html
+++ /dev/null
@@ -1,816 +0,0 @@
-
-
-
-
- GSW Control Panel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Flush data
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Refresh the page to login...
-
-
-
-
-
-
-
-
-
-
-
-
-
Careful — you have unsaved changes!
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..2267cf3
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,125 @@
+
+
+
+
+ GSW Control Panel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Flush data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Refresh the page to login...
+
+
+
+
+
+
+
+
+
+
+
+
+
Careful — you have unsaved changes!
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/main.css b/public/main.css
new file mode 100644
index 0000000..0b6e216
--- /dev/null
+++ b/public/main.css
@@ -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;
+}
\ No newline at end of file
diff --git a/public/main.js b/public/main.js
new file mode 100644
index 0000000..58b931d
--- /dev/null
+++ b/public/main.js
@@ -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 GameDig Type ID in the games list.",
+ "type": "string",
+ "options": {
+ "grid_columns": 6
+ }
+ },
+ "appId": {
+ "title": "Steam App ID",
+ "description": "Look for the AppID in the apps list.",
+ "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 Copy ID 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 @getidsbot and use /start 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('
'), 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 = $('', { class: 'alert alert-dismissible fade show shadow alert-' + type, role: 'alert' });
+ el.attr('role', 'alert');
+ if (html2) {
+ const h4 = $('', { class: 'alert-heading', html: html1 });
+ el.append(h4);
+
+ const p1 = $('', { html: html2 });
+ el.append(p1);
+ if (html3) {
+ el.append($('
'));
+ const p2 = $('', { class: 'mb-0', html: html3 });
+ el.append(p2);
+ }
+ } else {
+ el.html(html1);
+ }
+
+ if (dismissable !== false) {
+ const button = $('', { class: 'close', type: 'button', 'data-dismiss': 'alert', 'aria-label': 'Close' });
+
+ const 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);
+ };
+ });
+}
diff --git a/src/server.ts b/src/server.ts
index 9a70d97..2e79551 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -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('404 💢');
}
-}).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');
}