feat(server): improve API specification (#1853)

This commit is contained in:
Michel Heusschen 2023-02-24 17:01:10 +01:00 committed by GitHub
parent da9b9c8c69
commit 9323cc76d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1690 additions and 396 deletions

View file

@ -22,7 +22,7 @@ import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AlbumResponseDto } from '@app/domain';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
@ -37,7 +37,6 @@ import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/creat
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
@ApiBearerAuth()
@ApiTags('Album')
@Controller('album')
export class AlbumController {
@ -134,12 +133,13 @@ export class AlbumController {
@Authenticated({ isShared: true })
@Get('/:albumId/download')
@ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } })
async downloadArchive(
@GetAuthUser() authUser: AuthUserDto,
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
@Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
@Response({ passthrough: true }) res: Res,
): Promise<any> {
) {
this.albumService.checkDownloadAccess(authUser);
const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive(

View file

@ -28,7 +28,7 @@ import { Response as Res } from 'express';
import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetResponseDto, ImmichReadStream } from '@app/domain';
@ -62,7 +62,6 @@ function asStreamableFile({ stream, type, length }: ImmichReadStream) {
return new StreamableFile(stream, { type, length });
}
@ApiBearerAuth()
@ApiTags('Asset')
@Controller('asset')
export class AssetController {
@ -108,21 +107,23 @@ export class AssetController {
@Authenticated({ isShared: true })
@Get('/download/:assetId')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async downloadFile(
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res,
@Param('assetId') assetId: string,
): Promise<any> {
) {
return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile);
}
@Authenticated({ isShared: true })
@Post('/download-files')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async downloadFiles(
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res,
@Body(new ValidationPipe()) dto: DownloadFilesDto,
): Promise<any> {
) {
this.assetService.checkDownloadAccess(authUser);
await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]);
const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto);
@ -138,11 +139,12 @@ export class AssetController {
*/
@Authenticated({ isShared: true })
@Get('/download-library')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async downloadLibrary(
@GetAuthUser() authUser: AuthUserDto,
@Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
@Response({ passthrough: true }) res: Res,
): Promise<any> {
) {
this.assetService.checkDownloadAccess(authUser);
const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadLibrary(authUser, dto);
res.attachment(fileName);
@ -155,13 +157,14 @@ export class AssetController {
@Authenticated({ isShared: true })
@Get('/file/:assetId')
@Header('Cache-Control', 'max-age=31536000')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async serveFile(
@GetAuthUser() authUser: AuthUserDto,
@Headers() headers: Record<string, string>,
@Response({ passthrough: true }) res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param('assetId') assetId: string,
): Promise<any> {
) {
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.serveFile(authUser, assetId, query, res, headers);
}
@ -169,13 +172,14 @@ export class AssetController {
@Authenticated({ isShared: true })
@Get('/thumbnail/:assetId')
@Header('Cache-Control', 'max-age=31536000')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async getAssetThumbnail(
@GetAuthUser() authUser: AuthUserDto,
@Headers() headers: Record<string, string>,
@Response({ passthrough: true }) res: Res,
@Param('assetId') assetId: string,
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
): Promise<any> {
) {
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.getAssetThumbnail(assetId, query, res, headers);
}

View file

@ -1,5 +1,5 @@
import { Body, Controller, Get, Param, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto';
import { GetJobDto } from './dto/get-job.dto';
@ -8,7 +8,6 @@ import { JobCommandDto } from './dto/job-command.dto';
@Authenticated({ admin: true })
@ApiTags('Job')
@ApiBearerAuth()
@Controller('jobs')
export class JobController {
constructor(private readonly jobService: JobService) {}

View file

@ -14,7 +14,7 @@ import {
ValidateAccessTokenResponseDto,
} from '@app/domain';
import { Body, Controller, Ip, Post, Req, Res, ValidationPipe } from '@nestjs/common';
import { ApiBadRequestResponse, ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { GetAuthUser } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator';
@ -45,7 +45,6 @@ export class AuthController {
}
@Authenticated()
@ApiBearerAuth()
@Post('validateToken')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validateAccessToken(@GetAuthUser() authUser: AuthUserDto): ValidateAccessTokenResponseDto {
@ -53,7 +52,6 @@ export class AuthController {
}
@Authenticated()
@ApiBearerAuth()
@Post('change-password')
async changePassword(@GetAuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.authService.changePassword(authUser, dto);

View file

@ -5,12 +5,11 @@ import {
UpsertDeviceInfoDto as UpsertDto,
} from '@app/domain';
import { Body, Controller, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { GetAuthUser } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator';
@Authenticated()
@ApiBearerAuth()
@ApiTags('Device Info')
@Controller('device-info')
export class DeviceInfoController {

View file

@ -1,10 +1,9 @@
import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain';
import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { Authenticated } from '../decorators/authenticated.decorator';
@ApiTags('System Config')
@ApiBearerAuth()
@Authenticated({ admin: true })
@Controller('system-config')
export class SystemConfigController {

View file

@ -23,7 +23,7 @@ import { UpdateUserDto } from '@app/domain';
import { FileInterceptor } from '@nestjs/platform-express';
import { profileImageUploadOption } from '../config/profile-image-upload.config';
import { Response as Res } from 'express';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { UserResponseDto } from '@app/domain';
import { UserCountResponseDto } from '@app/domain';
import { CreateProfileImageDto } from '@app/domain';
@ -36,7 +36,6 @@ export class UserController {
constructor(private readonly userService: UserService) {}
@Authenticated()
@ApiBearerAuth()
@Get()
async getAllUsers(
@GetAuthUser() authUser: AuthUserDto,
@ -51,14 +50,12 @@ export class UserController {
}
@Authenticated()
@ApiBearerAuth()
@Get('me')
async getMyUserInfo(@GetAuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
return await this.userService.getUserInfo(authUser);
}
@Authenticated({ admin: true })
@ApiBearerAuth()
@Post()
async createUser(
@Body(new ValidationPipe({ transform: true })) createUserDto: CreateUserDto,
@ -72,21 +69,18 @@ export class UserController {
}
@Authenticated({ admin: true })
@ApiBearerAuth()
@Delete('/:userId')
async deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
return await this.userService.deleteUser(authUser, userId);
}
@Authenticated({ admin: true })
@ApiBearerAuth()
@Post('/:userId/restore')
async restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
return await this.userService.restoreUser(authUser, userId);
}
@Authenticated()
@ApiBearerAuth()
@Put()
async updateUser(
@GetAuthUser() authUser: AuthUserDto,
@ -97,7 +91,6 @@ export class UserController {
@UseInterceptors(FileInterceptor('file', profileImageUploadOption))
@Authenticated()
@ApiBearerAuth()
@ApiConsumes('multipart/form-data')
@ApiBody({
description: 'A new avatar for the user',

View file

@ -1,4 +1,5 @@
import { applyDecorators, SetMetadata } from '@nestjs/common';
import { ApiBearerAuth, ApiCookieAuth, ApiQuery } from '@nestjs/swagger';
interface AuthenticatedOptions {
admin?: boolean;
@ -12,7 +13,7 @@ export enum Metadata {
}
export const Authenticated = (options?: AuthenticatedOptions) => {
const decorators = [SetMetadata(Metadata.AUTH_ROUTE, true)];
const decorators: MethodDecorator[] = [ApiBearerAuth(), ApiCookieAuth(), SetMetadata(Metadata.AUTH_ROUTE, true)];
options = options || {};
@ -22,6 +23,7 @@ export const Authenticated = (options?: AuthenticatedOptions) => {
if (options.isShared) {
decorators.push(SetMetadata(Metadata.SHARED_ROUTE, true));
decorators.push(ApiQuery({ name: 'key', type: String, required: false }));
}
return applyDecorators(...decorators);

View file

@ -11,6 +11,7 @@ import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
import { json } from 'body-parser';
import { patchOpenAPI } from './utils/patch-open-api.util';
import { getLogLevels, MACHINE_LEARNING_ENABLED } from '@app/common';
import { IMMICH_ACCESS_COOKIE } from '@app/domain';
const logger = new Logger('ImmichServer');
@ -42,6 +43,7 @@ async function bootstrap() {
scheme: 'Bearer',
in: 'header',
})
.addCookieAuth(IMMICH_ACCESS_COOKIE)
.addServer('/api')
.build();