mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(server): improve validation of albums (#2188)
* feat(server): improve validation of albums * regenerate openapi + fix downloadArchive for web
This commit is contained in:
		
							
								
								
									
										6
									
								
								mobile/openapi/doc/AlbumApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/doc/AlbumApi.md
									
									
									
										generated
									
									
									
								
							| @@ -294,7 +294,7 @@ 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) | ||||
| 
 | ||||
| # **downloadArchive** | ||||
| > MultipartFile downloadArchive(albumId, skip, key) | ||||
| > MultipartFile downloadArchive(albumId, name, skip, key) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @@ -316,11 +316,12 @@ import 'package:openapi/api.dart'; | ||||
| 
 | ||||
| final api_instance = AlbumApi(); | ||||
| final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final name = name_example; // String |  | ||||
| final skip = 8.14; // num |  | ||||
| final key = key_example; // String |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.downloadArchive(albumId, skip, key); | ||||
|     final result = api_instance.downloadArchive(albumId, name, skip, key); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AlbumApi->downloadArchive: $e\n'); | ||||
| @@ -332,6 +333,7 @@ try { | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **albumId** | **String**|  |  | ||||
|  **name** | **String**|  | [optional]  | ||||
|  **skip** | **num**|  | [optional]  | ||||
|  **key** | **String**|  | [optional]  | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										6
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							| @@ -414,7 +414,7 @@ Name | Type | Description  | Notes | ||||
| [[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) | ||||
| 
 | ||||
| # **downloadLibrary** | ||||
| > MultipartFile downloadLibrary(skip, key) | ||||
| > MultipartFile downloadLibrary(name, skip, key) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @@ -435,11 +435,12 @@ import 'package:openapi/api.dart'; | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer'; | ||||
| 
 | ||||
| final api_instance = AssetApi(); | ||||
| final name = name_example; // String |  | ||||
| final skip = 8.14; // num |  | ||||
| final key = key_example; // String |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.downloadLibrary(skip, key); | ||||
|     final result = api_instance.downloadLibrary(name, skip, key); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->downloadLibrary: $e\n'); | ||||
| @@ -450,6 +451,7 @@ try { | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **name** | **String**|  | [optional]  | ||||
|  **skip** | **num**|  | [optional]  | ||||
|  **key** | **String**|  | [optional]  | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/CreateAlbumShareLinkDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/CreateAlbumShareLinkDto.md
									
									
									
										generated
									
									
									
								
							| @@ -9,7 +9,7 @@ import 'package:openapi/api.dart'; | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **albumId** | **String** |  |  | ||||
| **expiresAt** | **String** |  | [optional]  | ||||
| **expiresAt** | [**DateTime**](DateTime.md) |  | [optional]  | ||||
| **allowUpload** | **bool** |  | [optional]  | ||||
| **allowDownload** | **bool** |  | [optional]  | ||||
| **showExif** | **bool** |  | [optional]  | ||||
|   | ||||
							
								
								
									
										13
									
								
								mobile/openapi/lib/api/album_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								mobile/openapi/lib/api/album_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -295,10 +295,12 @@ class AlbumApi { | ||||
|   /// | ||||
|   /// * [String] albumId (required): | ||||
|   /// | ||||
|   /// * [String] name: | ||||
|   /// | ||||
|   /// * [num] skip: | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<Response> downloadArchiveWithHttpInfo(String albumId, { num? skip, String? key, }) async { | ||||
|   Future<Response> downloadArchiveWithHttpInfo(String albumId, { String? name, num? skip, String? key, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/album/{albumId}/download' | ||||
|       .replaceAll('{albumId}', albumId); | ||||
| @@ -310,6 +312,9 @@ class AlbumApi { | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     if (name != null) { | ||||
|       queryParams.addAll(_queryParams('', 'name', name)); | ||||
|     } | ||||
|     if (skip != null) { | ||||
|       queryParams.addAll(_queryParams('', 'skip', skip)); | ||||
|     } | ||||
| @@ -337,11 +342,13 @@ class AlbumApi { | ||||
|   /// | ||||
|   /// * [String] albumId (required): | ||||
|   /// | ||||
|   /// * [String] name: | ||||
|   /// | ||||
|   /// * [num] skip: | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<MultipartFile?> downloadArchive(String albumId, { num? skip, String? key, }) async { | ||||
|     final response = await downloadArchiveWithHttpInfo(albumId,  skip: skip, key: key, ); | ||||
|   Future<MultipartFile?> downloadArchive(String albumId, { String? name, num? skip, String? key, }) async { | ||||
|     final response = await downloadArchiveWithHttpInfo(albumId,  name: name, skip: skip, key: key, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										13
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -422,10 +422,12 @@ class AssetApi { | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] name: | ||||
|   /// | ||||
|   /// * [num] skip: | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<Response> downloadLibraryWithHttpInfo({ num? skip, String? key, }) async { | ||||
|   Future<Response> downloadLibraryWithHttpInfo({ String? name, num? skip, String? key, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset/download-library'; | ||||
| 
 | ||||
| @@ -436,6 +438,9 @@ class AssetApi { | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     if (name != null) { | ||||
|       queryParams.addAll(_queryParams('', 'name', name)); | ||||
|     } | ||||
|     if (skip != null) { | ||||
|       queryParams.addAll(_queryParams('', 'skip', skip)); | ||||
|     } | ||||
| @@ -461,11 +466,13 @@ class AssetApi { | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] name: | ||||
|   /// | ||||
|   /// * [num] skip: | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<MultipartFile?> downloadLibrary({ num? skip, String? key, }) async { | ||||
|     final response = await downloadLibraryWithHttpInfo( skip: skip, key: key, ); | ||||
|   Future<MultipartFile?> downloadLibrary({ String? name, num? skip, String? key, }) async { | ||||
|     final response = await downloadLibraryWithHttpInfo( name: name, skip: skip, key: key, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
| @@ -29,7 +29,7 @@ class CreateAlbumShareLinkDto { | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   String? expiresAt; | ||||
|   DateTime? expiresAt; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
| @@ -89,7 +89,7 @@ class CreateAlbumShareLinkDto { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'albumId'] = this.albumId; | ||||
|     if (this.expiresAt != null) { | ||||
|       json[r'expiresAt'] = this.expiresAt; | ||||
|       json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); | ||||
|     } else { | ||||
|       // json[r'expiresAt'] = null; | ||||
|     } | ||||
| @@ -136,7 +136,7 @@ class CreateAlbumShareLinkDto { | ||||
| 
 | ||||
|       return CreateAlbumShareLinkDto( | ||||
|         albumId: mapValueOfType<String>(json, r'albumId')!, | ||||
|         expiresAt: mapValueOfType<String>(json, r'expiresAt'), | ||||
|         expiresAt: mapDateTime(json, r'expiresAt', ''), | ||||
|         allowUpload: mapValueOfType<bool>(json, r'allowUpload'), | ||||
|         allowDownload: mapValueOfType<bool>(json, r'allowDownload'), | ||||
|         showExif: mapValueOfType<bool>(json, r'showExif'), | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/test/album_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/test/album_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -54,7 +54,7 @@ void main() { | ||||
| 
 | ||||
|     //  | ||||
|     // | ||||
|     //Future<MultipartFile> downloadArchive(String albumId, { num skip, String key }) async | ||||
|     //Future<MultipartFile> downloadArchive(String albumId, { String name, num skip, String key }) async | ||||
|     test('test downloadArchive', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -68,7 +68,7 @@ void main() { | ||||
| 
 | ||||
|     // Current this is not used in any UI element | ||||
|     // | ||||
|     //Future<MultipartFile> downloadLibrary({ num skip, String key }) async | ||||
|     //Future<MultipartFile> downloadLibrary({ String name, num skip, String key }) async | ||||
|     test('test downloadLibrary', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|   | ||||
| @@ -21,7 +21,7 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // String expiresAt | ||||
|     // DateTime expiresAt | ||||
|     test('to test the property `expiresAt`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|   | ||||
| @@ -1,16 +1,4 @@ | ||||
| import { | ||||
|   Controller, | ||||
|   Get, | ||||
|   Post, | ||||
|   Body, | ||||
|   Patch, | ||||
|   Param, | ||||
|   Delete, | ||||
|   ValidationPipe, | ||||
|   Put, | ||||
|   Query, | ||||
|   Response, | ||||
| } from '@nestjs/common'; | ||||
| import { Controller, Get, Post, Body, Patch, Param, Delete, Put, Query, Response } from '@nestjs/common'; | ||||
| import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe'; | ||||
| import { AlbumService } from './album.service'; | ||||
| import { CreateAlbumDto } from './dto/create-album.dto'; | ||||
| @@ -33,9 +21,11 @@ import { | ||||
| import { DownloadDto } from '../asset/dto/download-library.dto'; | ||||
| import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/create-album-shared-link.dto'; | ||||
| import { AlbumIdDto } from './dto/album-id.dto'; | ||||
| import { UseValidation } from '../../decorators/use-validation.decorator'; | ||||
|  | ||||
| @ApiTags('Album') | ||||
| @Controller('album') | ||||
| @UseValidation() | ||||
| export class AlbumController { | ||||
|   constructor(private readonly albumService: AlbumService) {} | ||||
|  | ||||
| @@ -47,7 +37,8 @@ export class AlbumController { | ||||
|  | ||||
|   @Authenticated() | ||||
|   @Post() | ||||
|   async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) { | ||||
|   async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body() createAlbumDto: CreateAlbumDto) { | ||||
|     // TODO: Handle nonexistent sharedWithUserIds and assetIds. | ||||
|     return this.albumService.create(authUser, createAlbumDto); | ||||
|   } | ||||
|  | ||||
| @@ -55,9 +46,10 @@ export class AlbumController { | ||||
|   @Put('/:albumId/users') | ||||
|   async addUsersToAlbum( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) addUsersDto: AddUsersDto, | ||||
|     @Body() addUsersDto: AddUsersDto, | ||||
|     @Param() { albumId }: AlbumIdDto, | ||||
|   ) { | ||||
|     // TODO: Handle nonexistent sharedUserIds. | ||||
|     return this.albumService.addUsersToAlbum(authUser, addUsersDto, albumId); | ||||
|   } | ||||
|  | ||||
| @@ -65,9 +57,11 @@ export class AlbumController { | ||||
|   @Put('/:albumId/assets') | ||||
|   async addAssetsToAlbum( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) addAssetsDto: AddAssetsDto, | ||||
|     @Body() addAssetsDto: AddAssetsDto, | ||||
|     @Param() { albumId }: AlbumIdDto, | ||||
|   ): Promise<AddAssetsResponseDto> { | ||||
|     // TODO: Handle nonexistent assetIds. | ||||
|     // TODO: Disallow adding assets of another user to an album. | ||||
|     return this.albumService.addAssetsToAlbum(authUser, addAssetsDto, albumId); | ||||
|   } | ||||
|  | ||||
| @@ -81,7 +75,7 @@ export class AlbumController { | ||||
|   @Delete('/:albumId/assets') | ||||
|   async removeAssetFromAlbum( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) removeAssetsDto: RemoveAssetsDto, | ||||
|     @Body() removeAssetsDto: RemoveAssetsDto, | ||||
|     @Param() { albumId }: AlbumIdDto, | ||||
|   ): Promise<AlbumResponseDto> { | ||||
|     return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId); | ||||
| @@ -107,9 +101,11 @@ export class AlbumController { | ||||
|   @Patch('/:albumId') | ||||
|   async updateAlbumInfo( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) updateAlbumInfoDto: UpdateAlbumDto, | ||||
|     @Body() updateAlbumInfoDto: UpdateAlbumDto, | ||||
|     @Param() { albumId }: AlbumIdDto, | ||||
|   ) { | ||||
|     // TODO: Handle nonexistent albumThumbnailAssetId. | ||||
|     // TODO: Disallow setting asset from other user as albumThumbnailAssetId. | ||||
|     return this.albumService.updateAlbumInfo(authUser, updateAlbumInfoDto, albumId); | ||||
|   } | ||||
|  | ||||
| @@ -119,7 +115,7 @@ export class AlbumController { | ||||
|   async downloadArchive( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Param() { albumId }: AlbumIdDto, | ||||
|     @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, | ||||
|     @Query() dto: DownloadDto, | ||||
|     @Response({ passthrough: true }) res: Res, | ||||
|   ) { | ||||
|     this.albumService.checkDownloadAccess(authUser); | ||||
| @@ -140,7 +136,7 @@ export class AlbumController { | ||||
|   @Post('/create-shared-link') | ||||
|   async createAlbumSharedLink( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) createAlbumShareLinkDto: CreateAlbumSharedLinkDto, | ||||
|     @Body() createAlbumShareLinkDto: CreateAlbumSharedLinkDto, | ||||
|   ) { | ||||
|     return this.albumService.createAlbumSharedLink(authUser, createAlbumShareLinkDto); | ||||
|   } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { IsNotEmpty } from 'class-validator'; | ||||
| import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; | ||||
|  | ||||
| export class AddAssetsDto { | ||||
|   @IsNotEmpty() | ||||
|   @ValidateUUID({ each: true }) | ||||
|   assetIds!: string[]; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { IsNotEmpty } from 'class-validator'; | ||||
| import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; | ||||
|  | ||||
| export class AddUsersDto { | ||||
|   @IsNotEmpty() | ||||
|   @ValidateUUID({ each: true }) | ||||
|   sharedUserIds!: string[]; | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsNotEmpty, IsUUID } from 'class-validator'; | ||||
| import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; | ||||
|  | ||||
| export class AlbumIdDto { | ||||
|   @IsNotEmpty() | ||||
|   @IsUUID('4') | ||||
|   @ApiProperty({ format: 'uuid' }) | ||||
|   @ValidateUUID() | ||||
|   albumId!: string; | ||||
| } | ||||
|   | ||||
| @@ -1,27 +1,33 @@ | ||||
| import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; | ||||
| import { IsBoolean, IsISO8601, IsOptional, IsString } from 'class-validator'; | ||||
|  | ||||
| export class CreateAlbumShareLinkDto { | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   @ValidateUUID() | ||||
|   albumId!: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsISO8601() | ||||
|   @IsOptional() | ||||
|   @ApiProperty({ format: 'date-time' }) | ||||
|   expiresAt?: string; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @ApiProperty() | ||||
|   allowUpload?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @ApiProperty() | ||||
|   allowDownload?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @ApiProperty() | ||||
|   showExif?: boolean; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsOptional() | ||||
|   @ApiProperty() | ||||
|   description?: string; | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,16 @@ | ||||
| import { IsNotEmpty, IsOptional } from 'class-validator'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; | ||||
| import { IsNotEmpty, IsString } from 'class-validator'; | ||||
|  | ||||
| export class CreateAlbumDto { | ||||
|   @IsNotEmpty() | ||||
|   @IsString() | ||||
|   @ApiProperty() | ||||
|   albumName!: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @ValidateUUID({ optional: true, each: true }) | ||||
|   sharedWithUserIds?: string[]; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @ValidateUUID({ optional: true, each: true }) | ||||
|   assetIds?: string[]; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { IsNotEmpty } from 'class-validator'; | ||||
| import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; | ||||
|  | ||||
| export class RemoveAssetsDto { | ||||
|   @IsNotEmpty() | ||||
|   @ValidateUUID({ each: true }) | ||||
|   assetIds!: string[]; | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; | ||||
| import { IsOptional } from 'class-validator'; | ||||
|  | ||||
| export class UpdateAlbumDto { | ||||
|   @IsOptional() | ||||
|   @ApiProperty() | ||||
|   albumName?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @ValidateUUID({ optional: true }) | ||||
|   albumThumbnailAssetId?: string; | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { IsNumber, IsOptional, IsPositive, IsString } from 'class-validator'; | ||||
| export class DownloadDto { | ||||
|   @IsOptional() | ||||
|   @IsString() | ||||
|   name = ''; | ||||
|   name?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsPositive() | ||||
|   | ||||
							
								
								
									
										17
									
								
								server/apps/immich/src/decorators/validate-uuid.decorator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/apps/immich/src/decorators/validate-uuid.decorator.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { applyDecorators } from '@nestjs/common'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; | ||||
|  | ||||
| export type Options = { | ||||
|   optional?: boolean; | ||||
|   each?: boolean; | ||||
| }; | ||||
|  | ||||
| export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) { | ||||
|   return applyDecorators( | ||||
|     IsUUID('4', { each }), | ||||
|     ApiProperty({ format: 'uuid' }), | ||||
|     optional ? IsOptional() : IsNotEmpty(), | ||||
|     each ? IsArray() : IsString(), | ||||
|   ); | ||||
| } | ||||
| @@ -1895,6 +1895,14 @@ | ||||
|         "operationId": "downloadLibrary", | ||||
|         "description": "Current this is not used in any UI element", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "skip", | ||||
|             "required": false, | ||||
| @@ -3343,6 +3351,14 @@ | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "name", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "skip", | ||||
|             "required": false, | ||||
| @@ -5359,7 +5375,8 @@ | ||||
|           "assetIds": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|               "type": "string", | ||||
|               "format": "uuid" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
| @@ -5373,7 +5390,8 @@ | ||||
|           "assetIds": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|               "type": "string", | ||||
|               "format": "uuid" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
| @@ -5435,13 +5453,15 @@ | ||||
|           "sharedWithUserIds": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|               "type": "string", | ||||
|               "format": "uuid" | ||||
|             } | ||||
|           }, | ||||
|           "assetIds": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|               "type": "string", | ||||
|               "format": "uuid" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
| @@ -5455,7 +5475,8 @@ | ||||
|           "sharedUserIds": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|               "type": "string", | ||||
|               "format": "uuid" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
| @@ -5491,7 +5512,8 @@ | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "albumThumbnailAssetId": { | ||||
|             "type": "string" | ||||
|             "type": "string", | ||||
|             "format": "uuid" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| @@ -5499,10 +5521,12 @@ | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "albumId": { | ||||
|             "type": "string" | ||||
|             "type": "string", | ||||
|             "format": "uuid" | ||||
|           }, | ||||
|           "expiresAt": { | ||||
|             "type": "string" | ||||
|             "type": "string", | ||||
|             "format": "date-time" | ||||
|           }, | ||||
|           "allowUpload": { | ||||
|             "type": "boolean" | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional, IsUUID } from 'class-validator'; | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { toBoolean } from 'apps/immich/src/utils/transform.util'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; | ||||
|  | ||||
| export class GetAlbumsDto { | ||||
|   @IsOptional() | ||||
| @@ -20,8 +21,6 @@ export class GetAlbumsDto { | ||||
|    * Ignores the shared parameter | ||||
|    * undefined: get all albums | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @IsUUID(4) | ||||
|   @ApiProperty({ format: 'uuid' }) | ||||
|   @ValidateUUID({ optional: true }) | ||||
|   assetId?: string; | ||||
| } | ||||
|   | ||||
							
								
								
									
										44
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -3160,12 +3160,13 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} albumId  | ||||
|          * @param {string} [name]  | ||||
|          * @param {number} [skip]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         downloadArchive: async (albumId: string, skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         downloadArchive: async (albumId: string, name?: string, skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'albumId' is not null or undefined
 | ||||
|             assertParamExists('downloadArchive', 'albumId', albumId) | ||||
|             const localVarPath = `/album/{albumId}/download` | ||||
| @@ -3187,6 +3188,10 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
| 
 | ||||
|             // authentication cookie required
 | ||||
| 
 | ||||
|             if (name !== undefined) { | ||||
|                 localVarQueryParameter['name'] = name; | ||||
|             } | ||||
| 
 | ||||
|             if (skip !== undefined) { | ||||
|                 localVarQueryParameter['skip'] = skip; | ||||
|             } | ||||
| @@ -3529,13 +3534,14 @@ export const AlbumApiFp = function(configuration?: Configuration) { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} albumId  | ||||
|          * @param {string} [name]  | ||||
|          * @param {number} [skip]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async downloadArchive(albumId: string, skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.downloadArchive(albumId, skip, key, options); | ||||
|         async downloadArchive(albumId: string, name?: string, skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.downloadArchive(albumId, name, skip, key, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -3663,13 +3669,14 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} albumId  | ||||
|          * @param {string} [name]  | ||||
|          * @param {number} [skip]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         downloadArchive(albumId: string, skip?: number, key?: string, options?: any): AxiosPromise<any> { | ||||
|             return localVarFp.downloadArchive(albumId, skip, key, options).then((request) => request(axios, basePath)); | ||||
|         downloadArchive(albumId: string, name?: string, skip?: number, key?: string, options?: any): AxiosPromise<any> { | ||||
|             return localVarFp.downloadArchive(albumId, name, skip, key, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -3800,14 +3807,15 @@ export class AlbumApi extends BaseAPI { | ||||
|     /** | ||||
|      *  | ||||
|      * @param {string} albumId  | ||||
|      * @param {string} [name]  | ||||
|      * @param {number} [skip]  | ||||
|      * @param {string} [key]  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AlbumApi | ||||
|      */ | ||||
|     public downloadArchive(albumId: string, skip?: number, key?: string, options?: AxiosRequestConfig) { | ||||
|         return AlbumApiFp(this.configuration).downloadArchive(albumId, skip, key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     public downloadArchive(albumId: string, name?: string, skip?: number, key?: string, options?: AxiosRequestConfig) { | ||||
|         return AlbumApiFp(this.configuration).downloadArchive(albumId, name, skip, key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -4195,12 +4203,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|         }, | ||||
|         /** | ||||
|          * Current this is not used in any UI element | ||||
|          * @param {string} [name]  | ||||
|          * @param {number} [skip]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         downloadLibrary: async (skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         downloadLibrary: async (name?: string, skip?: number, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             const localVarPath = `/asset/download-library`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
| @@ -4219,6 +4228,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
| 
 | ||||
|             // authentication cookie required
 | ||||
| 
 | ||||
|             if (name !== undefined) { | ||||
|                 localVarQueryParameter['name'] = name; | ||||
|             } | ||||
| 
 | ||||
|             if (skip !== undefined) { | ||||
|                 localVarQueryParameter['skip'] = skip; | ||||
|             } | ||||
| @@ -5029,13 +5042,14 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|         }, | ||||
|         /** | ||||
|          * Current this is not used in any UI element | ||||
|          * @param {string} [name]  | ||||
|          * @param {number} [skip]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async downloadLibrary(skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.downloadLibrary(skip, key, options); | ||||
|         async downloadLibrary(name?: string, skip?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.downloadLibrary(name, skip, key, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -5284,13 +5298,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|         }, | ||||
|         /** | ||||
|          * Current this is not used in any UI element | ||||
|          * @param {string} [name]  | ||||
|          * @param {number} [skip]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         downloadLibrary(skip?: number, key?: string, options?: any): AxiosPromise<any> { | ||||
|             return localVarFp.downloadLibrary(skip, key, options).then((request) => request(axios, basePath)); | ||||
|         downloadLibrary(name?: string, skip?: number, key?: string, options?: any): AxiosPromise<any> { | ||||
|             return localVarFp.downloadLibrary(name, skip, key, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          * Get all AssetEntity belong to the user | ||||
| @@ -5537,14 +5552,15 @@ export class AssetApi extends BaseAPI { | ||||
| 
 | ||||
|     /** | ||||
|      * Current this is not used in any UI element | ||||
|      * @param {string} [name]  | ||||
|      * @param {number} [skip]  | ||||
|      * @param {string} [key]  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public downloadLibrary(skip?: number, key?: string, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).downloadLibrary(skip, key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     public downloadLibrary(name?: string, skip?: number, key?: string, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).downloadLibrary(name, skip, key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|   | ||||
| @@ -264,6 +264,7 @@ | ||||
|  | ||||
| 				const { data, status, headers } = await api.albumApi.downloadArchive( | ||||
| 					album.id, | ||||
| 					undefined, | ||||
| 					skip || undefined, | ||||
| 					sharedLink?.key, | ||||
| 					{ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user