mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(server): improve API specification (#1853)
This commit is contained in:
parent
da9b9c8c69
commit
9323cc76d9
41 changed files with 1690 additions and 396 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue