mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[www] gate delete room on developer
This commit is contained in:
@@ -517,6 +517,15 @@ async function isStaff(c: Context<App>): Promise<boolean> {
|
||||
return roles?.some((role) => STAFF_ROLES.has(role)) ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the caller's token carries the `developer` role — the narrower of the two staff
|
||||
* roles, for the actions a moderator may not take (deleting a room that isn't theirs).
|
||||
*/
|
||||
async function isDeveloper(c: Context<App>): Promise<boolean> {
|
||||
const roles = await validateAndGetRoles(c.req.raw, await c.env.JWT_SECRET.get())
|
||||
return roles?.includes('developer') ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the `:playerId` in the path is banned from the `:roomId` in it — the read behind
|
||||
* both `isBanned` routes, which differ only in the envelope they wrap the answer in.
|
||||
@@ -2433,7 +2442,7 @@ const app = new Hono<App>()
|
||||
}
|
||||
)
|
||||
|
||||
// Delete a room. Auth-gated (401); owners and staff (developers/moderators) may delete.
|
||||
// Delete a room. Auth-gated (401); the owner or a developer may delete — not a moderator.
|
||||
// Removes the room record (and per-player interactions with it) and the room's
|
||||
// image object from the shared CDN bucket. Images players *took* in the room are
|
||||
// left alone — they live in the api/img world and outlast the room.
|
||||
@@ -2443,7 +2452,7 @@ const app = new Hono<App>()
|
||||
tags: ['Room settings'],
|
||||
summary: 'Delete a room',
|
||||
description: [
|
||||
'Owner or staff. Removes the room record, the per-player interactions with it, and the',
|
||||
'Owner or developer (a moderator token is refused). Removes the room record, the per-player interactions with it, and the',
|
||||
'room’s image object from the shared CDN bucket. Photos players TOOK in the room are',
|
||||
'left alone — those live in the api/img world and outlive the room.',
|
||||
].join(' '),
|
||||
@@ -2468,7 +2477,7 @@ const app = new Hono<App>()
|
||||
})
|
||||
}
|
||||
const isOwner = room.CreatorAccountId === accountId
|
||||
if (!isOwner && !(await isStaff(c))) {
|
||||
if (!isOwner && !(await isDeveloper(c))) {
|
||||
return roomResult(c, {
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.NotOwner',
|
||||
@@ -2487,7 +2496,7 @@ const app = new Hono<App>()
|
||||
await c.env.CDN_ASSETS.delete(`room/${imageName}`)
|
||||
}
|
||||
|
||||
// A takedown — staff deleting a room that isn't theirs — goes in the audit log; an
|
||||
// A takedown — a developer deleting a room that isn't theirs — goes in the audit log; an
|
||||
// owner deleting their own room is their business and doesn't. The row keeps the
|
||||
// room's identity, since the room itself is gone and nothing else will answer "which
|
||||
// room was that". Written after the fact and caught: a failed insert must not report
|
||||
|
||||
@@ -1759,9 +1759,9 @@ describe('rooms endpoints', () => {
|
||||
expect(interactions!.n).toBe(0)
|
||||
})
|
||||
|
||||
it('DELETE /rooms/:id also lets staff through — the website’s takedown — but only there', async () => {
|
||||
// Somebody else's room. A moderator token may delete it; the same token may NOT
|
||||
// rename it, which is what a misapplied check once allowed.
|
||||
it('DELETE /rooms/:id also lets developers through — the website’s takedown — but only there', async () => {
|
||||
// Somebody else's room. A developer token may delete it (a moderator's may not); the
|
||||
// same token may NOT rename it, which is what a misapplied check once allowed.
|
||||
await env.DB.prepare('INSERT INTO room (data) VALUES (?1)')
|
||||
.bind(
|
||||
JSON.stringify({
|
||||
@@ -1775,7 +1775,7 @@ describe('rooms endpoints', () => {
|
||||
})
|
||||
)
|
||||
.run()
|
||||
const staff = await bearer('999', ['gameClient', 'moderator'])
|
||||
const staff = await bearer('999', ['gameClient', 'developer'])
|
||||
const roomExists = async () =>
|
||||
(await env.DB.prepare('SELECT 1 FROM room WHERE room_id = 9501').first()) !== null
|
||||
|
||||
@@ -1786,13 +1786,17 @@ describe('rooms endpoints', () => {
|
||||
})
|
||||
expect(await bodyOf(rename)).toMatchObject({ Success: false, ErrorId: 'Rooms.NotOwner' })
|
||||
|
||||
// A plain player is still refused; the moderator gets through.
|
||||
// A plain player and a moderator are refused; the developer gets through.
|
||||
const del = async (headers: Record<string, string>) =>
|
||||
bodyOf(await SELF.fetch(`${ORIGIN}/rooms/9501`, { method: 'DELETE', headers }))
|
||||
expect(await del(await bearer('2', ['gameClient']))).toMatchObject({
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.NotOwner',
|
||||
})
|
||||
expect(await del(await bearer('998', ['gameClient', 'moderator']))).toMatchObject({
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.NotOwner',
|
||||
})
|
||||
expect(await roomExists()).toBe(true)
|
||||
expect(await del(staff)).toMatchObject({ Success: true })
|
||||
expect(await roomExists()).toBe(false)
|
||||
|
||||
@@ -1044,8 +1044,8 @@ const fetchRoomPhotos = (roomId: number): Promise<PublicPhoto[]> =>
|
||||
)
|
||||
|
||||
/**
|
||||
* Delete a room outright — the staff takedown. `rooms` lets the owner or a developer /
|
||||
* moderator through and refuses everyone else; it answers a refusal as a 200 carrying
|
||||
* Delete a room outright — the developer takedown. `rooms` lets the owner or a developer
|
||||
* through and refuses everyone else, moderators included; it answers a refusal as a 200 carrying
|
||||
* `Success: false` (the game's envelope), so the check is on the body, not the status.
|
||||
*/
|
||||
async function deleteRoom(roomId: number): Promise<void> {
|
||||
@@ -2817,12 +2817,12 @@ function PublicRoomView({
|
||||
</section>
|
||||
|
||||
{/* Staff only, and cosmetic: hidden for everyone else, but `rooms` checks the token's
|
||||
role itself on the DELETE and the listing PUT. */}
|
||||
role itself on the DELETE (developers only) and the listing PUT. */}
|
||||
{isAdmin() && (
|
||||
<>
|
||||
{isDeveloper() && <StaffRoomTokens room={room} />}
|
||||
<StaffListing room={room} onRoomChange={onRoomChange} />
|
||||
<StaffTakedown room={room} onTakenDown={onTakenDown} />
|
||||
{isDeveloper() && <StaffTakedown room={room} onTakenDown={onTakenDown} />}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user