mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 07:58:55 -07:00
[rooms] fix permissions update
This commit is contained in:
@@ -855,8 +855,9 @@ export const MoveSubRoomRequest = z.object({
|
|||||||
* A room in the PascalCase `{ Value, Success, Error, error_id }` envelope — the same mixed
|
* A room in the PascalCase `{ Value, Success, Error, error_id }` envelope — the same mixed
|
||||||
* casing the unprefixed isBanned check has ({@link IsBannedPascalEnvelope}), NOT the
|
* casing the unprefixed isBanned check has ({@link IsBannedPascalEnvelope}), NOT the
|
||||||
* lowercase `{ success, error, value }` most room mutations answer. The newer settings
|
* lowercase `{ success, error, value }` most room mutations answer. The newer settings
|
||||||
* routes (the subroom move, the max-player calculation mode) answer this one; the client
|
* routes (the subroom move, the max-player calculation mode, the subroom permissions)
|
||||||
* decodes them with a different reader, so the two are kept apart deliberately.
|
* answer this one; the client decodes them with a different reader, so the two are kept
|
||||||
|
* apart deliberately.
|
||||||
*/
|
*/
|
||||||
export const RoomPascalEnvelope = z.object({
|
export const RoomPascalEnvelope = z.object({
|
||||||
Value: RoomDto.nullable().describe('The room as it now stands; null on a rejection'),
|
Value: RoomDto.nullable().describe('The room as it now stands; null on a rejection'),
|
||||||
|
|||||||
+22
-10
@@ -4067,7 +4067,12 @@ const app = new Hono<App>()
|
|||||||
// creator or a co-owner (403), the same `canManageRoom` the other room-admin writes
|
// creator or a co-owner (403), the same `canManageRoom` the other room-admin writes
|
||||||
// take; a creator-only check here locked out co-owners the client shows the screen to,
|
// take; a creator-only check here locked out co-owners the client shows the screen to,
|
||||||
// and an owner whose room names them in `Roles` rather than `CreatorAccountId`. Answers
|
// and an owner whose room names them in `Roles` rather than `CreatorAccountId`. Answers
|
||||||
// an EMPTY 200 — the client fires this and re-reads nothing, so there is no envelope.
|
// the whole ROOM under `Value` in the PascalCase `{ Value, Success, Error, error_id }`
|
||||||
|
// envelope — the same reader the subroom move and max-player mode routes feed, and the
|
||||||
|
// same Room DTO `GET /rooms/{id}` serves (the permissions themselves aren't on it). A
|
||||||
|
// rejection is HTTP 200 with `Success: false` and the message in `Error`; only a
|
||||||
|
// missing token is a 401. This once answered an empty 200, which the client's reader
|
||||||
|
// couldn't decode.
|
||||||
.put(
|
.put(
|
||||||
'/rooms/:roomId{[0-9]+}/subrooms/:subRoomId{[0-9]+}/permissions',
|
'/rooms/:roomId{[0-9]+}/subrooms/:subRoomId{[0-9]+}/permissions',
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -4093,33 +4098,40 @@ const app = new Hono<App>()
|
|||||||
'to the subroom the caller is standing in, resolved from presence.',
|
'to the subroom the caller is standing in, resolved from presence.',
|
||||||
'',
|
'',
|
||||||
'Gated to the room’s managers — its creator or a co-owner (`canManageRoom`), as the',
|
'Gated to the room’s managers — its creator or a co-owner (`canManageRoom`), as the',
|
||||||
'other room-admin writes are. The response body is EMPTY: the client doesn’t read one.',
|
'other room-admin writes are. Answers the whole ROOM under `Value` in the PascalCase',
|
||||||
|
'`{ Value, Success, Error, error_id }` envelope — the same Room DTO `GET /rooms/{id}`',
|
||||||
|
'serves (the permission table itself is not part of it), and the same envelope the',
|
||||||
|
'subroom move and `max_player_calculation_mode` answer, NOT the lowercase',
|
||||||
|
'`{ success, error, value }` the other subroom mutations use. A rejection is HTTP 200',
|
||||||
|
'with `Success: false`, `Value: null` and the message in `Error`; only a missing',
|
||||||
|
'token is a 401.',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
security: AUTHED,
|
security: AUTHED,
|
||||||
parameters: [roomIdParam, subRoomIdParam],
|
parameters: [roomIdParam, subRoomIdParam],
|
||||||
requestBody: jsonBody(SubRoomPermissionsRequest, 'The permission entries to set'),
|
requestBody: jsonBody(SubRoomPermissionsRequest, 'The permission entries to set'),
|
||||||
responses: {
|
responses: {
|
||||||
200: { description: 'Stored (empty body)' },
|
200: json(RoomPascalEnvelope, 'The room, or a rejection with `Success: false`'),
|
||||||
401: UNAUTHORIZED_RESPONSE,
|
401: { description: 'No bearer token (empty body)' },
|
||||||
403: FORBIDDEN_RESPONSE,
|
|
||||||
404: { description: 'No such room or subroom' },
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const accountId = await authedAccountId(c)
|
const accountId = await authedAccountId(c)
|
||||||
if (accountId === null) return unauthorized(c)
|
if (accountId === null) return c.body(null, 401)
|
||||||
|
const refuse = (error: string) =>
|
||||||
|
c.json({ Value: null, Success: false, Error: error, error_id: null })
|
||||||
|
|
||||||
const roomId = Number.parseInt(c.req.param('roomId'), 10)
|
const roomId = Number.parseInt(c.req.param('roomId'), 10)
|
||||||
const subRoomId = Number.parseInt(c.req.param('subRoomId'), 10)
|
const subRoomId = Number.parseInt(c.req.param('subRoomId'), 10)
|
||||||
|
|
||||||
// Scoped through the room so a subroom id from another room can't be written.
|
// Scoped through the room so a subroom id from another room can't be written.
|
||||||
const room = await getRoomById(c.env.DB, roomId)
|
const room = await getRoomById(c.env.DB, roomId)
|
||||||
if (!room || !findSubRoom(room, subRoomId)) return c.notFound()
|
if (!room) return refuse('This room does not exist!')
|
||||||
if (!canManageRoom(room, accountId)) return c.body(null, 403)
|
if (!findSubRoom(room, subRoomId)) return refuse('This subroom does not exist!')
|
||||||
|
if (!canManageRoom(room, accountId)) return refuse('You are not the owner of this room!')
|
||||||
|
|
||||||
const permissions = parseRoomPermissions(await c.req.json().catch(() => null))
|
const permissions = parseRoomPermissions(await c.req.json().catch(() => null))
|
||||||
await setSubRoomPermissions(c.env.DB, subRoomId, permissions)
|
await setSubRoomPermissions(c.env.DB, subRoomId, permissions)
|
||||||
return c.body(null, 200)
|
return c.json({ Value: room, Success: true, Error: null, error_id: null })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4818,42 +4818,66 @@ describe('rooms endpoints', () => {
|
|||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Envelope = {
|
||||||
|
Value: { RoomId: number; SubRooms: unknown[] } | null
|
||||||
|
Success: boolean
|
||||||
|
Error: string | null
|
||||||
|
error_id: null
|
||||||
|
}
|
||||||
|
const refusal = async (res: Response): Promise<string | null> => {
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const env = (await res.json()) as Envelope
|
||||||
|
expect(env.Success).toBe(false)
|
||||||
|
expect(env.Value).toBeNull()
|
||||||
|
expect(env.error_id).toBeNull()
|
||||||
|
return env.Error
|
||||||
|
}
|
||||||
|
|
||||||
it('is auth-gated and open to the room’s managers only', async () => {
|
it('is auth-gated and open to the room’s managers only', async () => {
|
||||||
const body = [
|
const body = [
|
||||||
{ Permission: 'CAN_SAVE_INVENTIONS', Role: 30, Override: false, Type: 0, Value: 'True' },
|
{ Permission: 'CAN_SAVE_INVENTIONS', Role: 30, Override: false, Type: 0, Value: 'True' },
|
||||||
]
|
]
|
||||||
// No token → 401.
|
// No token → 401.
|
||||||
expect((await putPermissions('/rooms/2/subrooms/2/permissions', body)).status).toBe(401)
|
expect((await putPermissions('/rooms/2/subrooms/2/permissions', body)).status).toBe(401)
|
||||||
// A valid token that isn't the room's creator or a co-owner → 403.
|
// A valid token that isn't the room's creator or a co-owner → a `Success: false`
|
||||||
expect((await putPermissions('/rooms/2/subrooms/2/permissions', body, '999')).status).toBe(
|
// rejection, as the other PascalCase-envelope routes answer.
|
||||||
403
|
expect(
|
||||||
)
|
await refusal(await putPermissions('/rooms/2/subrooms/2/permissions', body, '999'))
|
||||||
|
).toBe('You are not the owner of this room!')
|
||||||
// A co-owner may: account 2 holds Role 30 on the seeded rooms. (This body clears a
|
// A co-owner may: account 2 holds Role 30 on the seeded rooms. (This body clears a
|
||||||
// pair that was never stored, so it changes nothing.)
|
// pair that was never stored, so it changes nothing.)
|
||||||
expect((await putPermissions('/rooms/2/subrooms/2/permissions', body, '2')).status).toBe(200)
|
expect((await putPermissions('/rooms/2/subrooms/2/permissions', body, '2')).status).toBe(200)
|
||||||
// A Host (a lower tier) may not.
|
// A Host (a lower tier) may not.
|
||||||
await setRoles()
|
await setRoles()
|
||||||
expect((await putPermissions('/rooms/2/subrooms/2/permissions', body, '748')).status).toBe(
|
expect(
|
||||||
403
|
await refusal(await putPermissions('/rooms/2/subrooms/2/permissions', body, '748'))
|
||||||
)
|
).toBe('You are not the owner of this room!')
|
||||||
// Unknown room / unknown subroom → 404.
|
// Unknown room / unknown subroom.
|
||||||
expect((await putPermissions('/rooms/99999/subrooms/2/permissions', body, '1')).status).toBe(
|
expect(
|
||||||
404
|
await refusal(await putPermissions('/rooms/99999/subrooms/2/permissions', body, '1'))
|
||||||
)
|
).toBe('This room does not exist!')
|
||||||
// A subroom id belonging to another room doesn't resolve either.
|
// A subroom id belonging to another room doesn't resolve either.
|
||||||
expect((await putPermissions('/rooms/2/subrooms/9999/permissions', body, '1')).status).toBe(
|
expect(
|
||||||
404
|
await refusal(await putPermissions('/rooms/2/subrooms/9999/permissions', body, '1'))
|
||||||
)
|
).toBe('This subroom does not exist!')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('answers an empty 200 — the client reads no body', async () => {
|
it('answers the whole room in the PascalCase `{ Value, Success, Error, error_id }` envelope', async () => {
|
||||||
const res = await putPermissions(
|
const res = await putPermissions(
|
||||||
'/rooms/2/subrooms/2/permissions',
|
'/rooms/2/subrooms/2/permissions',
|
||||||
[{ Permission: 'CAN_SPAWN_INVENTIONS', Role: 30, Override: true, Type: 0, Value: 'True' }],
|
[{ Permission: 'CAN_SPAWN_INVENTIONS', Role: 30, Override: true, Type: 0, Value: 'True' }],
|
||||||
'1'
|
'1'
|
||||||
)
|
)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(await res.text()).toBe('')
|
const env = (await res.json()) as Envelope
|
||||||
|
expect(env.Success).toBe(true)
|
||||||
|
expect(env.Error).toBeNull()
|
||||||
|
expect(env.error_id).toBeNull()
|
||||||
|
// `Value` is the same Room DTO `GET /rooms/2` serves — subrooms re-attached and all.
|
||||||
|
const room = (await (
|
||||||
|
await SELF.fetch(`${ORIGIN}/rooms/2`, { headers: await bearer('1') })
|
||||||
|
).json()) as Envelope['Value']
|
||||||
|
expect(env.Value).toEqual(room)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('a Role 0 grant reaches a visitor; a Role 30 grant does not', async () => {
|
it('a Role 0 grant reaches a visitor; a Role 30 grant does not', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user