diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index fd25a026ec..cc27eba627 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -17,6 +17,7 @@ doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md doc/AlbumUserResponseDto.md +doc/AlbumUserRole.md doc/AllJobStatusResponseDto.md doc/AssetApi.md doc/AssetBulkDeleteDto.md @@ -240,6 +241,7 @@ lib/model/add_users_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart lib/model/album_user_response_dto.dart +lib/model/album_user_role.dart lib/model/all_job_status_response_dto.dart lib/model/api_key_create_dto.dart lib/model/api_key_create_response_dto.dart @@ -419,6 +421,7 @@ test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart test/album_user_response_dto_test.dart +test/album_user_role_test.dart test/all_job_status_response_dto_test.dart test/api_key_api_test.dart test/api_key_create_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 0c130c6b2f..417093a89f 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -235,6 +235,7 @@ Class | Method | HTTP request | Description - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) + - [AlbumUserRole](doc//AlbumUserRole.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) diff --git a/mobile/openapi/doc/AlbumResponseDto.md b/mobile/openapi/doc/AlbumResponseDto.md index 3d94c0e65f..ec9a0a0a77 100644 --- a/mobile/openapi/doc/AlbumResponseDto.md +++ b/mobile/openapi/doc/AlbumResponseDto.md @@ -23,7 +23,7 @@ Name | Type | Description | Notes **owner** | [**UserResponseDto**](UserResponseDto.md) | | **ownerId** | **String** | | **shared** | **bool** | | -**sharedUsers** | [**List**](UserResponseDto.md) | Deprecated in favor of users | [default to const []] +**sharedUsers** | [**List**](UserResponseDto.md) | Deprecated in favor of sharedUsersV2 | [default to const []] **sharedUsersV2** | [**List**](AlbumUserResponseDto.md) | | [default to const []] **startDate** | [**DateTime**](DateTime.md) | | [optional] **updatedAt** | [**DateTime**](DateTime.md) | | diff --git a/mobile/openapi/doc/AlbumUserResponseDto.md b/mobile/openapi/doc/AlbumUserResponseDto.md index 415128aa4f..3f59d3142f 100644 --- a/mobile/openapi/doc/AlbumUserResponseDto.md +++ b/mobile/openapi/doc/AlbumUserResponseDto.md @@ -8,7 +8,7 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**readonly** | **bool** | | +**role** | [**AlbumUserRole**](AlbumUserRole.md) | | **user** | [**UserResponseDto**](UserResponseDto.md) | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/AlbumUserRole.md b/mobile/openapi/doc/AlbumUserRole.md new file mode 100644 index 0000000000..d0f64ef3ec --- /dev/null +++ b/mobile/openapi/doc/AlbumUserRole.md @@ -0,0 +1,14 @@ +# openapi.model.AlbumUserRole + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/UpdateAlbumUserDto.md b/mobile/openapi/doc/UpdateAlbumUserDto.md index a2e1b3c745..1a1050b4db 100644 --- a/mobile/openapi/doc/UpdateAlbumUserDto.md +++ b/mobile/openapi/doc/UpdateAlbumUserDto.md @@ -8,7 +8,7 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**readonly** | **bool** | | +**role** | [**AlbumUserRole**](AlbumUserRole.md) | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 72927349af..e479ed6670 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -63,6 +63,7 @@ part 'model/add_users_dto.dart'; part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; part 'model/album_user_response_dto.dart'; +part 'model/album_user_role.dart'; part 'model/all_job_status_response_dto.dart'; part 'model/asset_bulk_delete_dto.dart'; part 'model/asset_bulk_update_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 502a6d9096..a4260edff1 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -204,6 +204,8 @@ class ApiClient { return AlbumResponseDto.fromJson(value); case 'AlbumUserResponseDto': return AlbumUserResponseDto.fromJson(value); + case 'AlbumUserRole': + return AlbumUserRoleTypeTransformer().decode(value); case 'AllJobStatusResponseDto': return AllJobStatusResponseDto.fromJson(value); case 'AssetBulkDeleteDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 7ad74d9516..8d92ad1f0a 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -55,6 +55,9 @@ String parameterToString(dynamic value) { if (value is DateTime) { return value.toUtc().toIso8601String(); } + if (value is AlbumUserRole) { + return AlbumUserRoleTypeTransformer().encode(value).toString(); + } if (value is AssetJobName) { return AssetJobNameTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 18b11a3035..6f3dbc0723 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -82,7 +82,7 @@ class AlbumResponseDto { bool shared; - /// Deprecated in favor of users + /// Deprecated in favor of sharedUsersV2 List sharedUsers; List sharedUsersV2; diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index 338f8a0762..896c1cbb8f 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -13,31 +13,31 @@ part of openapi.api; class AlbumUserResponseDto { /// Returns a new [AlbumUserResponseDto] instance. AlbumUserResponseDto({ - required this.readonly, + required this.role, required this.user, }); - bool readonly; + AlbumUserRole role; UserResponseDto user; @override bool operator ==(Object other) => identical(this, other) || other is AlbumUserResponseDto && - other.readonly == readonly && + other.role == role && other.user == user; @override int get hashCode => // ignore: unnecessary_parenthesis - (readonly.hashCode) + + (role.hashCode) + (user.hashCode); @override - String toString() => 'AlbumUserResponseDto[readonly=$readonly, user=$user]'; + String toString() => 'AlbumUserResponseDto[role=$role, user=$user]'; Map toJson() { final json = {}; - json[r'readonly'] = this.readonly; + json[r'role'] = this.role; json[r'user'] = this.user; return json; } @@ -50,7 +50,7 @@ class AlbumUserResponseDto { final json = value.cast(); return AlbumUserResponseDto( - readonly: mapValueOfType(json, r'readonly')!, + role: AlbumUserRole.fromJson(json[r'role'])!, user: UserResponseDto.fromJson(json[r'user'])!, ); } @@ -99,7 +99,7 @@ class AlbumUserResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'readonly', + 'role', 'user', }; } diff --git a/mobile/openapi/lib/model/album_user_role.dart b/mobile/openapi/lib/model/album_user_role.dart new file mode 100644 index 0000000000..991d6d182c --- /dev/null +++ b/mobile/openapi/lib/model/album_user_role.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class AlbumUserRole { + /// Instantiate a new enum with the provided [value]. + const AlbumUserRole._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const editor = AlbumUserRole._(r'editor'); + static const viewer = AlbumUserRole._(r'viewer'); + + /// List of all possible values in this [enum][AlbumUserRole]. + static const values = [ + editor, + viewer, + ]; + + static AlbumUserRole? fromJson(dynamic value) => AlbumUserRoleTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AlbumUserRole.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AlbumUserRole] to String, +/// and [decode] dynamic data back to [AlbumUserRole]. +class AlbumUserRoleTypeTransformer { + factory AlbumUserRoleTypeTransformer() => _instance ??= const AlbumUserRoleTypeTransformer._(); + + const AlbumUserRoleTypeTransformer._(); + + String encode(AlbumUserRole data) => data.value; + + /// Decodes a [dynamic value][data] to a AlbumUserRole. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AlbumUserRole? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'editor': return AlbumUserRole.editor; + case r'viewer': return AlbumUserRole.viewer; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AlbumUserRoleTypeTransformer] instance. + static AlbumUserRoleTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart index 4a86f1dc0f..8e85349318 100644 --- a/mobile/openapi/lib/model/update_album_user_dto.dart +++ b/mobile/openapi/lib/model/update_album_user_dto.dart @@ -13,26 +13,26 @@ part of openapi.api; class UpdateAlbumUserDto { /// Returns a new [UpdateAlbumUserDto] instance. UpdateAlbumUserDto({ - required this.readonly, + required this.role, }); - bool readonly; + AlbumUserRole role; @override bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumUserDto && - other.readonly == readonly; + other.role == role; @override int get hashCode => // ignore: unnecessary_parenthesis - (readonly.hashCode); + (role.hashCode); @override - String toString() => 'UpdateAlbumUserDto[readonly=$readonly]'; + String toString() => 'UpdateAlbumUserDto[role=$role]'; Map toJson() { final json = {}; - json[r'readonly'] = this.readonly; + json[r'role'] = this.role; return json; } @@ -44,7 +44,7 @@ class UpdateAlbumUserDto { final json = value.cast(); return UpdateAlbumUserDto( - readonly: mapValueOfType(json, r'readonly')!, + role: AlbumUserRole.fromJson(json[r'role'])!, ); } return null; @@ -92,7 +92,7 @@ class UpdateAlbumUserDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'readonly', + 'role', }; } diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index e18db6ad07..405c3aca27 100644 --- a/mobile/openapi/test/album_response_dto_test.dart +++ b/mobile/openapi/test/album_response_dto_test.dart @@ -91,7 +91,7 @@ void main() { // TODO }); - // Deprecated in favor of users + // Deprecated in favor of sharedUsersV2 // List sharedUsers (default value: const []) test('to test the property `sharedUsers`', () async { // TODO diff --git a/mobile/openapi/test/album_user_response_dto_test.dart b/mobile/openapi/test/album_user_response_dto_test.dart index e12b6ea60c..19f15a305d 100644 --- a/mobile/openapi/test/album_user_response_dto_test.dart +++ b/mobile/openapi/test/album_user_response_dto_test.dart @@ -16,8 +16,8 @@ void main() { // final instance = AlbumUserResponseDto(); group('test AlbumUserResponseDto', () { - // bool readonly - test('to test the property `readonly`', () async { + // AlbumUserRole role + test('to test the property `role`', () async { // TODO }); diff --git a/mobile/openapi/test/album_user_role_test.dart b/mobile/openapi/test/album_user_role_test.dart new file mode 100644 index 0000000000..bc09896215 --- /dev/null +++ b/mobile/openapi/test/album_user_role_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AlbumUserRole +void main() { + + group('test AlbumUserRole', () { + + }); + +} diff --git a/mobile/openapi/test/update_album_user_dto_test.dart b/mobile/openapi/test/update_album_user_dto_test.dart index 13ba7632f7..a42ca38b2c 100644 --- a/mobile/openapi/test/update_album_user_dto_test.dart +++ b/mobile/openapi/test/update_album_user_dto_test.dart @@ -16,8 +16,8 @@ void main() { // final instance = UpdateAlbumUserDto(); group('test UpdateAlbumUserDto', () { - // bool readonly - test('to test the property `readonly`', () async { + // AlbumUserRole role + test('to test the property `role`', () async { // TODO }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 38538f7e65..ac567e54cc 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7174,7 +7174,7 @@ }, "sharedUsers": { "deprecated": true, - "description": "Deprecated in favor of users", + "description": "Deprecated in favor of sharedUsersV2", "items": { "$ref": "#/components/schemas/UserResponseDto" }, @@ -7216,19 +7216,26 @@ }, "AlbumUserResponseDto": { "properties": { - "readonly": { - "type": "boolean" + "role": { + "$ref": "#/components/schemas/AlbumUserRole" }, "user": { "$ref": "#/components/schemas/UserResponseDto" } }, "required": [ - "readonly", + "role", "user" ], "type": "object" }, + "AlbumUserRole": { + "enum": [ + "editor", + "viewer" + ], + "type": "string" + }, "AllJobStatusResponseDto": { "properties": { "backgroundTask": { @@ -10962,12 +10969,12 @@ }, "UpdateAlbumUserDto": { "properties": { - "readonly": { - "type": "boolean" + "role": { + "$ref": "#/components/schemas/AlbumUserRole" } }, "required": [ - "readonly" + "role" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7f828c7766..a9c768deb3 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -142,7 +142,7 @@ export type AssetResponseDto = { updatedAt: string; }; export type AlbumUserResponseDto = { - "readonly": boolean; + role: AlbumUserRole; user: UserResponseDto; }; export type AlbumResponseDto = { @@ -161,7 +161,7 @@ export type AlbumResponseDto = { owner: UserResponseDto; ownerId: string; shared: boolean; - /** Deprecated in favor of users */ + /** Deprecated in favor of sharedUsersV2 */ sharedUsers: UserResponseDto[]; sharedUsersV2: AlbumUserResponseDto[]; startDate?: string; @@ -194,7 +194,7 @@ export type BulkIdResponseDto = { success: boolean; }; export type UpdateAlbumUserDto = { - "readonly": boolean; + role: AlbumUserRole; }; export type AddUsersDto = { sharedUserIds: string[]; @@ -2901,6 +2901,10 @@ export enum AssetOrder { Asc = "asc", Desc = "desc" } +export enum AlbumUserRole { + Editor = "editor", + Viewer = "viewer" +} export enum Error { Duplicate = "duplicate", NoPermission = "no_permission", diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts index dfd4133ebe..18b1410d43 100644 --- a/server/src/cores/access.core.ts +++ b/server/src/cores/access.core.ts @@ -1,5 +1,6 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AlbumUserRole } from 'src/entities/album-user.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; @@ -219,7 +220,7 @@ export class AccessCore { const isShared = await this.repository.album.checkSharedAlbumAccess( auth.user.id, setDifference(ids, isOwner), - 'read', + AlbumUserRole.Viewer, ); return setUnion(isOwner, isShared); } @@ -229,7 +230,7 @@ export class AccessCore { const isShared = await this.repository.album.checkSharedAlbumAccess( auth.user.id, setDifference(ids, isOwner), - 'write', + AlbumUserRole.Editor, ); return setUnion(isOwner, isShared); } @@ -251,7 +252,7 @@ export class AccessCore { const isShared = await this.repository.album.checkSharedAlbumAccess( auth.user.id, setDifference(ids, isOwner), - 'read', + AlbumUserRole.Viewer, ); return setUnion(isOwner, isShared); } diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 15e606b073..0dfbb8fe36 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -3,6 +3,7 @@ import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; +import { AlbumUserRole } from 'src/entities/album-user.entity'; import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; @@ -84,13 +85,15 @@ export class AlbumCountResponseDto { } export class UpdateAlbumUserDto { - @ValidateBoolean() - readonly!: boolean; + @IsEnum(AlbumUserRole) + @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) + role!: AlbumUserRole; } export class AlbumUserResponseDto { user!: UserResponseDto; - readonly!: boolean; + @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' }) + role!: AlbumUserRole; } export class AlbumResponseDto { @@ -128,7 +131,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt sharedUsers.push(mapUser(permission.user)); sharedUsersV2.push({ user: mapUser(permission.user), - readonly: permission.readonly, + role: permission.role, }); } } diff --git a/server/src/entities/album-user.entity.ts b/server/src/entities/album-user.entity.ts index b45a574be4..4617b07757 100644 --- a/server/src/entities/album-user.entity.ts +++ b/server/src/entities/album-user.entity.ts @@ -2,6 +2,11 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { UserEntity } from 'src/entities/user.entity'; import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; +export enum AlbumUserRole { + Editor = 'editor', + Viewer = 'viewer', +} + @Entity('albums_shared_users_users') // Pre-existing indices from original album <--> user ManyToMany mapping @Index('IDX_427c350ad49bd3935a50baab73', ['album']) @@ -17,6 +22,6 @@ export class AlbumUserEntity { @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) user!: UserEntity; - @Column({ default: true }) - readonly!: boolean; + @Column({ type: 'varchar', default: 'viewer' }) + role!: AlbumUserRole; } diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts index 0e147bf65e..38f0721866 100644 --- a/server/src/interfaces/access.interface.ts +++ b/server/src/interfaces/access.interface.ts @@ -1,3 +1,5 @@ +import { AlbumUserRole } from 'src/entities/album-user.entity'; + export const IAccessRepository = 'IAccessRepository'; export type ReadWrite = 'read' | 'write'; @@ -22,7 +24,7 @@ export interface IAccessRepository { album: { checkOwnerAccess(userId: string, albumIds: Set): Promise>; - checkSharedAlbumAccess(userId: string, albumIds: Set, readWrite: ReadWrite): Promise>; + checkSharedAlbumAccess(userId: string, albumIds: Set, access: AlbumUserRole): Promise>; checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise>; }; diff --git a/server/src/migrations/1713298646379-AddAlbumUserReadonly.ts b/server/src/migrations/1713337511945-AddAlbumUserRole.ts similarity index 59% rename from server/src/migrations/1713298646379-AddAlbumUserReadonly.ts rename to server/src/migrations/1713337511945-AddAlbumUserRole.ts index bf42aa3048..d97abab9dc 100644 --- a/server/src/migrations/1713298646379-AddAlbumUserReadonly.ts +++ b/server/src/migrations/1713337511945-AddAlbumUserRole.ts @@ -1,15 +1,15 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class AddAlbumUserReadonly1713298646379 implements MigrationInterface { - name = 'AddAlbumUserReadonly1713298646379' +export class AddAlbumUserRole1713337511945 implements MigrationInterface { + name = 'AddAlbumUserRole1713337511945' public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD "readonly" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ALTER COLUMN "readonly" SET DEFAULT true`); + await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD "role" character varying NOT NULL DEFAULT 'editor'`); + await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ALTER COLUMN "role" SET DEFAULT 'viewer'`); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "readonly"`); + await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "role"`); } } diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index 019d7ef2a3..cfc38605d6 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -70,7 +70,7 @@ SELECT "AlbumEntity"."id" AS "AlbumEntity_id", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", - "AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly" + "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" @@ -83,6 +83,9 @@ WHERE ( "AlbumEntity__AlbumEntity_sharedUsers"."usersId" = $2 ) + AND ( + "AlbumEntity__AlbumEntity_sharedUsers"."role" IN ($3, $4) + ) ) ) ) diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 175b828562..cd7567f462 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -34,7 +34,7 @@ FROM "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", - "AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", + "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", @@ -114,7 +114,7 @@ SELECT "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", - "AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", + "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", @@ -176,7 +176,7 @@ SELECT "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", - "AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", + "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", @@ -296,7 +296,7 @@ SELECT "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", - "AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", + "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", @@ -373,7 +373,7 @@ SELECT "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", - "AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", + "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", @@ -489,7 +489,7 @@ SELECT "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", - "AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", + "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 6ca4e3f539..c322cb8478 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -1,6 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { ActivityEntity } from 'src/entities/activity.entity'; +import { AlbumUserRole } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; @@ -10,7 +11,7 @@ import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserTokenEntity } from 'src/entities/user-token.entity'; -import { IAccessRepository, ReadWrite } from 'src/interfaces/access.interface'; +import { IAccessRepository } from 'src/interfaces/access.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { Brackets, In, Repository } from 'typeorm'; @@ -119,7 +120,7 @@ class AlbumAccess implements IAlbumAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkSharedAlbumAccess(userId: string, albumIds: Set, readWrite: ReadWrite): Promise> { + async checkSharedAlbumAccess(userId: string, albumIds: Set, access: AlbumUserRole): Promise> { if (albumIds.size === 0) { return new Set(); } @@ -132,8 +133,9 @@ class AlbumAccess implements IAlbumAccess { id: In([...albumIds]), sharedUsers: { user: { id: userId }, - // If write is needed we check for it, otherwise both are accepted - readonly: readWrite === 'write' ? false : undefined, + // If editor access is needed we check for it, otherwise both are accepted + role: + access === AlbumUserRole.Editor ? AlbumUserRole.Editor : In([AlbumUserRole.Editor, AlbumUserRole.Viewer]), }, }, }) diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 932f7fc110..74185c3963 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -273,7 +273,7 @@ export class AlbumService { throw new BadRequestException('Album not shared with user'); } - await this.albumPermissionRepository.update({ albumId: id, userId }, { readonly: dto.readonly }); + await this.albumPermissionRepository.update({ albumId: id, userId }, { role: dto.role }); } private async findOrFail(id: string, options: AlbumInfoOptions) { diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index 79d10c5294..62cdb99709 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -4,8 +4,8 @@ removeUserFromAlbum, type AlbumResponseDto, type UserResponseDto, - updateAlbumUser, - } from '@immich/sdk'; + updateAlbumUser, AlbumUserRole, + } from '@immich/sdk' import { mdiDotsVertical } from '@mdi/js'; import { createEventDispatcher, onMount } from 'svelte'; import { getContextMenuPosition } from '../../utils/context-menu'; @@ -71,10 +71,10 @@ } }; - const handleSetReadonly = async (user: UserResponseDto, readonly: boolean) => { + const handleSetReadonly = async (user: UserResponseDto, role: AlbumUserRole) => { try { - await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { readonly } }); - const message = readonly ? `Set ${user.name} as viewer` : `Set ${user.name} as editor`; + await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } }); + const message = `Set ${user.name} as ${role}`; dispatch('refreshAlbum'); notificationController.show({ type: NotificationType.Info, message }); } catch (error) { @@ -99,14 +99,14 @@ {#each album.sharedUsersV2.toSorted((a, b) => { - if (a.readonly && !b.readonly) { + if (a.role === AlbumUserRole.Viewer && b.role === AlbumUserRole.Editor) { return 1; } - if (!a.readonly && b.readonly) { + if (a.role === AlbumUserRole.Editor && b.role === AlbumUserRole.Viewer) { return -1; } return a.user.name.localeCompare(b.user.name); - }) as { user, readonly }} + }) as { user, role }}
@@ -117,7 +117,7 @@
- {#if readonly} + {#if role === AlbumUserRole.Viewer} Viewer {:else} Editor @@ -136,10 +136,10 @@ {#if selectedMenuUser === user} (selectedMenuUser = null)}> - {#if readonly} - handleSetReadonly(user, false)} text="Allow edits" /> + {#if role === AlbumUserRole.Viewer} + handleSetReadonly(user, AlbumUserRole.Editor)} text="Allow edits" /> {:else} - handleSetReadonly(user, true)} text="Disallow edits" /> + handleSetReadonly(user, AlbumUserRole.Viewer)} text="Disallow edits" /> {/if} diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 0bc9da6e8b..7917b13d71 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -1,84 +1,83 @@