// ── Admin · Rust — the handlers ─────────────────────────────────────────── // // The write side of the module. Three things every handler here owes: // // 1. **Never return the token.** Not in a response, not in an error, not in an // activity-log detail. It is accepted, encrypted and forgotten. // 2. **Record the change.** `core.activity.log` writes core's own admin audit // row. These handlers edit the credential that reaches a game host; "who // changed this" has no second place it is recorded. // 3. **Answer rather than throw.** An unhandled rejection reaches core's error // handler and gets core blamed for a fault in this module. const core = require('../../core') const db = require('../../model/servers/servers.db') const servers = require('../../model/servers/servers.model') const sidecar = require('../../sidecarClient') const log = core.logger('admin') async function listServers(req, res) { try { res.json({ servers: await servers.listForAdmin() }) } catch (err) { log.error('failed to read the server list', { error: err.message }) res.status(500).json({ error: 'Failed to read the server list' }) } } async function putServer(req, res) { const { id } = req.params const { name, sidecarBaseUrl, sidecarToken, protocol, enabled, sortOrder } = req.body try { const existing = await db.getServer(id) // A NEW server with no token is a row that can never reach its sidecar, and // the operator will read the resulting "unreachable" as a network problem. // Refusing it up front costs one round trip and saves that hunt. An EXISTING // row is a different case: omitting the token is how you say "leave it". if (!existing && !sidecarToken) { return res.status(400).json({ error: 'A new server needs its sidecar token' }) } await db.upsertServer({ id, name, sidecarBaseUrl, // `encryptToken` returns null for an empty value, and `upsertServer` reads // null as "do not write this column". The two halves of that rule are in // different files on purpose: the model decides what a blank means, the SQL // decides what null does, and neither has to know the other's reason. sidecarTokenEnc: servers.encryptToken(sidecarToken), protocol: protocol === undefined ? sidecar.PROTOCOL_VERSION : protocol, enabled: enabled === undefined ? true : enabled, sortOrder: sortOrder === undefined ? 0 : sortOrder, }) await core.activity.log({ req, action: 'rust.server.save', detail: { server: id, created: !existing, sidecarBaseUrl, // Whether the credential was rotated, never the credential. tokenChanged: Boolean(sidecarToken), }, }) return res.status(204).end() } catch (err) { log.error('failed to save a server', { server: id, error: err.message }) return res.status(500).json({ error: 'Failed to save the server' }) } } async function deleteServer(req, res) { const { id } = req.params try { const existing = await db.getServer(id) if (!existing) return res.status(404).json({ error: 'No such server' }) await db.deleteServer(id) await core.activity.log({ req, action: 'rust.server.delete', detail: { server: id } }) return res.status(204).end() } catch (err) { log.error('failed to delete a server', { server: id, error: err.message }) return res.status(500).json({ error: 'Failed to delete the server' }) } } /** * Probe one sidecar and report what came back. * * This is the route that tells a wrong URL from a wrong token from a mismatched * protocol, and that distinction is the whole reason it exists: all three present * to an operator as "the site says my server is offline", and each has a * different fix. The status string from `sidecarClient` is carried through * verbatim so the panel can say which. */ async function testServer(req, res) { const { id } = req.params try { const row = await db.getServer(id) if (!row) return res.status(404).json({ error: 'No such server' }) const result = await sidecar.health(servers.withToken(row)) await core.activity.log({ req, action: 'rust.server.test', detail: { server: id, ok: result.ok, status: result.status }, }) return res.json({ ok: result.ok, status: result.status, // `data` is the sidecar's own health document on success and the mismatch // detail on a 409. Both are safe to show: neither carries a credential. sidecar: result.data || null, }) } catch (err) { log.error('failed to probe a sidecar', { server: id, error: err.message }) return res.status(500).json({ error: 'Failed to probe the sidecar' }) } } module.exports = { listServers, putServer, deleteServer, testServer }