forked from wmantly/mc-bot-town
fable
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { CJbot } = require('../model/minecraft');
|
||||
const Database = require('./storage/database');
|
||||
|
||||
class WebServer {
|
||||
constructor() {
|
||||
@@ -86,10 +87,9 @@ class WebServer {
|
||||
}
|
||||
|
||||
async start() {
|
||||
const conf = require('../conf');
|
||||
|
||||
this.port = conf.storage?.webPort || 3000;
|
||||
this.host = conf.storage?.webHost || '0.0.0.0';
|
||||
const settings = require('./settings/manager');
|
||||
this.port = settings.get('storage.webPort') || 3000;
|
||||
this.host = settings.get('storage.webHost') || '0.0.0.0';
|
||||
|
||||
this.app = express();
|
||||
|
||||
@@ -102,6 +102,12 @@ class WebServer {
|
||||
next();
|
||||
});
|
||||
|
||||
// Auth (OIDC/SSO) — the login routes and the session gate must be
|
||||
// mounted before any plugin routers so every route is protected
|
||||
const auth = require('./auth');
|
||||
this.app.use('/auth', auth.createRouter());
|
||||
this.app.use(auth.middleware);
|
||||
|
||||
// Flush any plugins that were queued before start()
|
||||
for (const cls of this._pendingPlugins) {
|
||||
this.registerPlugin(cls);
|
||||
@@ -110,6 +116,13 @@ class WebServer {
|
||||
|
||||
this.setupRoutes();
|
||||
|
||||
// JSON error handler — keeps stack traces out of responses
|
||||
this.app.use((err, req, res, next) => {
|
||||
console.error(`WebServer: error on ${req.method} ${req.path}:`, err.message);
|
||||
if (res.headersSent) return next(err);
|
||||
res.status(err.status || 500).json({ error: err.message || 'Internal server error' });
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = this.app.listen(this.port, this.host, () => {
|
||||
console.log(`WebServer: Running at http://${this.host}:${this.port}`);
|
||||
@@ -209,7 +222,37 @@ class WebServer {
|
||||
}
|
||||
});
|
||||
|
||||
this.app.post('/api/bots/:name/plugins/:plugin/load', async (req, res) => {
|
||||
this.app.put('/api/bots/:name/settings/:key', async (req, res) => {
|
||||
try {
|
||||
const bot = CJbot.bots[req.params.name];
|
||||
if (!bot) return res.status(404).json({ error: 'Bot not found' });
|
||||
|
||||
const key = req.params.key;
|
||||
const value = req.body?.value;
|
||||
|
||||
let type = 'string';
|
||||
if (typeof value === 'boolean') type = 'boolean';
|
||||
else if (typeof value === 'number') type = 'number';
|
||||
else if (typeof value === 'object') type = 'json';
|
||||
|
||||
await Database.setBotSetting(req.params.name, key, value, type);
|
||||
|
||||
switch (key) {
|
||||
case 'autoConnect': bot.autoConnect = value; break;
|
||||
case 'autoReConnect': bot.autoReConnect = value; break;
|
||||
case 'onDemand': bot.onDemand = value; break;
|
||||
case 'idleTimeout': bot._idleTimeout = Number(value) || 30000; break;
|
||||
case 'plugins': bot.pluginsWanted = value || {}; break;
|
||||
}
|
||||
|
||||
res.json({ key, value, type });
|
||||
} catch (error) {
|
||||
console.error('API Error /api/bots/:name/settings/:key:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.post('/api/bots/:name/plugins/:plugin/load', async (req, res) => {
|
||||
try {
|
||||
const bot = CJbot.bots[req.params.name];
|
||||
if (!bot) return res.status(404).json({ error: 'Bot not found' });
|
||||
@@ -351,6 +394,9 @@ body{font-family:'Segoe UI',Tahoma,sans-serif;background:#111827;color:#e5e7eb;m
|
||||
.plugin-tag .unload-btn:hover{color:#f87171}
|
||||
.bot-actions{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
||||
.bot-actions select{padding:6px;border:1px solid #374151;border-radius:4px;background:#1f2937;color:#e5e7eb;font-size:.8em}
|
||||
.bot-settings{margin-bottom:12px}
|
||||
.bot-setting-toggle{display:flex;align-items:center;gap:6px;font-size:.8em;color:#9ca3af;cursor:pointer}
|
||||
.bot-setting-toggle input[type="checkbox"]{accent-color:#60a5fa;cursor:pointer}
|
||||
.btn-connect{background:#059669;color:#fff;border:none;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:.8em}
|
||||
.btn-connect:hover{background:#047857}
|
||||
.btn-disconnect{background:#dc2626;color:#fff;border:none;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:.8em}
|
||||
@@ -394,6 +440,8 @@ ${pluginCSS}
|
||||
<div class="players-list" id="playersList"></div>
|
||||
</div>
|
||||
<button onclick="loadAll()">Refresh</button>
|
||||
<span id="userBadge" style="display:none;font-size:.85em;color:#9ca3af"></span>
|
||||
<button id="logoutBtn" style="display:none;background:#374151" onclick="location.href='/auth/logout'">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout">
|
||||
@@ -470,6 +518,32 @@ document.addEventListener('click', function(e) {
|
||||
pollOnlinePlayers();
|
||||
setInterval(pollOnlinePlayers, 15000);
|
||||
|
||||
// === Auth badge ===
|
||||
(async function() {
|
||||
try {
|
||||
const r = await fetch('/auth/me');
|
||||
const d = await r.json();
|
||||
if (d.enabled && d.username) {
|
||||
const badge = document.getElementById('userBadge');
|
||||
badge.textContent = d.username;
|
||||
badge.style.display = '';
|
||||
document.getElementById('logoutBtn').style.display = '';
|
||||
}
|
||||
} catch(e) {}
|
||||
})();
|
||||
|
||||
// Session-expiry guard: if any API call starts returning 401, go to login
|
||||
(function() {
|
||||
const origFetch = window.fetch;
|
||||
window.fetch = async function(...args) {
|
||||
const res = await origFetch.apply(this, args);
|
||||
if (res.status === 401 && !String(args[0]).startsWith('/auth/')) {
|
||||
location.href = '/auth/login?redirect=' + encodeURIComponent(location.pathname);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
})();
|
||||
|
||||
const ALL_TABS = [${allTabIds.join(',')}];
|
||||
const TAB_ACTIVE_HANDLERS = {${onTabActiveMap}};
|
||||
let currentTab = ALL_TABS[0] || 'bots';
|
||||
@@ -668,6 +742,12 @@ function renderBots(bots) {
|
||||
'<span style="font-size:.8em;color:#9ca3af">' + statusText + '</span>' +
|
||||
'</div>' +
|
||||
infoHtml +
|
||||
'<div class="bot-settings">' +
|
||||
'<label class="bot-setting-toggle">' +
|
||||
'<input type="checkbox" ' + (b.autoConnect ? 'checked' : '') + ' onchange="setBotSetting(\\'' + escHtml(name) + '\\',\\'autoConnect\\',this.checked)" />' +
|
||||
' Auto-connect' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
pluginHtml +
|
||||
'<div class="bot-actions">' + connBtn + ' ' + loadSelect + '</div>' +
|
||||
cmdHtml +
|
||||
@@ -695,6 +775,19 @@ async function disconnectBot(name) {
|
||||
} catch(e) { showToast('Network error', 'error'); }
|
||||
}
|
||||
|
||||
async function setBotSetting(botName, key, value) {
|
||||
try {
|
||||
const r = await fetch('/api/bots/' + botName + '/settings/' + key, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ value: value })
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!r.ok) showToast(d.error || 'Failed', 'error');
|
||||
else showToast(botName + ': ' + key + ' = ' + value, 'info');
|
||||
} catch(e) { showToast('Network error', 'error'); }
|
||||
}
|
||||
|
||||
async function loadPlugin(botName) {
|
||||
const sel = document.getElementById('plugin-select-' + botName);
|
||||
if (!sel) return;
|
||||
|
||||
Reference in New Issue
Block a user