mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[www] show private photos when logged in
This commit is contained in:
@@ -73,6 +73,17 @@ const typeFolder: Record<number, string> = {
|
||||
*/
|
||||
const PHOTO_TAGGING_KEY = 'playerPhotoTaggingSetting'
|
||||
|
||||
/**
|
||||
* The owner's-view clause the player photo lists share in their docs. The lists are
|
||||
* public, but they read a Bearer token if one is sent, and the one player it can unlock
|
||||
* anything for is the player whose list it is.
|
||||
*/
|
||||
const OWN_VIEW_NOTE =
|
||||
'Public, but reads a Bearer token if one is sent: a player asking for THEIR OWN list ' +
|
||||
'gets their private photos in it too (`Accessibility` 0 — that is where the photos they ' +
|
||||
'have not shared yet live). Any other player’s token, no token, or an invalid one serves ' +
|
||||
'the public list; none of those is a 401.'
|
||||
|
||||
/**
|
||||
* The preference a player has before they have ever set one. The value is an opaque enum
|
||||
* ordinal to this server (see `PhotoTaggingSettingRequest`), and 0 is what an unset .NET
|
||||
@@ -373,15 +384,22 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
||||
// A player's photos — the public images that player has taken, newest first.
|
||||
// Paginated via skip/take (take defaults to 100). Returns a bare array of the
|
||||
// client's ImagesPlayer projection (SavedImageId/SavedImageType, not Id/Type).
|
||||
//
|
||||
// Public, but a token is READ if one is sent: a player asking for their own list gets
|
||||
// their private photos in it too (the profile they open on themselves is where those
|
||||
// live), while anyone else's token — or none — serves the public list. A bad token is
|
||||
// not a 401 here; it just reads as nobody.
|
||||
.get(
|
||||
'/api/images/v4/player/:playerId{[0-9]+}',
|
||||
describeRoute({
|
||||
tags: ['Images'],
|
||||
summary: 'A player’s photos',
|
||||
description:
|
||||
'The public images that player has taken, newest first. Serves the client’s ' +
|
||||
'The public camera photos (`SavedImageType` 1) that player has taken, newest first — ' +
|
||||
'not the outfit, room, invention or profile thumbnails filed under them. Serves the client’s ' +
|
||||
'`ImagesPlayer` projection (`SavedImageId`/`SavedImageType`, no `TaggedPlayerIds`) ' +
|
||||
'— the raw `SavedImage` renders blank thumbnails here.',
|
||||
'— the raw `SavedImage` renders blank thumbnails here.\n\n' +
|
||||
OWN_VIEW_NOTE,
|
||||
parameters: [idParam('playerId', 'Account id'), ...pageParams(100)],
|
||||
responses: { 200: json(ImagesPlayerDto.array(), 'The player’s photos') },
|
||||
}),
|
||||
@@ -389,7 +407,8 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
||||
const playerId = Number.parseInt(c.req.param('playerId'), 10)
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
const images = await getImagesByPlayer(c.env.DB, playerId, 0, skip, take)
|
||||
const viewer = await authedId(c)
|
||||
const images = await getImagesByPlayer(c.env.DB, playerId, 0, skip, take, viewer)
|
||||
return c.json(images.map(toImagesPlayer))
|
||||
}
|
||||
)
|
||||
@@ -401,7 +420,9 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
||||
describeRoute({
|
||||
tags: ['Images'],
|
||||
summary: 'A player’s photos, sortable',
|
||||
description: 'v4 plus a `sort` option. Same `ImagesPlayer` projection — see the note on v4.',
|
||||
description:
|
||||
'v4 plus a `sort` option. Same `ImagesPlayer` projection, and the same owner’s view — ' +
|
||||
'see the notes on v4.',
|
||||
parameters: [
|
||||
idParam('playerId', 'Account id'),
|
||||
intQuery('sort', '1 = most cheered; anything else = newest first'),
|
||||
@@ -414,7 +435,8 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
||||
const sort = Number.parseInt(c.req.query('sort') ?? '0', 10) || 0
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
const images = await getImagesByPlayer(c.env.DB, playerId, sort, skip, take)
|
||||
const viewer = await authedId(c)
|
||||
const images = await getImagesByPlayer(c.env.DB, playerId, sort, skip, take, viewer)
|
||||
return c.json(images.map(toImagesPlayer))
|
||||
}
|
||||
)
|
||||
@@ -428,8 +450,11 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
||||
tags: ['Images'],
|
||||
summary: 'A player’s photo feed',
|
||||
description:
|
||||
'The public images they took PLUS the ones they are tagged in, newest first — the ' +
|
||||
'photo tab on a profile. Same `ImagesPlayer` projection as the player photo lists.',
|
||||
'The public camera photos they took PLUS the ones they are tagged in, newest first — the ' +
|
||||
'photo tab on a profile. Same `ImagesPlayer` projection as the player photo lists.\n\n' +
|
||||
OWN_VIEW_NOTE +
|
||||
' A photo someone else took and tagged them in stays public-only either way: its ' +
|
||||
'privacy is the taker’s.',
|
||||
parameters: [idParam('playerId', 'Account id'), ...pageParams(100)],
|
||||
responses: { 200: json(ImagesPlayerDto.array(), 'The player’s feed') },
|
||||
}),
|
||||
@@ -437,7 +462,8 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
||||
const playerId = Number.parseInt(c.req.param('playerId'), 10)
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
|
||||
const images = await getPlayerFeed(c.env.DB, playerId, skip, take)
|
||||
const viewer = await authedId(c)
|
||||
const images = await getPlayerFeed(c.env.DB, playerId, skip, take, viewer)
|
||||
return c.json(images.map(toImagesPlayer))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6845,6 +6845,10 @@ describe('images', () => {
|
||||
}),
|
||||
// Unrelated to 700 → in neither.
|
||||
seed({ Id: 205, PlayerId: 999, TaggedPlayerIds: [111] }),
|
||||
// 700's outfit thumbnail: an image under their id, but not a photo → in neither,
|
||||
// not even in their own view (207 is private, as the client stores such things).
|
||||
seed({ Id: 200, PlayerId: 700, Type: 2, CreatedAt: '2026-06-01T00:00:00.000Z' }),
|
||||
seed({ Id: 207, PlayerId: 700, Type: 3, Accessibility: 0 }),
|
||||
])
|
||||
|
||||
// The lists serve the client's ImagesPlayer projection: the id and type are
|
||||
@@ -6896,6 +6900,44 @@ describe('images', () => {
|
||||
expect(
|
||||
await (await exports.default.fetch(`${ORIGIN}/api/images/v3/feed/player/424242`)).json()
|
||||
).toEqual([])
|
||||
|
||||
// The owner's view: 700 asking for their own list gets the private 203 too, on v4,
|
||||
// v5 and the feed alike. 203 has the seed's default CreatedAt (January), so it sorts
|
||||
// last — the private photo takes its place in the order, it isn't pinned anywhere.
|
||||
const own = await bearer('700')
|
||||
for (const path of ['v4/player/700', 'v5/player/700?sort=0']) {
|
||||
const list = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/${path}`, { headers: own })
|
||||
).json()) as Array<ImagesPlayer & { Accessibility: number }>
|
||||
expect(list.map((i) => i.SavedImageId)).toEqual([202, 201, 203])
|
||||
expect(list[2]?.Accessibility).toBe(0)
|
||||
}
|
||||
const ownFeed = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v3/feed/player/700`, { headers: own })
|
||||
).json()) as ImagesPlayer[]
|
||||
expect(ownFeed.map((i) => i.SavedImageId)).toEqual([204, 202, 201, 203])
|
||||
|
||||
// Being tagged in someone else's private photo does NOT surface it in the feed —
|
||||
// the taker decides its privacy, not the tag.
|
||||
await seed({ Id: 206, PlayerId: 999, TaggedPlayerIds: [700], Accessibility: 0 }).run()
|
||||
const ownFeedAgain = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v3/feed/player/700`, { headers: own })
|
||||
).json()) as ImagesPlayer[]
|
||||
expect(ownFeedAgain.map((i) => i.SavedImageId)).not.toContain(206)
|
||||
|
||||
// Anyone else's token — a friend, staff — is the public list, same as no token.
|
||||
const other = await bearer('999', ['developer', 'moderator'])
|
||||
const asOther = (await (
|
||||
await exports.default.fetch(`${ORIGIN}/api/images/v4/player/700`, { headers: other })
|
||||
).json()) as ImagesPlayer[]
|
||||
expect(asOther.map((i) => i.SavedImageId)).toEqual([202, 201])
|
||||
|
||||
// A garbage token isn't a 401 on a public list; it reads as nobody.
|
||||
const junk = await exports.default.fetch(`${ORIGIN}/api/images/v4/player/700`, {
|
||||
headers: { Authorization: 'Bearer not.a.jwt' },
|
||||
})
|
||||
expect(junk.status).toBe(200)
|
||||
expect(((await junk.json()) as ImagesPlayer[]).map((i) => i.SavedImageId)).toEqual([202, 201])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user