feat(rust): what the site has given a player, as the player reads it
Phase 8's website half. Phase 7 made the site the author of in-game
privilege and gave an operator every view of it; this is the other side,
and it is the first time a player can see what they hold without asking
one.
`GET /player/rust/permissions` is self-scoped in SQL and read-only by
construction — a grant a player could change would not be a grant. Three
things make it a different shape from the admin read rather than a
filtered one:
* the scope arithmetic is answered on the server. A client handed `*`
would have to know what the fleet is to say anything, and then
`inScope` exists twice. Each entry carries the servers it reaches,
already resolved and already marked.
* `live` is the pushed ledger, never the authored row. A grant is not a
privilege in a game until a sync confirmed it, and phase 7 is careful
never to record a push that silently did nothing — so "waiting" is
honest, and the alternative is the site claiming to have given
something it has not.
* nothing says WHY it is waiting. An offline server, a permission no
loaded plugin registered and a store that has never seen the account
all look the same from here; telling them apart is an operator's
diagnosis and an inventory of what is installed.
An entitlement that reaches nobody still lists, and the page says so —
authored against the website account, it exists before a Steam id does,
and hiding it until one turns up is the defect the admin user page
shipped in phase 7 (PLAN.md §20.5).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
This commit is contained in:
@@ -213,6 +213,46 @@ async function listLinks() {
|
||||
return core.query(`SELECT user_id AS userId, steam_id AS steamId FROM ${LINKS}`)
|
||||
}
|
||||
|
||||
// ---- one person's own half of all of it (the player tier) ----
|
||||
//
|
||||
// Every read below is scoped inside the statement rather than filtered after it.
|
||||
// The admin reads above answer "who holds what"; these answer "what do I hold",
|
||||
// and the difference between the two is a `WHERE` that must not be somebody
|
||||
// else's job to remember.
|
||||
|
||||
/** The groups one website user belongs to. Ordered the way the admin list is. */
|
||||
async function listGroupsForUser(userId) {
|
||||
return core.query(
|
||||
`SELECT g.name, g.title, g.\`rank\`, g.scope, m.added_at AS addedAt
|
||||
FROM ${GROUP_MEMBERS} m
|
||||
JOIN ${GROUPS} g ON g.name = m.group_name
|
||||
WHERE m.user_id = ?
|
||||
ORDER BY g.\`rank\` DESC, g.name ASC`,
|
||||
[userId],
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Every pushed row naming one of these Steam ids, across every server.
|
||||
*
|
||||
* The pushed ledger is keyed by Steam id because it records what is in a GAME
|
||||
* (D28's other half), so this is the one read in the file that starts from an
|
||||
* account rather than from a user. `kind` is carried through: a direct grant and
|
||||
* a group membership are different rows about the same person and only the
|
||||
* caller can say which of them it was looking for.
|
||||
*/
|
||||
async function listPushedForSteamIds(steamIds) {
|
||||
if (!steamIds.length) return []
|
||||
|
||||
return core.query(
|
||||
`SELECT server_id AS serverId, kind, subject, object
|
||||
FROM ${PUSHED}
|
||||
WHERE subject IN (${steamIds.map(() => '?').join(',')})
|
||||
AND kind IN ('grant', 'member')`,
|
||||
steamIds,
|
||||
)
|
||||
}
|
||||
|
||||
// ---- what is actually out there ----
|
||||
|
||||
async function listPushed(serverId) {
|
||||
@@ -440,6 +480,8 @@ module.exports = {
|
||||
deleteGrant,
|
||||
findUserByUsername,
|
||||
listLinks,
|
||||
listGroupsForUser,
|
||||
listPushedForSteamIds,
|
||||
listPushed,
|
||||
addPushed,
|
||||
removePushed,
|
||||
|
||||
Reference in New Issue
Block a user