The public "Staff online" list on the Shard page exposed each staff member's in-game location (map + coordinates) to everyone, including logged-in players and unauthenticated visitors. Location is now privileged data: - Server: getOnline inspects the caller's role via getUserFromRequest (the same non-rejecting helper siteMode uses on public routes) and only includes map/x/y/z for admin/moderator callers. For everyone else the fields are omitted from the JSON entirely, so they can't be read from the network tab. serial + name (online status) still shown. - Client: Shard.jsx gates the location span on the viewer's role from useAuth() (same pattern as RoleGate); non-privileged viewers see who is online but no location field is rendered. Tests: publicShardOnline.test.js covers admin + moderator (location included), player + unauthenticated + editor (location omitted). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
106 lines
3.6 KiB
JavaScript
106 lines
3.6 KiB
JavaScript
// Staff-location visibility on GET /public/shard/online. The endpoint is
|
|
// token-free, so it inspects the caller's session (getUserFromRequest) and only
|
|
// includes each staff member's in-game location (map/x/y/z) for admins and
|
|
// moderators. Players and the public still see who is online, but not where.
|
|
//
|
|
// Point the DB at a closed port BEFORE requiring anything that builds the pool,
|
|
// so any stray query fails fast instead of hanging. The model + auth are stubbed,
|
|
// so the DB is never actually hit.
|
|
process.env.DB_HOST = '127.0.0.1'
|
|
process.env.DB_PORT = '59999'
|
|
|
|
const { test, after, afterEach } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const ctrl = require('../src/router/v1/public/shard.controller')
|
|
const shardState = require('../src/model/shardState/shardState.model')
|
|
const auth = require('../src/utils/auth')
|
|
const db = require('../src/utils/db')
|
|
|
|
after(() => db.close())
|
|
|
|
function mockRes() {
|
|
return {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(c) {
|
|
this.statusCode = c
|
|
return this
|
|
},
|
|
json(b) {
|
|
this.body = b
|
|
return this
|
|
},
|
|
}
|
|
}
|
|
|
|
// One online staff member with a location the model would return.
|
|
const ONLINE_ROW = { serial: '0x1', name: 'Lady Mod', map: 'Felucca', x: 1495, y: 1628, z: 10 }
|
|
|
|
const originals = {
|
|
listOnlineLinked: shardState.listOnlineLinked,
|
|
getUserFromRequest: auth.getUserFromRequest,
|
|
}
|
|
afterEach(() => {
|
|
shardState.listOnlineLinked = originals.listOnlineLinked
|
|
auth.getUserFromRequest = originals.getUserFromRequest
|
|
})
|
|
|
|
// Stub the model to return the staff member, and the session to the given viewer.
|
|
function setup(viewer) {
|
|
shardState.listOnlineLinked = async () => [ONLINE_ROW]
|
|
auth.getUserFromRequest = () => viewer
|
|
}
|
|
|
|
const LOCATION_KEYS = ['map', 'x', 'y', 'z']
|
|
|
|
for (const role of ['admin', 'moderator']) {
|
|
test(`getOnline includes location for a ${role}`, async () => {
|
|
setup({ id: 1, username: 'staff', role })
|
|
const res = mockRes()
|
|
await ctrl.getOnline({}, res)
|
|
assert.equal(res.statusCode, 200)
|
|
assert.equal(res.body.length, 1)
|
|
const entry = res.body[0]
|
|
assert.equal(entry.name, 'Lady Mod')
|
|
assert.equal(entry.serial, '0x1')
|
|
assert.equal(entry.map, 'Felucca')
|
|
assert.equal(entry.x, 1495)
|
|
assert.equal(entry.y, 1628)
|
|
assert.equal(entry.z, 10)
|
|
})
|
|
}
|
|
|
|
test('getOnline omits location for a logged-in player', async () => {
|
|
setup({ id: 2, username: 'joe', role: 'player' })
|
|
const res = mockRes()
|
|
await ctrl.getOnline({}, res)
|
|
assert.equal(res.statusCode, 200)
|
|
const entry = res.body[0]
|
|
// Still shows they are online…
|
|
assert.equal(entry.name, 'Lady Mod')
|
|
assert.equal(entry.serial, '0x1')
|
|
// …but the location fields are absent entirely (not null/placeholder).
|
|
for (const k of LOCATION_KEYS) assert.ok(!(k in entry), `expected "${k}" to be omitted`)
|
|
})
|
|
|
|
test('getOnline omits location for an unauthenticated request', async () => {
|
|
setup(null) // getUserFromRequest returns null for anon callers
|
|
const res = mockRes()
|
|
await ctrl.getOnline({}, res)
|
|
assert.equal(res.statusCode, 200)
|
|
const entry = res.body[0]
|
|
assert.equal(entry.name, 'Lady Mod')
|
|
assert.equal(entry.serial, '0x1')
|
|
for (const k of LOCATION_KEYS) assert.ok(!(k in entry), `expected "${k}" to be omitted`)
|
|
})
|
|
|
|
// An editor is staff but not admin/moderator — they should not see location.
|
|
test('getOnline omits location for an editor', async () => {
|
|
setup({ id: 3, username: 'ed', role: 'editor' })
|
|
const res = mockRes()
|
|
await ctrl.getOnline({}, res)
|
|
const entry = res.body[0]
|
|
for (const k of LOCATION_KEYS) assert.ok(!(k in entry), `expected "${k}" to be omitted`)
|
|
})
|