mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
implement stubs for /similar, visited, favroited, ownedby...implement clone
This commit is contained in:
+123
-20
@@ -5,11 +5,17 @@ import { withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
|
||||
import { validateAndGetAccountId } from './jwt'
|
||||
import {
|
||||
cloneRoom,
|
||||
getBaseRooms,
|
||||
getFavoritedRooms,
|
||||
getHotRooms,
|
||||
getInteraction,
|
||||
getRoomById,
|
||||
getRoomByName,
|
||||
getRoomsByCreator,
|
||||
getRoomsByIds,
|
||||
getSimilarRooms,
|
||||
getVisitedRooms,
|
||||
searchRooms,
|
||||
toggleCheer,
|
||||
toggleFavorite,
|
||||
@@ -72,16 +78,30 @@ function photonAccessToken() {
|
||||
}
|
||||
}
|
||||
|
||||
/** The account whose owned rooms to return: the Bearer token's `sub`, falling
|
||||
* back to account 1 (the stub player) when there's no valid token. */
|
||||
async function ownerId(c: Context<App>): Promise<number> {
|
||||
/** The Bearer token's account id (`sub`), or null when there's no valid token. */
|
||||
async function authedAccountId(c: Context<App>): Promise<number | null> {
|
||||
const authHeader = c.req.header('Authorization') ?? ''
|
||||
if (authHeader.toLowerCase().startsWith('bearer ')) {
|
||||
const sub = await validateAndGetAccountId(authHeader.slice('Bearer '.length))
|
||||
const id = sub ? Number.parseInt(sub, 10) : Number.NaN
|
||||
if (!Number.isNaN(id)) return id
|
||||
}
|
||||
return 1
|
||||
if (!authHeader.toLowerCase().startsWith('bearer ')) return null
|
||||
const sub = await validateAndGetAccountId(authHeader.slice('Bearer '.length))
|
||||
const id = sub ? Number.parseInt(sub, 10) : Number.NaN
|
||||
return Number.isNaN(id) ? null : id
|
||||
}
|
||||
|
||||
/** 401 for the auth-gated `*by/me` endpoints — no stub-account fallback. */
|
||||
function unauthorized(c: Context<App>) {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
/** Client envelope for room clone results: `{ success, error, value }`. */
|
||||
function cloneResult(c: Context<App>, value: unknown, error = '') {
|
||||
return c.json({ success: error === '', error, value })
|
||||
}
|
||||
|
||||
/** Rooms created/owned by the authed caller (shared by the createdby/ownedby routes). */
|
||||
async function ownedRooms(c: Context<App>) {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
return c.json(await getRoomsByCreator(c.env.DB, accountId))
|
||||
}
|
||||
|
||||
const app = new Hono<App>()
|
||||
@@ -127,6 +147,25 @@ const app = new Hono<App>()
|
||||
return c.json(await searchRooms(c.env.DB, query, skip, take))
|
||||
})
|
||||
|
||||
// "Hot" rooms feed — public, non-dorm rooms ordered by engagement, optionally
|
||||
// filtered to a single `tag` (e.g. `rro`). Paginated via skip/take (take
|
||||
// defaults to 100). Returns `{ Results, TotalResults }` like search.
|
||||
.get('/rooms/hot', async (c) => {
|
||||
const tag = c.req.query('tag') ?? ''
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
return c.json(await getHotRooms(c.env.DB, tag, skip, take))
|
||||
})
|
||||
|
||||
// "Base" rooms — template rooms (tagged `base`) the client offers when creating
|
||||
// a room. Returned regardless of accessibility. Paginated via skip/take (take
|
||||
// defaults to 100). Returns a bare array.
|
||||
.get('/rooms/base', async (c) => {
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
return c.json(await getBaseRooms(c.env.DB, skip, take))
|
||||
})
|
||||
|
||||
// Bulk room lookup by `id` or `name` — returns an array of matched rooms (the
|
||||
// client calls this bare on the rooms host). Rooms not in D1 are simply absent
|
||||
// from the result; the client treats an empty result as NoSuchRoom.
|
||||
@@ -144,42 +183,106 @@ const app = new Hono<App>()
|
||||
})
|
||||
|
||||
// Rooms created/owned by the caller (their dorm). The client calls all three.
|
||||
.get('/roomserver/rooms/createdby/me', async (c) =>
|
||||
c.json(await getRoomsByCreator(c.env.DB, await ownerId(c)))
|
||||
)
|
||||
.get('/rooms/ownedby/me', async (c) => c.json(await getRoomsByCreator(c.env.DB, await ownerId(c))))
|
||||
.get('/rooms/createdby/me', async (c) => c.json(await getRoomsByCreator(c.env.DB, await ownerId(c))))
|
||||
// Auth-gated — no token is a 401, never account 1.
|
||||
.get('/roomserver/rooms/createdby/me', ownedRooms)
|
||||
.get('/rooms/ownedby/me', ownedRooms)
|
||||
.get('/rooms/createdby/me', ownedRooms)
|
||||
|
||||
// Rooms the caller has favorited (from the interaction table). Auth-gated.
|
||||
// Paginated via skip/take (take defaults to 100). Returns a bare array, like the
|
||||
// other room-source `*by/me` lists the client loads.
|
||||
.get('/rooms/favoritedby/me', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
return c.json(await getFavoritedRooms(c.env.DB, accountId, skip, take))
|
||||
})
|
||||
|
||||
// Rooms the caller has visited (interaction rows with a last-visited time).
|
||||
// Auth-gated. Paginated via skip/take (take defaults to 100). Returns a bare array.
|
||||
.get('/rooms/visitedby/me', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
return c.json(await getVisitedRooms(c.env.DB, accountId, skip, take))
|
||||
})
|
||||
|
||||
// The current player's interaction state with a room (cheered/favorited/last
|
||||
// visited), read from the `interaction` table.
|
||||
// visited), read from the `interaction` table. Auth-gated.
|
||||
.get('/rooms/:roomId{[0-9]+}/interactionby/me', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
const interaction = await getInteraction(
|
||||
c.env.DB,
|
||||
await ownerId(c),
|
||||
accountId,
|
||||
Number.parseInt(c.req.param('roomId'), 10)
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
|
||||
// Toggle the player's cheer/favorite on a room. Both are PUTs that flip the
|
||||
// stored flag and return the updated interaction.
|
||||
// Toggle the player's cheer/favorite on a room. Both are auth-gated PUTs that
|
||||
// flip the stored flag and return the updated interaction.
|
||||
.put('/rooms/:roomId{[0-9]+}/interactionby/me/cheer', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
const interaction = await toggleCheer(
|
||||
c.env.DB,
|
||||
await ownerId(c),
|
||||
accountId,
|
||||
Number.parseInt(c.req.param('roomId'), 10)
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
.put('/rooms/:roomId{[0-9]+}/interactionby/me/favorite', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
const interaction = await toggleFavorite(
|
||||
c.env.DB,
|
||||
await ownerId(c),
|
||||
accountId,
|
||||
Number.parseInt(c.req.param('roomId'), 10)
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
|
||||
// Clone a room into a new one owned by the caller, using the `name` form field
|
||||
// (also accepted as a query param). Auth is required — no valid token is a 401,
|
||||
// with no stub-account fallback. Returns the `{ success, error, value }` envelope
|
||||
// the client expects; business failures are 200 with success:false.
|
||||
.post('/rooms/:roomId{[0-9]+}/clone', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) {
|
||||
return c.json({ success: false, error: 'Unauthorized', value: null }, 401)
|
||||
}
|
||||
|
||||
const body = (await c.req.parseBody().catch(() => ({}))) as Record<string, unknown>
|
||||
const raw = body.name ?? c.req.query('name') ?? ''
|
||||
const name = typeof raw === 'string' ? raw.trim() : ''
|
||||
|
||||
if (name === '') return cloneResult(c, null, 'You must enter a name for your room.')
|
||||
if (await getRoomByName(c.env.DB, name)) {
|
||||
return cloneResult(c, null, 'A room with that name already exists!')
|
||||
}
|
||||
const room = await cloneRoom(
|
||||
c.env.DB,
|
||||
Number.parseInt(c.req.param('roomId'), 10),
|
||||
name,
|
||||
accountId
|
||||
)
|
||||
if (!room) return cloneResult(c, null, "You can't clone this room!")
|
||||
return cloneResult(c, room)
|
||||
})
|
||||
|
||||
// Rooms similar to the given room (sharing tags). Paginated via skip/take (take
|
||||
// defaults to 100). Returns a bare array; empty when the room is unknown/untagged.
|
||||
.get('/rooms/:roomId{[0-9]+}/similar', async (c) => {
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
return c.json(
|
||||
await getSimilarRooms(c.env.DB, Number.parseInt(c.req.param('roomId'), 10), skip, take)
|
||||
)
|
||||
})
|
||||
|
||||
// Single room by id. 404 when the room isn't in D1. Ignores the
|
||||
// include/unityAsset* query params.
|
||||
.get('/rooms/:roomId{[0-9]+}', async (c) => {
|
||||
|
||||
Reference in New Issue
Block a user