mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix(server): only asset owner should see favorite status (#20654)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Docker / pre-job (push) Waiting to run
Docker / Re-Tag ML (push) Blocked by required conditions
Docker / Re-Tag Server (push) Blocked by required conditions
Docker / Build and Push ML (push) Blocked by required conditions
Docker / Build and Push Server (push) Blocked by required conditions
Docker / Docker Build & Push Server Success (push) Blocked by required conditions
Docker / Docker Build & Push ML Success (push) Blocked by required conditions
Docs build / pre-job (push) Waiting to run
Docs build / Docs Build (push) Blocked by required conditions
Zizmor / Zizmor (push) Waiting to run
Static Code Analysis / pre-job (push) Waiting to run
Static Code Analysis / Run Dart Code Analysis (push) Blocked by required conditions
Test / pre-job (push) Waiting to run
Test / Test & Lint Server (push) Blocked by required conditions
Test / Unit Test CLI (push) Blocked by required conditions
Test / Unit Test CLI (Windows) (push) Blocked by required conditions
Test / Lint Web (push) Blocked by required conditions
Test / Test Web (push) Blocked by required conditions
Test / Test i18n (push) Blocked by required conditions
Test / End-to-End Lint (push) Blocked by required conditions
Test / Medium Tests (Server) (push) Blocked by required conditions
Test / End-to-End Tests (Server & CLI) (push) Blocked by required conditions
Test / End-to-End Tests (Web) (push) Blocked by required conditions
Test / End-to-End Tests Success (push) Blocked by required conditions
Test / Unit Test Mobile (push) Blocked by required conditions
Test / Unit Test ML (push) Blocked by required conditions
Test / .github Files Formatting (push) Blocked by required conditions
Test / ShellCheck (push) Waiting to run
Test / OpenAPI Clients (push) Waiting to run
Test / SQL Schema Checks (push) Waiting to run
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Docker / pre-job (push) Waiting to run
Docker / Re-Tag ML (push) Blocked by required conditions
Docker / Re-Tag Server (push) Blocked by required conditions
Docker / Build and Push ML (push) Blocked by required conditions
Docker / Build and Push Server (push) Blocked by required conditions
Docker / Docker Build & Push Server Success (push) Blocked by required conditions
Docker / Docker Build & Push ML Success (push) Blocked by required conditions
Docs build / pre-job (push) Waiting to run
Docs build / Docs Build (push) Blocked by required conditions
Zizmor / Zizmor (push) Waiting to run
Static Code Analysis / pre-job (push) Waiting to run
Static Code Analysis / Run Dart Code Analysis (push) Blocked by required conditions
Test / pre-job (push) Waiting to run
Test / Test & Lint Server (push) Blocked by required conditions
Test / Unit Test CLI (push) Blocked by required conditions
Test / Unit Test CLI (Windows) (push) Blocked by required conditions
Test / Lint Web (push) Blocked by required conditions
Test / Test Web (push) Blocked by required conditions
Test / Test i18n (push) Blocked by required conditions
Test / End-to-End Lint (push) Blocked by required conditions
Test / Medium Tests (Server) (push) Blocked by required conditions
Test / End-to-End Tests (Server & CLI) (push) Blocked by required conditions
Test / End-to-End Tests (Web) (push) Blocked by required conditions
Test / End-to-End Tests Success (push) Blocked by required conditions
Test / Unit Test Mobile (push) Blocked by required conditions
Test / Unit Test ML (push) Blocked by required conditions
Test / .github Files Formatting (push) Blocked by required conditions
Test / ShellCheck (push) Waiting to run
Test / OpenAPI Clients (push) Waiting to run
Test / SQL Schema Checks (push) Waiting to run
* fix: Any asset update disables isFavorite action in GUI. Only owner of asset in album should see favorited image. * Fix unit tests * Fix formatting * better query, add medium test * update sql --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
parent
24bfdf3263
commit
505e16c37c
7 changed files with 95 additions and 25 deletions
|
|
@ -212,7 +212,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
||||||
fileModifiedAt: entity.fileModifiedAt,
|
fileModifiedAt: entity.fileModifiedAt,
|
||||||
localDateTime: entity.localDateTime,
|
localDateTime: entity.localDateTime,
|
||||||
updatedAt: entity.updatedAt,
|
updatedAt: entity.updatedAt,
|
||||||
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
|
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
|
||||||
isArchived: entity.visibility === AssetVisibility.Archive,
|
isArchived: entity.visibility === AssetVisibility.Archive,
|
||||||
isTrashed: !!entity.deletedAt,
|
isTrashed: !!entity.deletedAt,
|
||||||
visibility: entity.visibility,
|
visibility: entity.visibility,
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,8 @@ with
|
||||||
"asset"."duration",
|
"asset"."duration",
|
||||||
"asset"."id",
|
"asset"."id",
|
||||||
"asset"."visibility",
|
"asset"."visibility",
|
||||||
"asset"."isFavorite",
|
asset."isFavorite"
|
||||||
|
and asset."ownerId" = $1 as "isFavorite",
|
||||||
asset.type = 'IMAGE' as "isImage",
|
asset.type = 'IMAGE' as "isImage",
|
||||||
asset."deletedAt" is not null as "isTrashed",
|
asset."deletedAt" is not null as "isTrashed",
|
||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
|
|
@ -341,14 +342,14 @@ with
|
||||||
where
|
where
|
||||||
"stacked"."stackId" = "asset"."stackId"
|
"stacked"."stackId" = "asset"."stackId"
|
||||||
and "stacked"."deletedAt" is null
|
and "stacked"."deletedAt" is null
|
||||||
and "stacked"."visibility" = $1
|
and "stacked"."visibility" = $2
|
||||||
group by
|
group by
|
||||||
"stacked"."stackId"
|
"stacked"."stackId"
|
||||||
) as "stacked_assets" on true
|
) as "stacked_assets" on true
|
||||||
where
|
where
|
||||||
"asset"."deletedAt" is null
|
"asset"."deletedAt" is null
|
||||||
and "asset"."visibility" in ('archive', 'timeline')
|
and "asset"."visibility" in ('archive', 'timeline')
|
||||||
and date_trunc('MONTH', "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' = $2
|
and date_trunc('MONTH', "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' = $3
|
||||||
and not exists (
|
and not exists (
|
||||||
select
|
select
|
||||||
from
|
from
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { Stack } from 'src/database';
|
import { Stack } from 'src/database';
|
||||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
|
|
@ -589,9 +590,9 @@ export class AssetRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
params: [DummyValue.TIME_BUCKET, { withStacked: true }],
|
params: [DummyValue.TIME_BUCKET, { withStacked: true }, { user: { id: DummyValue.UUID } }],
|
||||||
})
|
})
|
||||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions) {
|
getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth: AuthDto) {
|
||||||
const query = this.db
|
const query = this.db
|
||||||
.with('cte', (qb) =>
|
.with('cte', (qb) =>
|
||||||
qb
|
qb
|
||||||
|
|
@ -601,7 +602,7 @@ export class AssetRepository {
|
||||||
'asset.duration',
|
'asset.duration',
|
||||||
'asset.id',
|
'asset.id',
|
||||||
'asset.visibility',
|
'asset.visibility',
|
||||||
'asset.isFavorite',
|
sql`asset."isFavorite" and asset."ownerId" = ${auth.user.id}`.as('isFavorite'),
|
||||||
sql`asset.type = 'IMAGE'`.as('isImage'),
|
sql`asset.type = 'IMAGE'`.as('isImage'),
|
||||||
sql`asset."deletedAt" is not null`.as('isTrashed'),
|
sql`asset."deletedAt" is not null`.as('isTrashed'),
|
||||||
'asset.livePhotoVideoId',
|
'asset.livePhotoVideoId',
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,11 @@ export class NotificationService extends BaseService {
|
||||||
|
|
||||||
const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId]);
|
const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId]);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
this.eventRepository.clientSend('on_asset_update', userId, mapAsset(asset));
|
this.eventRepository.clientSend(
|
||||||
|
'on_asset_update',
|
||||||
|
userId,
|
||||||
|
mapAsset(asset, { auth: { user: { id: userId } } as AuthDto }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,14 @@ describe(TimelineService.name, () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||||
timeBucket: 'bucket',
|
'bucket',
|
||||||
albumId: 'album-id',
|
{
|
||||||
});
|
timeBucket: 'bucket',
|
||||||
|
albumId: 'album-id',
|
||||||
|
},
|
||||||
|
authStub.admin,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
||||||
|
|
@ -60,6 +64,7 @@ describe(TimelineService.name, () => {
|
||||||
visibility: AssetVisibility.Archive,
|
visibility: AssetVisibility.Archive,
|
||||||
userIds: [authStub.admin.user.id],
|
userIds: [authStub.admin.user.id],
|
||||||
}),
|
}),
|
||||||
|
authStub.admin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -76,12 +81,16 @@ describe(TimelineService.name, () => {
|
||||||
withPartners: true,
|
withPartners: true,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(json);
|
).resolves.toEqual(json);
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||||
timeBucket: 'bucket',
|
'bucket',
|
||||||
visibility: AssetVisibility.Timeline,
|
{
|
||||||
withPartners: true,
|
timeBucket: 'bucket',
|
||||||
userIds: [authStub.admin.user.id],
|
visibility: AssetVisibility.Timeline,
|
||||||
});
|
withPartners: true,
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
},
|
||||||
|
authStub.admin,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should check permissions to read tag', async () => {
|
it('should check permissions to read tag', async () => {
|
||||||
|
|
@ -96,11 +105,15 @@ describe(TimelineService.name, () => {
|
||||||
tagId: 'tag-123',
|
tagId: 'tag-123',
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(json);
|
).resolves.toEqual(json);
|
||||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||||
tagId: 'tag-123',
|
'bucket',
|
||||||
timeBucket: 'bucket',
|
{
|
||||||
userIds: [authStub.admin.user.id],
|
tagId: 'tag-123',
|
||||||
});
|
timeBucket: 'bucket',
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
},
|
||||||
|
authStub.admin,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the assets for a library time bucket if user has library.read', async () => {
|
it('should return the assets for a library time bucket if user has library.read', async () => {
|
||||||
|
|
@ -119,6 +132,7 @@ describe(TimelineService.name, () => {
|
||||||
timeBucket: 'bucket',
|
timeBucket: 'bucket',
|
||||||
userIds: [authStub.admin.user.id],
|
userIds: [authStub.admin.user.id],
|
||||||
}),
|
}),
|
||||||
|
authStub.admin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export class TimelineService extends BaseService {
|
||||||
const timeBucketOptions = await this.buildTimeBucketOptions(auth, { ...dto });
|
const timeBucketOptions = await this.buildTimeBucketOptions(auth, { ...dto });
|
||||||
|
|
||||||
// TODO: use id cursor for pagination
|
// TODO: use id cursor for pagination
|
||||||
const bucket = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
const bucket = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions, auth);
|
||||||
return bucket.assets;
|
return bucket.assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { AssetVisibility } from 'src/enum';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { TimelineService } from 'src/services/timeline.service';
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
import { newMediumService } from 'test/medium.factory';
|
import { newMediumService } from 'test/medium.factory';
|
||||||
|
|
@ -15,7 +16,7 @@ let defaultDatabase: Kysely<DB>;
|
||||||
const setup = (db?: Kysely<DB>) => {
|
const setup = (db?: Kysely<DB>) => {
|
||||||
return newMediumService(TimelineService, {
|
return newMediumService(TimelineService, {
|
||||||
database: db || defaultDatabase,
|
database: db || defaultDatabase,
|
||||||
real: [AssetRepository, AccessRepository],
|
real: [AssetRepository, AccessRepository, PartnerRepository],
|
||||||
mock: [LoggingRepository],
|
mock: [LoggingRepository],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -155,5 +156,54 @@ describe(TimelineService.name, () => {
|
||||||
const response = JSON.parse(rawResponse);
|
const response = JSON.parse(rawResponse);
|
||||||
expect(response).toEqual(expect.objectContaining({ isTrashed: [true] }));
|
expect(response).toEqual(expect.objectContaining({ isTrashed: [true] }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false for favorite status unless asset owner', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([
|
||||||
|
ctx.newUser().then(async ({ user }) => {
|
||||||
|
const result = await ctx.newAsset({
|
||||||
|
ownerId: user.id,
|
||||||
|
fileCreatedAt: new Date('1970-02-12'),
|
||||||
|
localDateTime: new Date('1970-02-12'),
|
||||||
|
isFavorite: true,
|
||||||
|
});
|
||||||
|
await ctx.newExif({ assetId: result.asset.id, make: 'Canon' });
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
ctx.newUser().then(async ({ user }) => {
|
||||||
|
const result = await ctx.newAsset({
|
||||||
|
ownerId: user.id,
|
||||||
|
fileCreatedAt: new Date('1970-02-13'),
|
||||||
|
localDateTime: new Date('1970-02-13'),
|
||||||
|
isFavorite: true,
|
||||||
|
});
|
||||||
|
await ctx.newExif({ assetId: result.asset.id, make: 'Canon' });
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
ctx.newPartner({ sharedById: asset1.ownerId, sharedWithId: asset2.ownerId }),
|
||||||
|
ctx.newPartner({ sharedById: asset2.ownerId, sharedWithId: asset1.ownerId }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const auth1 = factory.auth({ user: { id: asset1.ownerId } });
|
||||||
|
const rawResponse1 = await sut.getTimeBucket(auth1, {
|
||||||
|
timeBucket: '1970-02-01',
|
||||||
|
withPartners: true,
|
||||||
|
visibility: AssetVisibility.Timeline,
|
||||||
|
});
|
||||||
|
const response1 = JSON.parse(rawResponse1);
|
||||||
|
expect(response1).toEqual(expect.objectContaining({ id: [asset2.id, asset1.id], isFavorite: [false, true] }));
|
||||||
|
|
||||||
|
const auth2 = factory.auth({ user: { id: asset2.ownerId } });
|
||||||
|
const rawResponse2 = await sut.getTimeBucket(auth2, {
|
||||||
|
timeBucket: '1970-02-01',
|
||||||
|
withPartners: true,
|
||||||
|
visibility: AssetVisibility.Timeline,
|
||||||
|
});
|
||||||
|
const response2 = JSON.parse(rawResponse2);
|
||||||
|
expect(response2).toEqual(expect.objectContaining({ id: [asset2.id, asset1.id], isFavorite: [true, false] }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue