diff --git a/apps/api/src/routes/images.ts b/apps/api/src/routes/images.ts index 80e2695..7339502 100644 --- a/apps/api/src/routes/images.ts +++ b/apps/api/src/routes/images.ts @@ -468,10 +468,11 @@ export const imageRoutes = new Hono({ strict: false }) } ) - // Global slideshow feed — the most recent publicly-listable ShareCamera photos - // (Accessibility 0 or 1, Type 1) across all rooms, newest first, each joined to its - // creator's username and room name. Public (no auth): it only surfaces already-public - // images and backs the anonymous homepage slideshow. Returns `{ Images, ValidTill }`, + // Global slideshow feed — the most recent public ShareCamera photos (Accessibility 1, + // Type 1) across all rooms, newest first, each joined to its creator's username and + // room name. Public (no auth): it only surfaces already-public images and backs the + // anonymous homepage slideshow. Private photos (Accessibility 0) never appear — the + // feed is anonymous, so there is no viewer it could be unlocked for. Returns `{ Images, ValidTill }`, // where ValidTill is a short (2-minute) cache hint the client refreshes against. // Serves 10 by default and never more than SLIDESHOW_MAX_LIMIT (100): it's public and // unauthenticated, so an unclamped `take` would let anyone ask for the whole image diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index 477e56c..8dec08e 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -6550,6 +6550,37 @@ describe('images', () => { }) }) + // The feed is anonymous, so a private photo (Accessibility 0) must never reach it — + // there is no viewer it could be unlocked for. It once accepted 0 alongside 1. + test('GET /api/images/v1/slideshow leaves private photos out', async () => { + const pub = await createImage(env.DB, { + imageName: 'slidepublic.jpg', + playerId: 42, + accessibility: 1, + }) + const priv = await createImage(env.DB, { + imageName: 'slideprivate.jpg', + playerId: 42, + accessibility: 0, + }) + // A non-photo (room thumbnail) that happens to be public stays out too. + const thumb = await createImage(env.DB, { + imageName: 'slidethumb.jpg', + playerId: 42, + type: 3, + accessibility: 1, + }) + + const res = await exports.default.fetch(`${ORIGIN}/api/images/v1/slideshow?take=100`) + expect(res.status).toBe(200) + const ids = ((await res.json()) as { Images: Array<{ SavedImageId: number }> }).Images.map( + (i) => i.SavedImageId + ) + expect(ids).toContain(pub.Id) + expect(ids).not.toContain(priv.Id) + expect(ids).not.toContain(thumb.Id) + }) + // The feed is public and unauthenticated, so `take` is clamped rather than trusted: // without the cap a single anonymous request could pull the whole image table through // the two joins behind it. diff --git a/packages/domain/src/images-db.ts b/packages/domain/src/images-db.ts index 4829395..b3d1e69 100644 --- a/packages/domain/src/images-db.ts +++ b/packages/domain/src/images-db.ts @@ -518,9 +518,11 @@ async function getRoomNames(db: D1Database, ids: number[]): Promise