change readonly boolean to role enum

This commit is contained in:
mgabor
2024-04-17 09:31:56 +02:00
parent 9126bf2520
commit 87bc244b68
30 changed files with 297 additions and 141 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -23,7 +23,7 @@ Name | Type | Description | Notes
**owner** | [**UserResponseDto**](UserResponseDto.md) | |
**ownerId** | **String** | |
**shared** | **bool** | |
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | Deprecated in favor of users | [default to const []]
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | Deprecated in favor of sharedUsersV2 | [default to const []]
**sharedUsersV2** | [**List<AlbumUserResponseDto>**](AlbumUserResponseDto.md) | | [default to const []]
**startDate** | [**DateTime**](DateTime.md) | | [optional]
**updatedAt** | [**DateTime**](DateTime.md) | |

View File

@@ -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)

14
mobile/openapi/doc/AlbumUserRole.md generated Normal file
View File

@@ -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)

View File

@@ -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)

View File

@@ -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';

View File

@@ -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':

View File

@@ -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();
}

View File

@@ -82,7 +82,7 @@ class AlbumResponseDto {
bool shared;
/// Deprecated in favor of users
/// Deprecated in favor of sharedUsersV2
List<UserResponseDto> sharedUsers;
List<AlbumUserResponseDto> sharedUsersV2;

View File

@@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return AlbumUserResponseDto(
readonly: mapValueOfType<bool>(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 = <String>{
'readonly',
'role',
'user',
};
}

View File

@@ -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 = <AlbumUserRole>[
editor,
viewer,
];
static AlbumUserRole? fromJson(dynamic value) => AlbumUserRoleTypeTransformer().decode(value);
static List<AlbumUserRole> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumUserRole>[];
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;
}

View File

@@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'readonly'] = this.readonly;
json[r'role'] = this.role;
return json;
}
@@ -44,7 +44,7 @@ class UpdateAlbumUserDto {
final json = value.cast<String, dynamic>();
return UpdateAlbumUserDto(
readonly: mapValueOfType<bool>(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 = <String>{
'readonly',
'role',
};
}

View File

@@ -91,7 +91,7 @@ void main() {
// TODO
});
// Deprecated in favor of users
// Deprecated in favor of sharedUsersV2
// List<UserResponseDto> sharedUsers (default value: const [])
test('to test the property `sharedUsers`', () async {
// TODO

View File

@@ -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
});

View File

@@ -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', () {
});
}

View File

@@ -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
});

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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,
});
}
}

View File

@@ -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;
}

View File

@@ -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<string>): Promise<Set<string>>;
checkSharedAlbumAccess(userId: string, albumIds: Set<string>, readWrite: ReadWrite): Promise<Set<string>>;
checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>>;
checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>;
};

View File

@@ -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<void> {
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<void> {
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "readonly"`);
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "role"`);
}
}

View File

@@ -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)
)
)
)
)

View File

@@ -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",

View File

@@ -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<string>, readWrite: ReadWrite): Promise<Set<string>> {
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>> {
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]),
},
},
})

View File

