1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-28 09:33:27 +02: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:
Michel Heusschen 2023-04-06 19:50:55 +02:00 committed by GitHub
parent b03ce897c7
commit 8e3a7caebd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 129 additions and 66 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.

Binary file not shown.

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

View File

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

View File

@ -264,6 +264,7 @@
const { data, status, headers } = await api.albumApi.downloadArchive(
album.id,
undefined,
skip || undefined,
sharedLink?.key,
{