diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index d2f865ba1e..f83444413e 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -142,6 +142,7 @@ doc/ServerPingResponse.md doc/ServerStatsResponseDto.md doc/ServerThemeDto.md doc/ServerVersionResponseDto.md +doc/SetAlbumPermissionDto.md doc/SharedLinkApi.md doc/SharedLinkCreateDto.md doc/SharedLinkEditDto.md @@ -355,6 +356,7 @@ lib/model/server_ping_response.dart lib/model/server_stats_response_dto.dart lib/model/server_theme_dto.dart lib/model/server_version_response_dto.dart +lib/model/set_album_permission_dto.dart lib/model/shared_link_create_dto.dart lib/model/shared_link_edit_dto.dart lib/model/shared_link_response_dto.dart @@ -547,6 +549,7 @@ test/server_ping_response_test.dart test/server_stats_response_dto_test.dart test/server_theme_dto_test.dart test/server_version_response_dto_test.dart +test/set_album_permission_dto_test.dart test/shared_link_api_test.dart test/shared_link_create_dto_test.dart test/shared_link_edit_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 4093a7f572..5c57c7258b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -90,6 +90,7 @@ Class | Method | HTTP request | Description *AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album | *AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | +*AlbumApi* | [**setAlbumPermission**](doc//AlbumApi.md#setalbumpermission) | **PUT** /album/{id}/permission/{userId} | *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | *AssetApi* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check | *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | @@ -346,6 +347,7 @@ Class | Method | HTTP request | Description - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) - [ServerThemeDto](doc//ServerThemeDto.md) - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) + - [SetAlbumPermissionDto](doc//SetAlbumPermissionDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) diff --git a/mobile/openapi/doc/AlbumApi.md b/mobile/openapi/doc/AlbumApi.md index 427181880d..02c2651122 100644 --- a/mobile/openapi/doc/AlbumApi.md +++ b/mobile/openapi/doc/AlbumApi.md @@ -18,6 +18,7 @@ Method | HTTP request | Description [**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album | [**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | [**removeUserFromAlbum**](AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | +[**setAlbumPermission**](AlbumApi.md#setalbumpermission) | **PUT** /album/{id}/permission/{userId} | [**updateAlbumInfo**](AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | @@ -526,6 +527,64 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **setAlbumPermission** +> setAlbumPermission(id, userId, setAlbumPermissionDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = AlbumApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final userId = userId_example; // String | +final setAlbumPermissionDto = SetAlbumPermissionDto(); // SetAlbumPermissionDto | + +try { + api_instance.setAlbumPermission(id, userId, setAlbumPermissionDto); +} catch (e) { + print('Exception when calling AlbumApi->setAlbumPermission: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **userId** | **String**| | + **setAlbumPermissionDto** | [**SetAlbumPermissionDto**](SetAlbumPermissionDto.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **updateAlbumInfo** > AlbumResponseDto updateAlbumInfo(id, updateAlbumDto) diff --git a/mobile/openapi/doc/SetAlbumPermissionDto.md b/mobile/openapi/doc/SetAlbumPermissionDto.md new file mode 100644 index 0000000000..d15312ba04 --- /dev/null +++ b/mobile/openapi/doc/SetAlbumPermissionDto.md @@ -0,0 +1,15 @@ +# openapi.model.SetAlbumPermissionDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**readonly** | **bool** | | + +[[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 a0fc9f3fc9..7d4cb5086f 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -175,6 +175,7 @@ part 'model/server_ping_response.dart'; part 'model/server_stats_response_dto.dart'; part 'model/server_theme_dto.dart'; part 'model/server_version_response_dto.dart'; +part 'model/set_album_permission_dto.dart'; part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_response_dto.dart'; diff --git a/mobile/openapi/lib/api/album_api.dart b/mobile/openapi/lib/api/album_api.dart index f5fe2e3c1d..49b0fde873 100644 --- a/mobile/openapi/lib/api/album_api.dart +++ b/mobile/openapi/lib/api/album_api.dart @@ -485,6 +485,55 @@ class AlbumApi { } } + /// Performs an HTTP 'PUT /album/{id}/permission/{userId}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] userId (required): + /// + /// * [SetAlbumPermissionDto] setAlbumPermissionDto (required): + Future setAlbumPermissionWithHttpInfo(String id, String userId, SetAlbumPermissionDto setAlbumPermissionDto,) async { + // ignore: prefer_const_declarations + final path = r'/album/{id}/permission/{userId}' + .replaceAll('{id}', id) + .replaceAll('{userId}', userId); + + // ignore: prefer_final_locals + Object? postBody = setAlbumPermissionDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] userId (required): + /// + /// * [SetAlbumPermissionDto] setAlbumPermissionDto (required): + Future setAlbumPermission(String id, String userId, SetAlbumPermissionDto setAlbumPermissionDto,) async { + final response = await setAlbumPermissionWithHttpInfo(id, userId, setAlbumPermissionDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'PATCH /album/{id}' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 982ede489d..0d318333f4 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -428,6 +428,8 @@ class ApiClient { return ServerThemeDto.fromJson(value); case 'ServerVersionResponseDto': return ServerVersionResponseDto.fromJson(value); + case 'SetAlbumPermissionDto': + return SetAlbumPermissionDto.fromJson(value); case 'SharedLinkCreateDto': return SharedLinkCreateDto.fromJson(value); case 'SharedLinkEditDto': diff --git a/mobile/openapi/lib/model/set_album_permission_dto.dart b/mobile/openapi/lib/model/set_album_permission_dto.dart new file mode 100644 index 0000000000..64156f72d1 --- /dev/null +++ b/mobile/openapi/lib/model/set_album_permission_dto.dart @@ -0,0 +1,98 @@ +// +// 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 SetAlbumPermissionDto { + /// Returns a new [SetAlbumPermissionDto] instance. + SetAlbumPermissionDto({ + required this.readonly, + }); + + bool readonly; + + @override + bool operator ==(Object other) => identical(this, other) || other is SetAlbumPermissionDto && + other.readonly == readonly; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (readonly.hashCode); + + @override + String toString() => 'SetAlbumPermissionDto[readonly=$readonly]'; + + Map toJson() { + final json = {}; + json[r'readonly'] = this.readonly; + return json; + } + + /// Returns a new [SetAlbumPermissionDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SetAlbumPermissionDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SetAlbumPermissionDto( + readonly: mapValueOfType(json, r'readonly')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SetAlbumPermissionDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SetAlbumPermissionDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SetAlbumPermissionDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SetAlbumPermissionDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'readonly', + }; +} + diff --git a/mobile/openapi/test/album_api_test.dart b/mobile/openapi/test/album_api_test.dart index aee26e095d..8c0e5803f5 100644 --- a/mobile/openapi/test/album_api_test.dart +++ b/mobile/openapi/test/album_api_test.dart @@ -62,6 +62,11 @@ void main() { // TODO }); + //Future setAlbumPermission(String id, String userId, SetAlbumPermissionDto setAlbumPermissionDto) async + test('test setAlbumPermission', () async { + // TODO + }); + //Future updateAlbumInfo(String id, UpdateAlbumDto updateAlbumDto) async test('test updateAlbumInfo', () async { // TODO diff --git a/mobile/openapi/test/set_album_permission_dto_test.dart b/mobile/openapi/test/set_album_permission_dto_test.dart new file mode 100644 index 0000000000..caac924da7 --- /dev/null +++ b/mobile/openapi/test/set_album_permission_dto_test.dart @@ -0,0 +1,27 @@ +// +// 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 SetAlbumPermissionDto +void main() { + // final instance = SetAlbumPermissionDto(); + + group('test SetAlbumPermissionDto', () { + // bool readonly + test('to test the property `readonly`', () async { + // TODO + }); + + + }); + +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2147d87b52..2ef6697baf 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -589,6 +589,59 @@ ] } }, + "/album/{id}/permission/{userId}": { + "put": { + "operationId": "setAlbumPermission", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetAlbumPermissionDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Album" + ] + } + }, "/album/{id}/user/{userId}": { "delete": { "operationId": "removeUserFromAlbum", @@ -9914,6 +9967,17 @@ ], "type": "object" }, + "SetAlbumPermissionDto": { + "properties": { + "readonly": { + "type": "boolean" + } + }, + "required": [ + "readonly" + ], + "type": "object" + }, "SharedLinkCreateDto": { "properties": { "albumId": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 5533e9d3d2..9e42b67af4 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -193,6 +193,9 @@ export type BulkIdResponseDto = { id: string; success: boolean; }; +export type SetAlbumPermissionDto = { + "readonly": boolean; +}; export type AddUsersDto = { sharedUserIds: string[]; }; @@ -1193,6 +1196,17 @@ export function addAssetsToAlbum({ id, key, bulkIdsDto }: { body: bulkIdsDto }))); } +export function setAlbumPermission({ id, userId, setAlbumPermissionDto }: { + id: string; + userId: string; + setAlbumPermissionDto: SetAlbumPermissionDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/album/${encodeURIComponent(id)}/permission/${encodeURIComponent(userId)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: setAlbumPermissionDto + }))); +} export function removeUserFromAlbum({ id, userId }: { id: string; userId: string; diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index c4b11fbb4c..869071f411 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -7,6 +7,7 @@ import { AlbumResponseDto, CreateAlbumDto, GetAlbumsDto, + SetAlbumPermissionDto, UpdateAlbumDto, } from 'src/dtos/album.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; @@ -96,4 +97,14 @@ export class AlbumController { ) { return this.service.removeUser(auth, id, userId); } + + @Put(':id/permission/:userId') + setAlbumPermission( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string, + @Body() dto: SetAlbumPermissionDto, + ): Promise { + return this.service.setAlbumPermission(auth, id, userId, dto); + } } diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 6205190505..ee5d07f667 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -83,6 +83,11 @@ export class AlbumCountResponseDto { notShared!: number; } +export class SetAlbumPermissionDto { + @ValidateBoolean() + readonly!: boolean; +} + export class AlbumPermissionResponseDto { user!: UserResponseDto; readonly!: boolean; diff --git a/server/src/repositories/album-permission.repository.ts b/server/src/repositories/album-permission.repository.ts index dce7dbf985..a569e324d3 100644 --- a/server/src/repositories/album-permission.repository.ts +++ b/server/src/repositories/album-permission.repository.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { Equals } from 'class-validator'; import { AlbumPermissionEntity } from 'src/entities/album-permission.entity'; import { IAlbumPermissionRepository } from 'src/interfaces/album-permission.interface'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Repository } from 'typeorm'; +import { Equal, Repository } from 'typeorm'; @Instrumentation() @Injectable() @@ -16,9 +17,11 @@ export class AlbumPermissionRepository implements IAlbumPermissionRepository { } async update(userId: string, albumId: string, dto: Partial): Promise { - await this.repository.update({ users: { id: userId }, albums: { id: albumId } }, dto); + // @ts-expect-error I'm pretty sure I messed something up with the entity because + // if I follow what typescript says I get postgres errors + await this.repository.update({ users: userId, albums: albumId }, dto); return this.repository.findOneOrFail({ - where: { users: { id: userId }, albums: { id: albumId } }, + where: { users: Equal(userId), albums: Equal(albumId) }, relations: { users: true }, }); } diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index b4b025b83a..628ea3e8c6 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -264,6 +264,26 @@ export class AlbumService { await this.albumPermissionRepository.delete(userId, id); } + async setAlbumPermission( + auth: AuthDto, + id: string, + userId: string, + dto: Partial, + ): Promise { + await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); + + const album = await this.findOrFail(id, { withAssets: false }); + + const permission = album.albumPermissions.find(({ users: { id } }) => id === userId); + if (!permission) { + throw new BadRequestException('Album not shared with user'); + } + + console.log(userId, id, dto.readonly); + + await this.albumPermissionRepository.update(userId, id, { readonly: dto.readonly }); + } + private async findOrFail(id: string, options: AlbumInfoOptions) { const album = await this.albumRepository.getById(id, options); if (!album) { 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 2249d20510..b1a66af4b4 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -1,5 +1,5 @@ {#if !selectedRemoveUser} @@ -78,7 +92,7 @@

Owner

- {#each album.sharedUsers as user} + {#each album.albumPermissions as {user, readonly}}
@@ -87,7 +101,14 @@

{user.name}

-
+
+
+ {#if readonly} + Viewer + {:else} + Editor + {/if} +
{#if isOwned}
(selectedMenuUser = null)}> + {#if readonly} + handleSetReadonly(user, false)} text="Allow edits" /> + {:else} + handleSetReadonly(user, true)} text="Disallow edits" /> + {/if} {/if} diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 86499d7313..3455d3111d 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -344,6 +344,14 @@ } }; + const handleUpdatePermissions = async () => { + try { + await refreshAlbum(); + } catch (error) { + handleError(error, 'Error updating permissions'); + } + }; + const handleDownloadAlbum = async () => { await downloadAlbum(album); }; @@ -695,6 +703,7 @@ onClose={() => (viewMode = ViewMode.VIEW)} {album} on:remove={({ detail: userId }) => handleRemoveUser(userId)} + on:updatePermissions={handleUpdatePermissions} /> {/if}