From f12a940dfb7d58a93fdb47c9eb360ee72b5e3377 Mon Sep 17 00:00:00 2001 From: timonrieger Date: Fri, 12 Jun 2026 16:41:27 +0200 Subject: [PATCH] fix(server): hide partner archived asset locations from map markers --- e2e/src/specs/server/api/map.e2e-spec.ts | 53 ++++++++++++++++++++--- server/src/repositories/map.repository.ts | 5 ++- server/src/services/map.service.spec.ts | 1 + server/src/services/map.service.ts | 2 +- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/e2e/src/specs/server/api/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts index 86664b2dc4..4050ca042a 100644 --- a/e2e/src/specs/server/api/map.e2e-spec.ts +++ b/e2e/src/specs/server/api/map.e2e-spec.ts @@ -2,6 +2,7 @@ import { AssetVisibility, LoginResponseDto } from '@immich/sdk'; import { readFile } from 'node:fs/promises'; import { basename, join } from 'node:path'; import { Socket } from 'socket.io-client'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; @@ -9,28 +10,48 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; describe('/map', () => { let websocket: Socket; + let partnerWebsocket: Socket; let admin: LoginResponseDto; + let partner: LoginResponseDto; + let partnerArchivedAssetId: string; + let adminArchivedAssetId: string; beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup({ onboarding: false }); + partner = await utils.userSetup(admin.accessToken, createUserDto.user1); websocket = await utils.connectWebsocket(admin.accessToken); + partnerWebsocket = await utils.connectWebsocket(partner.accessToken); - const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg']; + const adminFiles = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg']; + const adminArchivedFile = 'metadata/dates/datetimeoriginal-gps.jpg'; + const partnerFile = 'metadata/gps-position/thompson-springs.jpg'; utils.resetEvents(); - const uploadFile = async (input: string) => { + const uploadFile = async (accessToken: string, input: string) => { const filepath = join(testAssetDir, input); - const { id } = await utils.createAsset(admin.accessToken, { + const { id } = await utils.createAsset(accessToken, { assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, }); await utils.waitForWebsocketEvent({ event: 'assetUpload', id }); + return id; }; - await Promise.all(files.map((f) => uploadFile(f))); + [, , adminArchivedAssetId, partnerArchivedAssetId] = await Promise.all([ + ...adminFiles.map((f) => uploadFile(admin.accessToken, f)), + uploadFile(admin.accessToken, adminArchivedFile), + uploadFile(partner.accessToken, partnerFile), + ]); + + await Promise.all([ + utils.archiveAssets(admin.accessToken, [adminArchivedAssetId]), + utils.archiveAssets(partner.accessToken, [partnerArchivedAssetId]), + utils.createPartner(partner.accessToken, admin.userId), + ]); }); afterAll(() => { utils.disconnectWebsocket(websocket); + utils.disconnectWebsocket(partnerWebsocket); }); describe('GET /map/markers', () => { @@ -40,7 +61,6 @@ describe('/map', () => { expect(body).toEqual(errorDto.unauthorized); }); - // TODO archive one of these assets it('should get map markers for all non-archived assets', async () => { const { status, body } = await request(app) .get('/map/markers') @@ -69,7 +89,28 @@ describe('/map', () => { ]); }); - // TODO archive one of these assets + it('should not expose partner archived asset locations', async () => { + const { status, body } = await request(app) + .get('/map/markers') + .query({ withPartners: true, isArchived: true }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + const ids = body.map((m: { id: string }) => m.id); + expect(ids).not.toContain(partnerArchivedAssetId); + expect(ids).toContain(adminArchivedAssetId); + }); + + it('should include own archived asset locations', async () => { + const { status, body } = await request(app) + .get('/map/markers') + .query({ isArchived: true }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body.map((m: { id: string }) => m.id)).toContain(adminArchivedAssetId); + }); + it('should get all map markers', async () => { const { status, body } = await request(app) .get('/map/markers') diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 909a35c1e1..87cf30b618 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -78,8 +78,9 @@ export class MapRepository { .execute(); } - @GenerateSql({ params: [[DummyValue.UUID], [DummyValue.UUID]] }) + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID], [DummyValue.UUID]] }) getMapMarkers( + authUserId: string, ownerIds: string[], albumIds: string[], { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore }: MapMarkerSearchOptions = {}, @@ -89,7 +90,7 @@ export class MapRepository { qb.where((eb) => eb.or([ eb('asset.visibility', '=', AssetVisibility.Timeline), - eb('asset.visibility', '=', AssetVisibility.Archive), + eb.and([eb('asset.ownerId', '=', authUserId), eb('asset.visibility', '=', AssetVisibility.Archive)]), ]), ), ) diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index 6ef94cd40c..d90726649c 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -59,6 +59,7 @@ describe(MapService.name, () => { const markers = await sut.getMapMarkers(auth, { withPartners: true }); expect(mocks.map.getMapMarkers).toHaveBeenCalledWith( + auth.user.id, [auth.user.id, partner.sharedById], expect.arrayContaining([]), { withPartners: true }, diff --git a/server/src/services/map.service.ts b/server/src/services/map.service.ts index 3a825697b4..594ef99c70 100644 --- a/server/src/services/map.service.ts +++ b/server/src/services/map.service.ts @@ -15,7 +15,7 @@ export class MapService extends BaseService { const albumIds = options.withSharedAlbums ? await this.albumRepository.getAllIds(auth.user.id) : []; - return this.mapRepository.getMapMarkers(userIds, albumIds, options); + return this.mapRepository.getMapMarkers(auth.user.id, userIds, albumIds, options); } async reverseGeocode(dto: MapReverseGeocodeDto) {