@@ -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) {

View File

@@ -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 @@
</div>
</div>
{#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 }}
<div
class="flex w-full place-items-center justify-between gap-4 p-5 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
>
@@ -117,7 +117,7 @@
<div id="icon-{user.id}" class="flex place-items-center gap-2">
<div>
{#if readonly}
{#if role === AlbumUserRole.Viewer}
Viewer
{:else}
Editor
@@ -136,10 +136,10 @@
{#if selectedMenuUser === user}
<ContextMenu {...position} on:outclick={() => (selectedMenuUser = null)}>
{#if readonly}
<MenuOption on:click={() => handleSetReadonly(user, false)} text="Allow edits" />
{#if role === AlbumUserRole.Viewer}
<MenuOption on:click={() => handleSetReadonly(user, AlbumUserRole.Editor)} text="Allow edits" />
{:else}
<MenuOption on:click={() => handleSetReadonly(user, true)} text="Disallow edits" />
<MenuOption on:click={() => handleSetReadonly(user, AlbumUserRole.Viewer)} text="Disallow edits" />
{/if}
<MenuOption on:click={handleMenuRemove} text="Remove" />
</ContextMenu>

View File

@@ -1,84 +1,83 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import AlbumOptions from '$lib/components/album-page/album-options.svelte';
import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte';
import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
import {afterNavigate, goto} from '$app/navigation'
import AlbumDescription from '$lib/components/album-page/album-description.svelte'
import AlbumOptions from '$lib/components/album-page/album-options.svelte'
import AlbumSummary from '$lib/components/album-page/album-summary.svelte'
import AlbumTitle from '$lib/components/album-page/album-title.svelte'
import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte'
import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte'
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte'
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte'
import Button from '$lib/components/elements/buttons/button.svelte'
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'
import Icon from '$lib/components/elements/icon.svelte'
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte'
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'
import CreateSharedLinkModal
from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'
import {notificationController, NotificationType,} from '$lib/components/shared-components/notification/notification'
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'
import {AppRoute} from '$lib/constants'
import {numberOfComments, setNumberOfComments, updateNumberOfComments} from '$lib/stores/activity.store'
import {createAssetInteractionStore} from '$lib/stores/asset-interaction.store'
import {assetViewingStore} from '$lib/stores/asset-viewing.store'
import {AssetStore} from '$lib/stores/assets.store'
import {locale} from '$lib/stores/preferences.store'
import {SlideshowNavigation, SlideshowState, slideshowStore} from '$lib/stores/slideshow.store'
import {user} from '$lib/stores/user.store'
import {handlePromiseError} from '$lib/utils'
import {downloadAlbum} from '$lib/utils/asset-utils'
import {clickOutside} from '$lib/utils/click-outside'
import {getContextMenuPosition} from '$lib/utils/context-menu'
import {openFileUploadDialog} from '$lib/utils/file-uploader'
import {handleError} from '$lib/utils/handle-error'
import {
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute } from '$lib/constants';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store';
import { locale } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store';
import { downloadAlbum } from '$lib/utils/asset-utils';
import { clickOutside } from '$lib/utils/click-outside';
import { getContextMenuPosition } from '$lib/utils/context-menu';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error';
import {
ReactionLevel,
ReactionType,
type ActivityResponseDto,
addAssetsToAlbum,
addUsersToAlbum,
AlbumUserRole,
AssetOrder,
createActivity,
deleteActivity,
deleteAlbum,
getActivities,
getActivityStatistics,
getAlbumInfo,
ReactionLevel,
ReactionType,
updateAlbumInfo,
type ActivityResponseDto,
type UserResponseDto,
AssetOrder,
} from '@immich/sdk';
} from '@immich/sdk'
import {
mdiArrowLeft,
mdiCogOutline,
mdiDeleteOutline,
mdiDotsVertical,
mdiFolderDownloadOutline,
mdiLink,
mdiPlus,
mdiShareVariantOutline,
mdiPresentationPlay,
mdiCogOutline,
mdiImageOutline,
mdiImagePlusOutline,
} from '@mdi/js';
import { fly } from 'svelte/transition';
import type { PageData } from './$types';
import AlbumTitle from '$lib/components/album-page/album-title.svelte';
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
import { handlePromiseError } from '$lib/utils';
import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
mdiLink,
mdiPlus,
mdiPresentationPlay,
mdiShareVariantOutline,
} from '@mdi/js'
import {fly} from 'svelte/transition'
import type {PageData} from './$types'
export let data: PageData;
@@ -136,8 +135,8 @@
$: showActivityStatus =
album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
$: userHasWriteAccess = !album.sharedUsersV2.find(({ user: { id } }) => id === $user.id)?.readonly;
$: albumHasReadonlyUsers = album.sharedUsersV2.some(({ readonly }) => readonly);
$: isEditor = album.sharedUsersV2.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor;
$: albumHasViewers = album.sharedUsersV2.some(({ role }) => role === AlbumUserRole.Viewer);
afterNavigate(({ from }) => {
assetViewingStore.showAssetViewer(false);
@@ -436,7 +435,7 @@
{#if viewMode === ViewMode.VIEW || viewMode === ViewMode.ALBUM_OPTIONS}
<ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(backUrl)}>
<svelte:fragment slot="trailing">
{#if userHasWriteAccess}
{#if isEditor}
<CircleIconButton
title="Add photos"
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
@@ -584,14 +583,14 @@
</button>
<!-- users with write access (collaborators) -->
{#each album.sharedUsersV2.filter(({ readonly }) => !readonly) as { user } (user.id)}
{#each album.sharedUsersV2.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
<UserAvatar {user} size="md" />
</button>
{/each}
<!-- display ellipsis if there are readonly users too -->
{#if albumHasReadonlyUsers}
{#if albumHasViewers}
<CircleIconButton
title="View all users"
backgroundColor="#d3d3d3"