1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

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 109 additions and 36 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

View File

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

View File

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