// ── Player · Rust — the handlers ────────────────────────────────────────── // // Two things live here now: the server list as a signed-in caller sees it (phase // 1's honest placeholder, which must not reshape the list — it calls the same // model the public tier does so the two cannot drift), and R1's identity link. // // ── Every refusal is a sentence, and they are not interchangeable ───────── // // The link handler's whole job is turning a discriminated result into the right // thing to tell a player, and the four wrong answers are wrong in different ways: // // • "that code is unknown or expired" → run `/link` again // • "another account holds that Steam id" → run `/unlink` in game, or ask staff // • "we could not reach a server" → try again in a minute; the code is fine // • "no servers are configured" → nothing the player can do at all // // A player told to run `/link` again when the server their code came from was // merely unreachable will run it again, get another code from the same // unreachable server, and be told the same thing. That is the failure the // `unsure` branch exists to prevent. const core = require('../../core') const links = require('../../model/links/links.model') const permissions = require('../../model/permissions/permissions.model') const servers = require('../../model/servers/servers.model') const log = core.logger('player') async function listServers(req, res) { try { res.json({ servers: await servers.listPublic() }) } catch (err) { log.error('failed to read the server list', { error: err.message }) res.status(500).json({ message: 'Failed to read the server list' }) } } /** GET /player/rust/links — the Steam accounts the caller holds. */ async function listLinks(req, res) { try { res.json({ links: await links.listForUser(req.user.id) }) } catch (err) { log.error('failed to read a player’s links', { error: err.message }) res.status(500).json({ message: 'Failed to read your linked accounts' }) } } /** * POST /player/rust/link — redeem a code from `/link` in game. * * The fleet loop is the model's (D24); this maps its answer onto a status and a * sentence. **A refused code is a 400 and an unreachable server is a 503**, * because a client that cannot tell them apart cannot tell a player whether to * try again or to go and get a new code. */ async function confirmLink(req, res) { const code = String(req.body.code || '').trim() try { const result = await links.redeem({ code, userId: req.user.id }) if (result.ok) { // Logged on the player tier too, not only for admin writes: this is the // moment a website account starts being able to hold permissions and // entitlements in a game, and "when did this account become that Steam id" // is a question an operator will eventually need answered. await core.activity.log({ req, action: 'rust.account.link', detail: { steamId: result.link.steamId, serverId: result.link.serverId }, }) return res.json({ linked: true, link: result.link, already: Boolean(result.already) }) } switch (result.reason) { case 'taken': // Naming the holder is deliberate and it is not a leak: the player is // signed in, the account named is one they may well own, and without the // name the advice ("sign in as that account, or ask staff") is unusable. return res.status(409).json({ message: result.username ? `That Steam account is already linked to ${result.username}. Run /unlink in game to release it.` : 'That Steam account is already linked to another website account. Run /unlink in game to release it.', }) case 'unsure': return res.status(503).json({ message: 'One of the servers could not be reached, so that code could not be checked. ' + 'Your code is still good — try again in a minute.', }) case 'offline': return res.status(503).json({ message: 'The game servers are unreachable right now — try again in a minute.', }) case 'no-servers': return res.status(503).json({ message: 'No Rust servers are configured on this site yet.' }) default: return res.status(400).json({ message: 'That code is unknown or has expired. Type /link in game for a new one.', }) } } catch (err) { log.error('failed to confirm a link code', { error: err.message }) return res.status(500).json({ message: 'Failed to confirm that code' }) } } /** * DELETE /player/rust/links/:steamId — release a link the caller holds. * * Scoped to the caller inside the statement, so "not linked" and "not yours" * answer the same 404 — a signed-in stranger must not be able to discover which * Steam ids are linked by deleting them one at a time. */ async function removeLink(req, res) { const { steamId } = req.params try { const removed = await links.unlinkOwned(steamId, req.user.id) if (!removed) return res.status(404).json({ message: 'That account is not linked to you' }) await core.activity.log({ req, action: 'rust.account.unlink', detail: { steamId } }) return res.json({ unlinked: true }) } catch (err) { log.error('failed to unlink', { error: err.message }) return res.status(500).json({ message: 'Failed to unlink that account' }) } } /** * GET /player/rust/permissions — what the site has given this player in game. * * Phase 7 made the website the author of in-game privilege and gave an operator * every view of it; this is the other side of that, and it is the first time a * player can see what they hold without asking one. Read-only by construction: * nothing a player can do here changes a grant, because a grant they could * change would not be a grant. * * The caller's Steam ids come from the link model rather than the permission * one, so the two questions stay in the files that own them — and the pushed * ledger is keyed by Steam id, which is the whole reason this read needs them. */ async function listPermissions(req, res) { try { const [accounts, serverRows] = await Promise.all([ links.listForUser(req.user.id), servers.listPublic(), ]) const held = await permissions.forPlayer( req.user.id, accounts.map((account) => account.steamId), serverRows, ) res.json({ ...held, accounts: accounts.length }) } catch (err) { log.error('failed to read a player’s entitlements', { error: err.message }) res.status(500).json({ message: 'Failed to read what you hold in game' }) } } module.exports = { listServers, listLinks, confirmLink, removeLink, listPermissions }