refactor(server): more consistent param validation (#2166)

This commit is contained in:
Michel Heusschen 2023-04-05 00:24:08 +02:00 committed by GitHub
parent ad680b6a35
commit d5f2e3e45c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 136 additions and 63 deletions

View file

@ -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<AddAssetsResponseDto> {
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<AlbumResponseDto> {
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,
) {

View file

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

View file

@ -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<string, string>,
@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<string, string>,
@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<AssetResponseDto> {
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<AssetResponseDto> {
await this.assetService.checkAssetsAccess(authUser, [assetId], true);

View file

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

View file

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

View file

@ -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<TagResponseDto> {
async findOne(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
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<TagResponseDto> {
return this.tagService.update(authUser, id, updateTagDto);
}
@Delete(':id')
delete(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise<void> {
delete(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.tagService.remove(authUser, id);
}
}

View file

@ -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<APIKeyResponseDto> {
getKey(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(authUser, id);
}
@Put(':id')
updateKey(
@GetAuthUser() authUser: AuthUserDto,
@Param('id') id: string,
@Param() { id }: UUIDParamDto,
@Body() dto: APIKeyUpdateDto,
): Promise<APIKeyResponseDto> {
return this.service.update(authUser, id, dto);
}
@Delete(':id')
deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise<void> {
deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id);
}
}

View file

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

View file

@ -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<SharedLinkResponseDto> {
getSharedLinkById(
@GetAuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
): Promise<SharedLinkResponseDto> {
return this.service.getById(authUser, id, true);
}
@Authenticated()
@Delete(':id')
removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise<void> {
removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
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<SharedLinkResponseDto> {
return this.service.edit(authUser, id, dto);

View file

@ -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<UserResponseDto> {
getUserById(@Param() { userId }: UserIdDto): Promise<UserResponseDto> {
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<UserResponseDto> {
deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param() { userId }: UserIdDto): Promise<UserResponseDto> {
return this.service.deleteUser(authUser, userId);
}
@Authenticated({ admin: true })
@Post('/:userId/restore')
restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param() { userId }: UserIdDto): Promise<UserResponseDto> {
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<any> {
async getProfileImage(@Param() { userId }: UserIdDto, @Response({ passthrough: true }) res: Res): Promise<any> {
const readableStream = await this.service.getUserProfileImage(userId);
res.header('Content-Type', 'image/jpeg');
return new StreamableFile(readableStream);