feat(server): improve validation of albums (#2188)

* feat(server): improve validation of albums

* regenerate openapi + fix downloadArchive for web
This commit is contained in:
Michel Heusschen
2023-04-06 19:50:55 +02:00
committed by GitHub
parent b03ce897c7
commit 8e3a7caebd
23 changed files with 164 additions and 83 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { IsNumber, IsOptional, IsPositive, IsString } from 'class-validator';
export class DownloadDto {
@IsOptional()
@IsString()
name = '';
name?: string;
@IsOptional()
@IsPositive()

View 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(),
);
}

View File

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

View File

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