mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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:
		@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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[];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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[];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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[];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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[];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { IsNumber, IsOptional, IsPositive, IsString } from 'class-validator';
 | 
			
		||||
export class DownloadDto {
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  name = '';
 | 
			
		||||
  name?: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsPositive()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								server/apps/immich/src/decorators/validate-uuid.decorator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/apps/immich/src/decorators/validate-uuid.decorator.ts
									
									
									
									
									
										Normal 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(),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user