mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
tags, dorms
This commit is contained in:
@@ -125,6 +125,29 @@ export async function setRoomImage(db: D1Database, roomId: number, imageName: st
|
||||
.run()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user tag (`Type: 0`) to a room's `Tags`, skipping it when already present
|
||||
* (case-insensitive). The caller supplies the already-loaded room (owner-checked)
|
||||
* to avoid a re-read; the whole room JSON is rewritten. Returns the updated room.
|
||||
*/
|
||||
export async function addRoomTag(
|
||||
db: D1Database,
|
||||
roomId: number,
|
||||
room: Room,
|
||||
tag: string
|
||||
): Promise<Room> {
|
||||
const tags = Array.isArray(room.Tags) ? (room.Tags as Array<Record<string, unknown>>) : []
|
||||
if (!tags.some((t) => String(t?.Tag).toLowerCase() === tag.toLowerCase())) {
|
||||
tags.push({ Tag: tag, Type: 0 })
|
||||
}
|
||||
const updated: Room = { ...room, Tags: tags }
|
||||
await db
|
||||
.prepare('UPDATE rooms SET data = ?2 WHERE room_id = ?1')
|
||||
.bind(roomId, JSON.stringify(updated))
|
||||
.run()
|
||||
return updated
|
||||
}
|
||||
|
||||
/** Find a subroom (by SubRoomId) inside a room's `SubRooms` array, or undefined. */
|
||||
export function findSubRoom(room: Room, subRoomId: number): Record<string, unknown> | undefined {
|
||||
const subRooms = Array.isArray(room.SubRooms)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { logger, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
|
||||
import { validateAndGetAccountId } from './jwt'
|
||||
import {
|
||||
addRoomTag,
|
||||
cloneRoom,
|
||||
findSubRoom,
|
||||
getBaseRooms,
|
||||
@@ -503,6 +504,43 @@ const app = new Hono<App>()
|
||||
return roomResult(c, { Success: true })
|
||||
})
|
||||
|
||||
// Add a tag to a room. Auth-gated (401) and owner-only. Body is the `tag` form
|
||||
// field; the tag is added as a user tag (Type 0), deduped case-insensitively.
|
||||
// Business results use the `{ Success, Value, ErrorId, Error }` envelope at 200.
|
||||
.put('/rooms/:roomId{[0-9]+}/tags', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
|
||||
const roomId = Number.parseInt(c.req.param('roomId'), 10)
|
||||
const room = await getRoomById(c.env.DB, roomId)
|
||||
if (!room) {
|
||||
return roomResult(c, {
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.DoesntExist',
|
||||
Error: 'This room does not exist!',
|
||||
})
|
||||
}
|
||||
if (room.CreatorAccountId !== accountId) {
|
||||
return roomResult(c, {
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.NotOwner',
|
||||
Error: 'You are not the owner of this room!',
|
||||
})
|
||||
}
|
||||
|
||||
const body = (await c.req.parseBody().catch(() => ({}))) as Record<string, unknown>
|
||||
const tag = typeof body.tag === 'string' ? body.tag.trim() : ''
|
||||
if (tag === '') {
|
||||
return roomResult(c, {
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.InvalidTag',
|
||||
Error: 'You must provide a tag!',
|
||||
})
|
||||
}
|
||||
await addRoomTag(c.env.DB, roomId, room, tag)
|
||||
return roomResult(c, { Success: true })
|
||||
})
|
||||
|
||||
// Set a room's image. Auth-gated (401) and owner-only. Body is the `imageName`
|
||||
// form field (a key from the storage/image upload). Business results use the
|
||||
// `{ Success, Value, ErrorId, Error }` envelope at HTTP 200.
|
||||
|
||||
@@ -640,6 +640,40 @@ describe('rooms endpoints', () => {
|
||||
expect(await bodyOf(coOwner)).toMatchObject({ SubRoomId: 2, CreatorAccountId: 1 })
|
||||
})
|
||||
|
||||
it('PUT /rooms/:id/tags is auth-gated, owner-only, dedupes, and persists', async () => {
|
||||
// No token → 401.
|
||||
expect((await putForm('/rooms/2/tags', { tag: 'quest' })).status).toBe(401)
|
||||
// Not the owner → NotOwner envelope.
|
||||
expect(await bodyOf(await putForm('/rooms/2/tags', { tag: 'quest' }, '999'))).toMatchObject({
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.NotOwner',
|
||||
})
|
||||
// Unknown room → DoesntExist.
|
||||
expect(await bodyOf(await putForm('/rooms/99999/tags', { tag: 'quest' }, '1'))).toMatchObject({
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.DoesntExist',
|
||||
})
|
||||
// Empty tag → InvalidTag.
|
||||
expect(await bodyOf(await putForm('/rooms/2/tags', { tag: ' ' }, '1'))).toMatchObject({
|
||||
Success: false,
|
||||
ErrorId: 'Rooms.InvalidTag',
|
||||
})
|
||||
|
||||
// Owner adds a tag → it persists as a Type-0 user tag.
|
||||
expect(await bodyOf(await putForm('/rooms/2/tags', { tag: 'quest' }, '1'))).toMatchObject({
|
||||
Success: true,
|
||||
})
|
||||
const tagsOf = async () =>
|
||||
((await (await SELF.fetch(`${ORIGIN}/rooms/2`)).json()) as {
|
||||
Tags: Array<{ Tag: string; Type: number }>
|
||||
}).Tags
|
||||
expect(await tagsOf()).toContainEqual({ Tag: 'quest', Type: 0 })
|
||||
|
||||
// Adding the same tag again (different case) is a no-op — no duplicate.
|
||||
await putForm('/rooms/2/tags', { tag: 'QUEST' }, '1')
|
||||
expect((await tagsOf()).filter((t) => t.Tag.toLowerCase() === 'quest')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('PUT /rooms/:id/name is auth-gated, owner-only, unique, and persists', async () => {
|
||||
// No token → 401 (auth gate).
|
||||
expect((await putForm('/rooms/2/name', { name: 'Whatever' })).status).toBe(401)
|
||||
|
||||
Reference in New Issue
Block a user