const express = require("express"); const crypto = require("crypto"); const session = require("express-session"); const BetterSqlite3Store = require("better-sqlite3-session-store")(session); const { db } = require("./src/services/db"); const { getSetting, setSetting } = require("./src/services/settings"); const { buildDiscordAuthUrl, exchangeDiscordCode, fetchDiscordUser, fetchDiscordGuildMember } = require("./src/services/auth"); const { getRoleFlags, hasAccess } = require("./src/services/rbac"); const { listSnapshots, restoreSnapshot } = require("./src/services/update-manager"); const { requestRestart } = require("./src/services/updater"); const { safeModeStatus, clearRecoveryMarker } = require("./src/services/recovery-mode"); const { setPluginEnabled } = require("./src/services/plugins"); function ensureSessionSecret() { let secret = getSetting("session_secret"); if (!secret) { secret = crypto.randomBytes(32).toString("hex"); setSetting("session_secret", secret); } return secret; } function isConfigured() { return Boolean( getSetting("discord_client_id") && getSetting("discord_client_secret") && getSetting("discord_guild_id") ); } function renderPage(title, content) { return ` ${title}
Safe Mode
${content}
`; } function buildSnapshotTable(snapshots) { if (!snapshots.length) { return "

No snapshots available.

"; } const rows = snapshots .map((snap) => { const label = snap.type === "plugin" ? `Plugin: ${snap.pluginId}` : "Bot core"; const when = new Date(snap.createdAt).toLocaleString(); return ` ${label} ${when}
`; }) .join(""); return ` ${rows}
Snapshot Created Action
`; } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function buildRecoveryPanel() { const status = safeModeStatus(); const marker = status.marker; if (!marker) { return `

Recovery state

No recovery marker is present.

`; } const pluginActions = marker.target_kind === "plugin" && marker.target_id ? `
` : ""; return `

Last update marker

Status: ${escapeHtml(marker.status)}

Target${escapeHtml(marker.target_kind || "unknown")} ${escapeHtml(marker.target_id || "")}
Version${escapeHtml(marker.from_version || "?")} -> ${escapeHtml(marker.to_version || "?")}
Method${escapeHtml(marker.update_method || "?")}
Source branch${escapeHtml(marker.source_branch || "?")}
Snapshot${escapeHtml(marker.snapshot_id || marker.recovery_marker_id || "pending")}
Updated${escapeHtml(marker.updated_at || marker.created_at || "?")}
Error${escapeHtml(marker.error || "")}
${pluginActions}
`; } const app = express(); const sessionStore = new BetterSqlite3Store({ client: db }); app.use( session({ secret: ensureSessionSecret(), resave: false, saveUninitialized: false, store: sessionStore }) ); app.use(express.urlencoded({ extended: false })); app.get("/", (req, res) => { if (!isConfigured()) { return res.send( renderPage( "Safe Mode", `

Discord not configured

Discord settings are required to enter safe mode.

` ) ); } if (!req.session.user) { return res.send( renderPage( "Safe Mode", `

Login required

Authenticate with Discord to access rollback tools.

Login with Discord
` ) ); } if (!hasAccess(req.session.user, "admin")) { return res.send( renderPage( "Safe Mode", `

Access denied

You do not have administrator access.

` ) ); } const snapshots = listSnapshots(); const table = buildSnapshotTable(snapshots); res.send( renderPage( "Safe Mode", `${buildRecoveryPanel()}

Rollback snapshots

Use these snapshots to roll back failed updates. Major-version rollback is blocked unless the snapshot is marked rollback safe. The server will restart after rollback.

${table}
` ) ); }); app.get("/auth/discord", (req, res) => { if (!isConfigured()) { return res.redirect("/"); } const state = crypto.randomBytes(16).toString("hex"); req.session.discordState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = `${baseUrl}/auth/discord/callback`; const url = buildDiscordAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/discord/callback", async (req, res) => { const { code, state } = req.query; if (!code || state !== req.session.discordState) { return res.send(renderPage("Safe Mode", "
Invalid login state.
")); } try { const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = `${baseUrl}/auth/discord/callback`; const token = await exchangeDiscordCode(code, redirectUri); const user = await fetchDiscordUser(token.access_token); const guildId = getSetting("discord_guild_id"); const member = guildId ? await fetchDiscordGuildMember(token.access_token, guildId) : null; const roles = member?.roles || []; const flags = getRoleFlags(roles); req.session.user = { id: user.id, username: user.global_name || user.username, roles, ...flags }; res.redirect("/"); } catch (error) { console.error(error); res.send(renderPage("Safe Mode", "
Login failed.
")); } }); app.post("/rollback/:id", (req, res) => { if (!req.session.user || !hasAccess(req.session.user, "admin")) { return res.status(403).send(renderPage("Safe Mode", "
Access denied.
")); } try { restoreSnapshot(req.params.id); res.send( renderPage( "Safe Mode", "

Rollback complete

Restarting the bot now...

" ) ); requestRestart(); } catch (error) { res.send( renderPage( "Safe Mode", `

Rollback failed

${error.message}

` ) ); } }); app.post("/plugins/:id/disable", (req, res) => { if (!req.session.user || !hasAccess(req.session.user, "admin")) { return res.status(403).send(renderPage("Safe Mode", "
Access denied.
")); } setPluginEnabled(req.params.id, false); res.redirect("/"); }); app.post("/recovery/clear-marker", (req, res) => { if (!req.session.user || !hasAccess(req.session.user, "admin")) { return res.status(403).send(renderPage("Safe Mode", "
Access denied.
")); } clearRecoveryMarker(); res.redirect("/"); }); app.post("/recovery/retry-normal-startup", (req, res) => { if (!req.session.user || !hasAccess(req.session.user, "admin")) { return res.status(403).send(renderPage("Safe Mode", "
Access denied.
")); } res.send(renderPage("Safe Mode", "

Retrying normal startup

Safe mode is exiting. The wrapper will restart Lumi normally.

")); requestRestart(); }); const port = Number(process.env.SAFE_MODE_PORT || 3001); app.listen(port, () => { console.log(`Safe mode listening on http://localhost:${port}`); });