From d5f2e3e45cdc96f504f6328ac51ace381d0437f6 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed, 5 Apr 2023 00:24:08 +0200 Subject: [PATCH] refactor(server): more consistent param validation (#2166) --- mobile/openapi/doc/APIKeyApi.md | 6 ++--- mobile/openapi/doc/AlbumApi.md | 16 +++++------ mobile/openapi/doc/AssetApi.md | 12 ++++----- mobile/openapi/doc/ShareApi.md | 6 ++--- mobile/openapi/doc/TagApi.md | 6 ++--- mobile/openapi/doc/UserApi.md | 8 +++--- .../src/api-v1/album/album.controller.ts | 26 +++++++----------- .../src/api-v1/album/dto/album-id.dto.ts | 9 +++++++ .../src/api-v1/asset/asset.controller.ts | 14 +++++----- .../src/api-v1/asset/dto/asset-id.dto.ts | 9 +++++++ .../src/api-v1/asset/dto/device-id.dto.ts | 9 +++++++ .../immich/src/api-v1/tag/tag.controller.ts | 7 ++--- .../src/controllers/api-key.controller.ts | 7 ++--- .../src/controllers/dto/uuid-param.dto.ts | 9 +++++++ .../src/controllers/share.controller.ts | 10 ++++--- .../immich/src/controllers/user.controller.ts | 9 ++++--- server/immich-openapi-specs.json | 27 +++++++++++++++++++ .../libs/domain/src/user/dto/user-id.dto.ts | 9 +++++++ 18 files changed, 136 insertions(+), 63 deletions(-) create mode 100644 server/apps/immich/src/api-v1/album/dto/album-id.dto.ts create mode 100644 server/apps/immich/src/api-v1/asset/dto/asset-id.dto.ts create mode 100644 server/apps/immich/src/api-v1/asset/dto/device-id.dto.ts create mode 100644 server/apps/immich/src/controllers/dto/uuid-param.dto.ts create mode 100644 server/libs/domain/src/user/dto/user-id.dto.ts diff --git a/mobile/openapi/doc/APIKeyApi.md b/mobile/openapi/doc/APIKeyApi.md index ecf2fcc674..4bb570bfe1 100644 --- a/mobile/openapi/doc/APIKeyApi.md +++ b/mobile/openapi/doc/APIKeyApi.md @@ -91,7 +91,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = APIKeyApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { api_instance.deleteKey(id); @@ -143,7 +143,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = APIKeyApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.getKey(id); @@ -245,7 +245,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = APIKeyApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final aPIKeyUpdateDto = APIKeyUpdateDto(); // APIKeyUpdateDto | try { diff --git a/mobile/openapi/doc/AlbumApi.md b/mobile/openapi/doc/AlbumApi.md index c51909bace..72cb3392ed 100644 --- a/mobile/openapi/doc/AlbumApi.md +++ b/mobile/openapi/doc/AlbumApi.md @@ -45,7 +45,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AlbumApi(); -final albumId = albumId_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final addAssetsDto = AddAssetsDto(); // AddAssetsDto | final key = key_example; // String | @@ -102,7 +102,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AlbumApi(); -final albumId = albumId_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final addUsersDto = AddUsersDto(); // AddUsersDto | try { @@ -263,7 +263,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AlbumApi(); -final albumId = albumId_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { api_instance.deleteAlbum(albumId); @@ -315,7 +315,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AlbumApi(); -final albumId = albumId_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final skip = 8.14; // num | final key = key_example; // String | @@ -421,7 +421,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AlbumApi(); -final albumId = albumId_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final key = key_example; // String | try { @@ -531,7 +531,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AlbumApi(); -final albumId = albumId_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto | try { @@ -586,7 +586,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AlbumApi(); -final albumId = albumId_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final userId = userId_example; // String | try { @@ -640,7 +640,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AlbumApi(); -final albumId = albumId_example; // String | +final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final updateAlbumDto = UpdateAlbumDto(); // UpdateAlbumDto | try { diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index e1a4165b3b..53208634b3 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -325,7 +325,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AssetApi(); -final assetId = assetId_example; // String | +final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final key = key_example; // String | try { @@ -547,7 +547,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AssetApi(); -final assetId = assetId_example; // String | +final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final key = key_example; // String | try { @@ -806,7 +806,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AssetApi(); -final assetId = assetId_example; // String | +final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final format = ; // ThumbnailFormat | final key = key_example; // String | @@ -961,7 +961,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AssetApi(); -final deviceId = deviceId_example; // String | +final deviceId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.getUserAssetsByDeviceId(deviceId); @@ -1122,7 +1122,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AssetApi(); -final assetId = assetId_example; // String | +final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final isThumb = true; // bool | final isWeb = true; // bool | final key = key_example; // String | @@ -1181,7 +1181,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = AssetApi(); -final assetId = assetId_example; // String | +final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final updateAssetDto = UpdateAssetDto(); // UpdateAssetDto | try { diff --git a/mobile/openapi/doc/ShareApi.md b/mobile/openapi/doc/ShareApi.md index a0673dd28d..70ceafff32 100644 --- a/mobile/openapi/doc/ShareApi.md +++ b/mobile/openapi/doc/ShareApi.md @@ -38,7 +38,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = ShareApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final editSharedLinkDto = EditSharedLinkDto(); // EditSharedLinkDto | try { @@ -195,7 +195,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = ShareApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.getSharedLinkById(id); @@ -248,7 +248,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = ShareApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { api_instance.removeSharedLink(id); diff --git a/mobile/openapi/doc/TagApi.md b/mobile/openapi/doc/TagApi.md index bdf66dcfef..2fc3a48ac3 100644 --- a/mobile/openapi/doc/TagApi.md +++ b/mobile/openapi/doc/TagApi.md @@ -91,7 +91,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = TagApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { api_instance.delete(id); @@ -192,7 +192,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = TagApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.findOne(id); @@ -245,7 +245,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = TagApi(); -final id = id_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final updateTagDto = UpdateTagDto(); // UpdateTagDto | try { diff --git a/mobile/openapi/doc/UserApi.md b/mobile/openapi/doc/UserApi.md index 1982ea5106..0bd35a904c 100644 --- a/mobile/openapi/doc/UserApi.md +++ b/mobile/openapi/doc/UserApi.md @@ -149,7 +149,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = UserApi(); -final userId = userId_example; // String | +final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.deleteUser(userId); @@ -304,7 +304,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = UserApi(); -final userId = userId_example; // String | +final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.getProfileImage(userId); @@ -357,7 +357,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = UserApi(); -final userId = userId_example; // String | +final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.getUserById(userId); @@ -453,7 +453,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; final api_instance = UserApi(); -final userId = userId_example; // String | +final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { final result = api_instance.restoreUser(userId); diff --git a/server/apps/immich/src/api-v1/album/album.controller.ts b/server/apps/immich/src/api-v1/album/album.controller.ts index cf94e9e6ac..d204f44afd 100644 --- a/server/apps/immich/src/api-v1/album/album.controller.ts +++ b/server/apps/immich/src/api-v1/album/album.controller.ts @@ -7,7 +7,6 @@ import { Param, Delete, ValidationPipe, - ParseUUIDPipe, Put, Query, Response, @@ -33,8 +32,7 @@ import { } from '../../constants/download.constant'; import { DownloadDto } from '../asset/dto/download-library.dto'; import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/create-album-shared-link.dto'; - -// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe. +import { AlbumIdDto } from './dto/album-id.dto'; @ApiTags('Album') @Controller('album') @@ -58,7 +56,7 @@ export class AlbumController { async addUsersToAlbum( @GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) addUsersDto: AddUsersDto, - @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, + @Param() { albumId }: AlbumIdDto, ) { return this.albumService.addUsersToAlbum(authUser, addUsersDto, albumId); } @@ -68,17 +66,14 @@ export class AlbumController { async addAssetsToAlbum( @GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) addAssetsDto: AddAssetsDto, - @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, + @Param() { albumId }: AlbumIdDto, ): Promise { return this.albumService.addAssetsToAlbum(authUser, addAssetsDto, albumId); } @Authenticated({ isShared: true }) @Get('/:albumId') - async getAlbumInfo( - @GetAuthUser() authUser: AuthUserDto, - @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, - ) { + async getAlbumInfo(@GetAuthUser() authUser: AuthUserDto, @Param() { albumId }: AlbumIdDto) { return this.albumService.getAlbumInfo(authUser, albumId); } @@ -87,17 +82,14 @@ export class AlbumController { async removeAssetFromAlbum( @GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) removeAssetsDto: RemoveAssetsDto, - @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, + @Param() { albumId }: AlbumIdDto, ): Promise { return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId); } @Authenticated() @Delete('/:albumId') - async deleteAlbum( - @GetAuthUser() authUser: AuthUserDto, - @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, - ) { + async deleteAlbum(@GetAuthUser() authUser: AuthUserDto, @Param() { albumId }: AlbumIdDto) { return this.albumService.deleteAlbum(authUser, albumId); } @@ -105,7 +97,7 @@ export class AlbumController { @Delete('/:albumId/user/:userId') async removeUserFromAlbum( @GetAuthUser() authUser: AuthUserDto, - @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, + @Param() { albumId }: AlbumIdDto, @Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string, ) { return this.albumService.removeUserFromAlbum(authUser, albumId, userId); @@ -116,7 +108,7 @@ export class AlbumController { async updateAlbumInfo( @GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) updateAlbumInfoDto: UpdateAlbumDto, - @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, + @Param() { albumId }: AlbumIdDto, ) { return this.albumService.updateAlbumInfo(authUser, updateAlbumInfoDto, albumId); } @@ -126,7 +118,7 @@ export class AlbumController { @ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } }) async downloadArchive( @GetAuthUser() authUser: AuthUserDto, - @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, + @Param() { albumId }: AlbumIdDto, @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, @Response({ passthrough: true }) res: Res, ) { diff --git a/server/apps/immich/src/api-v1/album/dto/album-id.dto.ts b/server/apps/immich/src/api-v1/album/dto/album-id.dto.ts new file mode 100644 index 0000000000..c9bf09e328 --- /dev/null +++ b/server/apps/immich/src/api-v1/album/dto/album-id.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class AlbumIdDto { + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + albumId!: string; +} diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index d63e99f83c..0c14386157 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -57,6 +57,8 @@ import { AssetSearchDto } from './dto/asset-search.dto'; import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; import FileNotEmptyValidator from '../validation/file-not-empty-validator'; import { RemoveAssetsDto } from '../album/dto/remove-assets.dto'; +import { AssetIdDto } from './dto/asset-id.dto'; +import { DeviceIdDto } from './dto/device-id.dto'; function asStreamableFile({ stream, type, length }: ImmichReadStream) { return new StreamableFile(stream, { type, length }); @@ -111,7 +113,7 @@ export class AssetController { async downloadFile( @GetAuthUser() authUser: AuthUserDto, @Response({ passthrough: true }) res: Res, - @Param('assetId') assetId: string, + @Param() { assetId }: AssetIdDto, ) { return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile); } @@ -163,7 +165,7 @@ export class AssetController { @Headers() headers: Record, @Response({ passthrough: true }) res: Res, @Query(new ValidationPipe({ transform: true })) query: ServeFileDto, - @Param('assetId') assetId: string, + @Param() { assetId }: AssetIdDto, ) { await this.assetService.checkAssetsAccess(authUser, [assetId]); return this.assetService.serveFile(authUser, assetId, query, res, headers); @@ -177,7 +179,7 @@ export class AssetController { @GetAuthUser() authUser: AuthUserDto, @Headers() headers: Record, @Response({ passthrough: true }) res: Res, - @Param('assetId') assetId: string, + @Param() { assetId }: AssetIdDto, @Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto, ) { await this.assetService.checkAssetsAccess(authUser, [assetId]); @@ -258,7 +260,7 @@ export class AssetController { */ @Authenticated() @Get('/:deviceId') - async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) { + async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId); } @@ -269,7 +271,7 @@ export class AssetController { @Get('/assetById/:assetId') async getAssetById( @GetAuthUser() authUser: AuthUserDto, - @Param('assetId') assetId: string, + @Param() { assetId }: AssetIdDto, ): Promise { await this.assetService.checkAssetsAccess(authUser, [assetId]); return await this.assetService.getAssetById(authUser, assetId); @@ -282,7 +284,7 @@ export class AssetController { @Put('/:assetId') async updateAsset( @GetAuthUser() authUser: AuthUserDto, - @Param('assetId') assetId: string, + @Param() { assetId }: AssetIdDto, @Body(ValidationPipe) dto: UpdateAssetDto, ): Promise { await this.assetService.checkAssetsAccess(authUser, [assetId], true); diff --git a/server/apps/immich/src/api-v1/asset/dto/asset-id.dto.ts b/server/apps/immich/src/api-v1/asset/dto/asset-id.dto.ts new file mode 100644 index 0000000000..5404878382 --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/dto/asset-id.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class AssetIdDto { + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + assetId!: string; +} diff --git a/server/apps/immich/src/api-v1/asset/dto/device-id.dto.ts b/server/apps/immich/src/api-v1/asset/dto/device-id.dto.ts new file mode 100644 index 0000000000..ff2f4163b5 --- /dev/null +++ b/server/apps/immich/src/api-v1/asset/dto/device-id.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class DeviceIdDto { + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + deviceId!: string; +} diff --git a/server/apps/immich/src/api-v1/tag/tag.controller.ts b/server/apps/immich/src/api-v1/tag/tag.controller.ts index b02b222276..d769b157aa 100644 --- a/server/apps/immich/src/api-v1/tag/tag.controller.ts +++ b/server/apps/immich/src/api-v1/tag/tag.controller.ts @@ -6,6 +6,7 @@ import { Authenticated } from '../../decorators/authenticated.decorator'; import { ApiTags } from '@nestjs/swagger'; import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; import { mapTag, TagResponseDto } from '@app/domain'; +import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto'; @Authenticated() @ApiTags('Tag') @@ -27,7 +28,7 @@ export class TagController { } @Get(':id') - async findOne(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise { + async findOne(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { const tag = await this.tagService.findOne(authUser, id); return mapTag(tag); } @@ -35,14 +36,14 @@ export class TagController { @Patch(':id') update( @GetAuthUser() authUser: AuthUserDto, - @Param('id') id: string, + @Param() { id }: UUIDParamDto, @Body(ValidationPipe) updateTagDto: UpdateTagDto, ): Promise { return this.tagService.update(authUser, id, updateTagDto); } @Delete(':id') - delete(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise { + delete(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.tagService.remove(authUser, id); } } diff --git a/server/apps/immich/src/controllers/api-key.controller.ts b/server/apps/immich/src/controllers/api-key.controller.ts index d666bd4272..e025b248f6 100644 --- a/server/apps/immich/src/controllers/api-key.controller.ts +++ b/server/apps/immich/src/controllers/api-key.controller.ts @@ -11,6 +11,7 @@ import { ApiTags } from '@nestjs/swagger'; import { GetAuthUser } from '../decorators/auth-user.decorator'; import { Authenticated } from '../decorators/authenticated.decorator'; import { UseValidation } from '../decorators/use-validation.decorator'; +import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('API Key') @Controller('api-key') @@ -30,21 +31,21 @@ export class APIKeyController { } @Get(':id') - getKey(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise { + getKey(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(authUser, id); } @Put(':id') updateKey( @GetAuthUser() authUser: AuthUserDto, - @Param('id') id: string, + @Param() { id }: UUIDParamDto, @Body() dto: APIKeyUpdateDto, ): Promise { return this.service.update(authUser, id, dto); } @Delete(':id') - deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise { + deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(authUser, id); } } diff --git a/server/apps/immich/src/controllers/dto/uuid-param.dto.ts b/server/apps/immich/src/controllers/dto/uuid-param.dto.ts new file mode 100644 index 0000000000..6e1b5a36cc --- /dev/null +++ b/server/apps/immich/src/controllers/dto/uuid-param.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class UUIDParamDto { + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + id!: string; +} diff --git a/server/apps/immich/src/controllers/share.controller.ts b/server/apps/immich/src/controllers/share.controller.ts index 72b0926d2b..aef3dad793 100644 --- a/server/apps/immich/src/controllers/share.controller.ts +++ b/server/apps/immich/src/controllers/share.controller.ts @@ -4,6 +4,7 @@ import { ApiTags } from '@nestjs/swagger'; import { GetAuthUser } from '../decorators/auth-user.decorator'; import { Authenticated } from '../decorators/authenticated.decorator'; import { UseValidation } from '../decorators/use-validation.decorator'; +import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('share') @Controller('share') @@ -25,13 +26,16 @@ export class ShareController { @Authenticated() @Get(':id') - getSharedLinkById(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise { + getSharedLinkById( + @GetAuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + ): Promise { return this.service.getById(authUser, id, true); } @Authenticated() @Delete(':id') - removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise { + removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(authUser, id); } @@ -39,7 +43,7 @@ export class ShareController { @Patch(':id') editSharedLink( @GetAuthUser() authUser: AuthUserDto, - @Param('id') id: string, + @Param() { id }: UUIDParamDto, @Body() dto: EditSharedLinkDto, ): Promise { return this.service.edit(authUser, id, dto); diff --git a/server/apps/immich/src/controllers/user.controller.ts b/server/apps/immich/src/controllers/user.controller.ts index 9e7bd311fe..c1da0427aa 100644 --- a/server/apps/immich/src/controllers/user.controller.ts +++ b/server/apps/immich/src/controllers/user.controller.ts @@ -28,6 +28,7 @@ import { CreateProfileImageDto } from '@app/domain'; import { CreateProfileImageResponseDto } from '@app/domain'; import { UserCountDto } from '@app/domain'; import { UseValidation } from '../decorators/use-validation.decorator'; +import { UserIdDto } from '@app/domain/user/dto/user-id.dto'; @ApiTags('User') @Controller('user') @@ -43,7 +44,7 @@ export class UserController { @Authenticated() @Get('/info/:userId') - getUserById(@Param('userId') userId: string): Promise { + getUserById(@Param() { userId }: UserIdDto): Promise { return this.service.getUserById(userId); } @@ -66,13 +67,13 @@ export class UserController { @Authenticated({ admin: true }) @Delete('/:userId') - deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise { + deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param() { userId }: UserIdDto): Promise { return this.service.deleteUser(authUser, userId); } @Authenticated({ admin: true }) @Post('/:userId/restore') - restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise { + restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param() { userId }: UserIdDto): Promise { return this.service.restoreUser(authUser, userId); } @@ -100,7 +101,7 @@ export class UserController { @Authenticated() @Get('/profile-image/:userId') @Header('Cache-Control', 'max-age=600') - async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: Res): Promise { + async getProfileImage(@Param() { userId }: UserIdDto, @Response({ passthrough: true }) res: Res): Promise { const readableStream = await this.service.getUserProfileImage(userId); res.header('Content-Type', 'image/jpeg'); return new StreamableFile(readableStream); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 3eb8159577..147a7f733f 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -172,6 +172,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -209,6 +210,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -256,6 +258,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -1117,6 +1120,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -1154,6 +1158,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -1184,6 +1189,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -1479,6 +1485,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -1580,6 +1587,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -1619,6 +1627,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -1699,6 +1708,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -1788,6 +1798,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } }, @@ -1955,6 +1966,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } }, @@ -2003,6 +2015,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } }, @@ -2414,6 +2427,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -2456,6 +2470,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } }, @@ -2503,6 +2518,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -2850,6 +2866,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -2887,6 +2904,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -2934,6 +2952,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -2996,6 +3015,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -3045,6 +3065,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } }, @@ -3100,6 +3121,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -3149,6 +3171,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } }, @@ -3194,6 +3217,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -3224,6 +3248,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } } @@ -3273,6 +3298,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } }, @@ -3313,6 +3339,7 @@ "required": true, "in": "path", "schema": { + "format": "uuid", "type": "string" } }, diff --git a/server/libs/domain/src/user/dto/user-id.dto.ts b/server/libs/domain/src/user/dto/user-id.dto.ts new file mode 100644 index 0000000000..ce23581e37 --- /dev/null +++ b/server/libs/domain/src/user/dto/user-id.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class UserIdDto { + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + userId!: string; +}