mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 15:09:18 -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
|
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
|
* 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.
|
* 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
|
// 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
|
// 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.
|
// 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'],
|
tags: ['Room settings'],
|
||||||
summary: 'Delete a room',
|
summary: 'Delete a room',
|
||||||
description: [
|
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',
|
'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.',
|
'left alone — those live in the api/img world and outlive the room.',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
@@ -2468,7 +2477,7 @@ const app = new Hono<App>()
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const isOwner = room.CreatorAccountId === accountId
|
const isOwner = room.CreatorAccountId === accountId
|
||||||
if (!isOwner && !(await isStaff(c))) {
|
if (!isOwner && !(await isDeveloper(c))) {
|
||||||
return roomResult(c, {
|
return roomResult(c, {
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorId: 'Rooms.NotOwner',
|
ErrorId: 'Rooms.NotOwner',
|
||||||
@@ -2487,7 +2496,7 @@ const app = new Hono<App>()
|
|||||||
await c.env.CDN_ASSETS.delete(`room/${imageName}`)
|
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
|
// 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'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
|
// 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)
|
expect(interactions!.n).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('DELETE /rooms/:id also lets staff through — the website’s takedown — but only there', async () => {
|
it('DELETE /rooms/:id also lets developers through — the website’s takedown — but only there', async () => {
|
||||||
// Somebody else's room. A moderator token may delete it; the same token may NOT
|
// Somebody else's room. A developer token may delete it (a moderator's may not); the
|
||||||
// rename it, which is what a misapplied check once allowed.
|
// same token may NOT rename it, which is what a misapplied check once allowed.
|
||||||
await env.DB.prepare('INSERT INTO room (data) VALUES (?1)')
|
await env.DB.prepare('INSERT INTO room (data) VALUES (?1)')
|
||||||
.bind(
|
.bind(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -1775,7 +1775,7 @@ describe('rooms endpoints', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.run()
|
.run()
|
||||||
const staff = await bearer('999', ['gameClient', 'moderator'])
|
const staff = await bearer('999', ['gameClient', 'developer'])
|
||||||
const roomExists = async () =>
|
const roomExists = async () =>
|
||||||
(await env.DB.prepare('SELECT 1 FROM room WHERE room_id = 9501').first()) !== null
|
(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' })
|
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>) =>
|
const del = async (headers: Record<string, string>) =>
|
||||||
bodyOf(await SELF.fetch(`${ORIGIN}/rooms/9501`, { method: 'DELETE', headers }))
|
bodyOf(await SELF.fetch(`${ORIGIN}/rooms/9501`, { method: 'DELETE', headers }))
|
||||||
expect(await del(await bearer('2', ['gameClient']))).toMatchObject({
|
expect(await del(await bearer('2', ['gameClient']))).toMatchObject({
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorId: 'Rooms.NotOwner',
|
ErrorId: 'Rooms.NotOwner',
|
||||||
})
|
})
|
||||||
|
expect(await del(await bearer('998', ['gameClient', 'moderator']))).toMatchObject({
|
||||||
|
Success: false,
|
||||||
|
ErrorId: 'Rooms.NotOwner',
|
||||||
|
})
|
||||||
expect(await roomExists()).toBe(true)
|
expect(await roomExists()).toBe(true)
|
||||||
expect(await del(staff)).toMatchObject({ Success: true })
|
expect(await del(staff)).toMatchObject({ Success: true })
|
||||||
expect(await roomExists()).toBe(false)
|
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 /
|
* Delete a room outright — the developer takedown. `rooms` lets the owner or a developer
|
||||||
* moderator through and refuses everyone else; it answers a refusal as a 200 carrying
|
* 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.
|
* `Success: false` (the game's envelope), so the check is on the body, not the status.
|
||||||
*/
|
*/
|
||||||
async function deleteRoom(roomId: number): Promise<void> {
|
async function deleteRoom(roomId: number): Promise<void> {
|
||||||
@@ -2817,12 +2817,12 @@ function PublicRoomView({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Staff only, and cosmetic: hidden for everyone else, but `rooms` checks the token's
|
{/* 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() && (
|
{isAdmin() && (
|
||||||
<>
|
<>
|
||||||
{isDeveloper() && <StaffRoomTokens room={room} />}
|
{isDeveloper() && <StaffRoomTokens room={room} />}
|
||||||
<StaffListing room={room} onRoomChange={onRoomChange} />
|
<StaffListing room={room} onRoomChange={onRoomChange} />
|
||||||
<StaffTakedown room={room} onTakenDown={onTakenDown} />
|
{isDeveloper() && <StaffTakedown room={room} onTakenDown={onTakenDown} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user