diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index d3d7db697..a1b717883 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -1,6 +1,7 @@ import { ActivityCreateDto, AlbumResponseDto, + AlbumUserRole, AssetFileUploadResponseDto, LoginResponseDto, ReactionType, @@ -33,7 +34,7 @@ describe('/activity', () => { createAlbumDto: { albumName: 'Album 1', assetIds: [asset.id], - sharedWithUserIds: [nonOwner.userId], + albumUsers: [{ userId: nonOwner.userId, role: AlbumUserRole.Editor }], }, }, { headers: asBearerAuth(admin.accessToken) }, diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index a3459bea3..f7d05aac5 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -49,72 +49,50 @@ describe('/album', () => { utils.createAsset(user1.accessToken), ]); - const albums = await Promise.all([ - // user 1 - /* 0 */ + user1Albums = await Promise.all([ utils.createAlbum(user1.accessToken, { albumName: user1SharedEditorUser, - sharedWithUserIds: [user2.userId], + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }], assetIds: [user1Asset1.id], }), - /* 1 */ utils.createAlbum(user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset1.id], }), - /* 2 */ utils.createAlbum(user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset1.id, user1Asset2.id], }), - - // user 2 - /* 3 */ - utils.createAlbum(user2.accessToken, { - albumName: user2SharedUser, - sharedWithUserIds: [user1.userId, user3.userId], - }), - /* 4 */ - utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), - /* 5 */ - utils.createAlbum(user2.accessToken, { albumName: user2NotShared }), - - // user 3 - /* 6 */ - utils.createAlbum(user3.accessToken, { - albumName: 'Deleted', - sharedWithUserIds: [user1.userId], - }), - - // user1 shared with an editor - /* 7 */ utils.createAlbum(user1.accessToken, { albumName: user1SharedViewerUser, - sharedWithUserIds: [user2.userId], + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }], assetIds: [user1Asset1.id], }), ]); - // Make viewer - await utils.updateAlbumUser(user1.accessToken, { - id: albums[7].id, - userId: user2.userId, - updateAlbumUserDto: { role: AlbumUserRole.Viewer }, + user2Albums = await Promise.all([ + utils.createAlbum(user2.accessToken, { + albumName: user2SharedUser, + albumUsers: [ + { userId: user1.userId, role: AlbumUserRole.Editor }, + { userId: user3.userId, role: AlbumUserRole.Editor }, + ], + }), + utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), + utils.createAlbum(user2.accessToken, { albumName: user2NotShared }), + ]); + + await utils.createAlbum(user3.accessToken, { + albumName: 'Deleted', + albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }], }); - albums[0].albumUsers[0].role = AlbumUserRole.Editor; - albums[3].albumUsers[0].role = AlbumUserRole.Editor; - albums[6].albumUsers[0].role = AlbumUserRole.Editor; - await addAssetsToAlbum( - { id: albums[3].id, bulkIdsDto: { ids: [user1Asset1.id] } }, + { id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id] } }, { headers: asBearerAuth(user1.accessToken) }, ); - albums[3] = await getAlbumInfo({ id: albums[3].id }, { headers: asBearerAuth(user2.accessToken) }); - - user1Albums = [...albums.slice(0, 3), albums[7]]; - user2Albums = albums.slice(3, 6); + user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }); await Promise.all([ // add shared link to user1SharedLink album @@ -641,9 +619,11 @@ describe('/album', () => { it('should allow the album owner to change the role of a shared user', async () => { const album = await utils.createAlbum(user1.accessToken, { albumName: 'testAlbum', - sharedWithUserIds: [user2.userId], + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }], }); + expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer); + const { status } = await request(app) .put(`/album/${album.id}/user/${user2.userId}`) .set('Authorization', `Bearer ${user1.accessToken}`) @@ -663,9 +643,11 @@ describe('/album', () => { it('should not allow a shared user to change the role of another shared user', async () => { const album = await utils.createAlbum(user1.accessToken, { albumName: 'testAlbum', - sharedWithUserIds: [user2.userId], + albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }], }); + expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer); + const { status, body } = await request(app) .put(`/album/${album.id}/user/${user2.userId}`) .set('Authorization', `Bearer ${user2.accessToken}`) diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 65cf5428f..570132ada 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -18,6 +18,7 @@ doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md doc/AlbumUserAddDto.md +doc/AlbumUserCreateDto.md doc/AlbumUserResponseDto.md doc/AlbumUserRole.md doc/AllJobStatusResponseDto.md @@ -257,6 +258,7 @@ lib/model/admin_onboarding_update_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart lib/model/album_user_add_dto.dart +lib/model/album_user_create_dto.dart lib/model/album_user_response_dto.dart lib/model/album_user_role.dart lib/model/all_job_status_response_dto.dart @@ -444,6 +446,7 @@ test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart test/album_user_add_dto_test.dart +test/album_user_create_dto_test.dart test/album_user_response_dto_test.dart test/album_user_role_test.dart test/all_job_status_response_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 65ecb51b9..c98745430 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AlbumUserCreateDto.md b/mobile/openapi/doc/AlbumUserCreateDto.md new file mode 100644 index 000000000..78acbc476 Binary files /dev/null and b/mobile/openapi/doc/AlbumUserCreateDto.md differ diff --git a/mobile/openapi/doc/CreateAlbumDto.md b/mobile/openapi/doc/CreateAlbumDto.md index 0a472725e..34035d4af 100644 Binary files a/mobile/openapi/doc/CreateAlbumDto.md and b/mobile/openapi/doc/CreateAlbumDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 555bad2ee..02da5876d 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index f1105f6f5..3b21ff6e0 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart new file mode 100644 index 000000000..4c8f2ec6d Binary files /dev/null and b/mobile/openapi/lib/model/album_user_create_dto.dart differ diff --git a/mobile/openapi/lib/model/create_album_dto.dart b/mobile/openapi/lib/model/create_album_dto.dart index 7b527eb7b..7af3526a4 100644 Binary files a/mobile/openapi/lib/model/create_album_dto.dart and b/mobile/openapi/lib/model/create_album_dto.dart differ diff --git a/mobile/openapi/test/album_user_create_dto_test.dart b/mobile/openapi/test/album_user_create_dto_test.dart new file mode 100644 index 000000000..a1459172f Binary files /dev/null and b/mobile/openapi/test/album_user_create_dto_test.dart differ diff --git a/mobile/openapi/test/create_album_dto_test.dart b/mobile/openapi/test/create_album_dto_test.dart index d23e66cf7..f3dc3c864 100644 Binary files a/mobile/openapi/test/create_album_dto_test.dart and b/mobile/openapi/test/create_album_dto_test.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index ae6a3dfb0..993705f97 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6779,6 +6779,22 @@ ], "type": "object" }, + "AlbumUserCreateDto": { + "properties": { + "role": { + "$ref": "#/components/schemas/AlbumUserRole" + }, + "userId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "role", + "userId" + ], + "type": "object" + }, "AlbumUserResponseDto": { "properties": { "role": { @@ -7599,6 +7615,13 @@ "albumName": { "type": "string" }, + "albumUsers": { + "description": "This property was added in v1.104.0", + "items": { + "$ref": "#/components/schemas/AlbumUserCreateDto" + }, + "type": "array" + }, "assetIds": { "items": { "format": "uuid", @@ -7610,6 +7633,8 @@ "type": "string" }, "sharedWithUserIds": { + "deprecated": true, + "description": "This property was deprecated in v1.104.0", "items": { "format": "uuid", "type": "string" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 903d8e048..23b3b00be 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -169,10 +169,17 @@ export type AlbumResponseDto = { startDate?: string; updatedAt: string; }; +export type AlbumUserCreateDto = { + role: AlbumUserRole; + userId: string; +}; export type CreateAlbumDto = { albumName: string; + /** This property was added in v1.104.0 */ + albumUsers?: AlbumUserCreateDto[]; assetIds?: string[]; description?: string; + /** This property was deprecated in v1.104.0 */ sharedWithUserIds?: string[]; }; export type AlbumCountResponseDto = { diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index fb4aff948..c588847c6 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator'; import _ from 'lodash'; import { PropertyLifecycle } from 'src/decorators'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; @@ -32,6 +33,14 @@ export class AddUsersDto { albumUsers!: AlbumUserAddDto[]; } +class AlbumUserCreateDto { + @ValidateUUID() + userId!: string; + + @IsEnum(AlbumUserRole) + role!: AlbumUserRole; +} + export class CreateAlbumDto { @IsString() @ApiProperty() @@ -41,7 +50,15 @@ export class CreateAlbumDto { @Optional() description?: string; + @Optional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AlbumUserCreateDto) + @PropertyLifecycle({ addedAt: 'v1.104.0' }) + albumUsers?: AlbumUserCreateDto[]; + @ValidateUUID({ optional: true, each: true }) + @PropertyLifecycle({ deprecatedAt: 'v1.104.0' }) sharedWithUserIds?: string[]; @ValidateUUID({ optional: true, each: true }) diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 06891ef4d..e3183c36a 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -281,11 +281,11 @@ export class AlbumRepository implements IAlbumRepository { .execute(); } - async create(album: Partial): Promise { + create(album: Partial): Promise { return this.save(album); } - async update(album: Partial): Promise { + update(album: Partial): Promise { return this.save(album); } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 3a050cd59..e2a7fc49c 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -194,7 +194,7 @@ describe(AlbumService.name, () => { ownerId: authStub.admin.user.id, albumName: albumStub.empty.albumName, description: albumStub.empty.description, - albumUsers: [{ user: { id: 'user-id' } }], + albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], assets: [{ id: '123' }], albumThumbnailAssetId: '123', }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 1cc049d85..38464bd75 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -114,7 +114,12 @@ export class AlbumService { } async create(auth: AuthDto, dto: CreateAlbumDto): Promise { + const albumUsers = dto.albumUsers || []; for (const userId of dto.sharedWithUserIds || []) { + albumUsers.push({ userId, role: AlbumUserRole.EDITOR }); + } + + for (const { userId } of albumUsers) { const exists = await this.userRepository.get(userId, {}); if (!exists) { throw new BadRequestException('User not found'); @@ -128,7 +133,7 @@ export class AlbumService { ownerId: auth.user.id, albumName: dto.albumName, description: dto.description, - albumUsers: dto.sharedWithUserIds?.map((userId) => ({ user: { id: userId } }) as AlbumUserEntity) ?? [], + albumUsers: albumUsers.map((albumUser) => albumUser as AlbumUserEntity) ?? [], assets, albumThumbnailAssetId: assets[0]?.id || null, });