From da70388f4b932c702726b13b2c74ba806a921ef0 Mon Sep 17 00:00:00 2001 From: jubnl Date: Thu, 16 Apr 2026 15:37:24 +0200 Subject: [PATCH] fix(journey): resolve Immich photos on public share by matching trek_photos.id validateShareTokenForPhoto was querying journey_photos by jp.id but the public page sends p.photo_id (trek_photos.id) in the URL. In a fresh database the IDs coincidentally match, masking the bug. In production instances with many Immich-synced photos the trek_photos autoincrement is far ahead of journey_photos, causing a 404 for every Immich photo on the public share page. Fix: change the lookup to jp.photo_id = ? so validation is keyed on trek_photos.id, which is what the client sends and what streamPhoto needs. Updated the test helper to return trekId and added a regression test that pre-populates trek_photos to produce diverging IDs. Closes #675. --- server/src/services/journeyShareService.ts | 2 +- .../unit/services/journeyShareService.test.ts | 33 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/server/src/services/journeyShareService.ts b/server/src/services/journeyShareService.ts index 46ef6926..c85621de 100644 --- a/server/src/services/journeyShareService.ts +++ b/server/src/services/journeyShareService.ts @@ -63,7 +63,7 @@ export function validateShareTokenForPhoto(token: string, photoId: number): { jo FROM journey_photos jp JOIN trek_photos tkp ON tkp.id = jp.photo_id JOIN journey_entries je ON jp.entry_id = je.id - WHERE jp.id = ? AND je.journey_id = ? + WHERE jp.photo_id = ? AND je.journey_id = ? `).get(photoId, row.journey_id) as any; if (!photo) return null; const journey = db.prepare('SELECT user_id FROM journeys WHERE id = ?').get(row.journey_id) as any; diff --git a/server/tests/unit/services/journeyShareService.test.ts b/server/tests/unit/services/journeyShareService.test.ts index 371e170e..8027e068 100644 --- a/server/tests/unit/services/journeyShareService.test.ts +++ b/server/tests/unit/services/journeyShareService.test.ts @@ -58,7 +58,7 @@ afterAll(() => { // -- Helpers ------------------------------------------------------------------ -/** Insert a journey_photos row and return its id. */ +/** Insert a trek_photos + journey_photos row and return the trek_photos id (used as photoId in public URLs). */ function insertJourneyPhoto( entryId: number, opts: { filePath?: string; assetId?: string; ownerId?: number } = {} @@ -70,11 +70,13 @@ function insertJourneyPhoto( VALUES (?, ?, ?, ?, ?) `).run(provider, opts.assetId ?? null, filePath, opts.ownerId ?? null, Date.now()); const trekId = trekResult.lastInsertRowid as number; - const result = testDb.prepare(` + testDb.prepare(` INSERT INTO journey_photos (entry_id, photo_id, caption, sort_order, created_at) VALUES (?, ?, NULL, 0, ?) `).run(entryId, trekId, Date.now()); - return result.lastInsertRowid as number; + // Return trek_photos.id — this is p.photo_id in the public API response + // and the value the client sends to /api/public/journey/:token/photos/:photoId/:kind + return trekId; } // -- Tests -------------------------------------------------------------------- @@ -237,6 +239,31 @@ describe('validateShareTokenForPhoto', () => { expect(result).not.toBeNull(); expect(result!.ownerId).toBe(user.id); }); + + it('JOURNEY-SHARE-016: resolves correctly when trek_photos.id differs from journey_photos.id (Immich bulk-sync scenario)', () => { + // Simulate a user who has many trek_photos from Immich syncs before adding a journey photo. + // trek_photos.id will be higher than journey_photos.id — the previous bug matched on jp.id + // instead of jp.photo_id, causing a 404 for Immich photos in public shares. + const { user } = createUser(testDb); + const journey = createJourney(testDb, user.id); + const entry = createJourneyEntry(testDb, journey.id, user.id); + + // Pre-populate trek_photos to push the autoincrement higher + for (let i = 0; i < 5; i++) { + testDb.prepare(`INSERT INTO trek_photos (provider, asset_id, owner_id, created_at) VALUES ('immich', ?, ?, ?)`).run(`bulk-asset-${i}`, user.id, Date.now()); + } + + // This trek_photos row gets a high id (e.g. 6) while journey_photos id will be 1 + const trekPhotoId = insertJourneyPhoto(entry.id, { assetId: 'journey-asset-xyz', ownerId: user.id }); + const { token } = createOrUpdateJourneyShareLink(journey.id, user.id, {}); + + // photoId = trek_photos.id (6), not journey_photos.id (1) + const result = validateShareTokenForPhoto(token, trekPhotoId); + + expect(result).not.toBeNull(); + expect(result!.ownerId).toBe(user.id); + expect(result!.journeyId).toBe(journey.id); + }); }); describe('validateShareTokenForAsset', () => {