mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
refactor(server): more consistent param validation (#2166)
This commit is contained in:
parent
ad680b6a35
commit
d5f2e3e45c
18 changed files with 136 additions and 63 deletions
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
9
server/apps/immich/src/api-v1/album/dto/album-id.dto.ts
Normal file
9
server/apps/immich/src/api-v1/album/dto/album-id.dto.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
9
server/apps/immich/src/api-v1/asset/dto/asset-id.dto.ts
Normal file
9
server/apps/immich/src/api-v1/asset/dto/asset-id.dto.ts
Normal 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;
|
||||
}
|
||||
9
server/apps/immich/src/api-v1/asset/dto/device-id.dto.ts
Normal file
9
server/apps/immich/src/api-v1/asset/dto/device-id.dto.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
server/apps/immich/src/controllers/dto/uuid-param.dto.ts
Normal file
9
server/apps/immich/src/controllers/dto/uuid-param.dto.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue