Browse Source

refactor(server): auth decorator (#2588)

Jason Rasmussen 2 years ago
parent
commit
ffe397247e

+ 5 - 9
server/apps/immich/src/api-v1/album/album.controller.ts

@@ -1,7 +1,7 @@
 import { Controller, Get, Post, Body, Param, Delete, Put, Query, Response } from '@nestjs/common';
 import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
 import { AlbumService } from './album.service';
-import { Authenticated } from '../../decorators/authenticated.decorator';
+import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator';
 import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
 import { AddAssetsDto } from './dto/add-assets.dto';
 import { AddUsersDto } from './dto/add-users.dto';
@@ -32,24 +32,23 @@ const handleDownload = (download: DownloadArchive, res: Res) => {
 
 @ApiTags('Album')
 @Controller('album')
+@Authenticated()
 @UseValidation()
 export class AlbumController {
   constructor(private readonly service: AlbumService) {}
 
-  @Authenticated()
   @Get('count-by-user-id')
   getAlbumCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
     return this.service.getCountByUserId(authUser);
   }
 
-  @Authenticated()
   @Put(':id/users')
   addUsersToAlbum(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: AddUsersDto) {
     // TODO: Handle nonexistent sharedUserIds.
     return this.service.addUsers(authUser, id, dto);
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Put(':id/assets')
   addAssetsToAlbum(
     @GetAuthUser() authUser: AuthUserDto,
@@ -61,13 +60,12 @@ export class AlbumController {
     return this.service.addAssets(authUser, id, dto);
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Get(':id')
   getAlbumInfo(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
     return this.service.get(authUser, id);
   }
 
-  @Authenticated()
   @Delete(':id/assets')
   removeAssetFromAlbum(
     @GetAuthUser() authUser: AuthUserDto,
@@ -77,7 +75,6 @@ export class AlbumController {
     return this.service.removeAssets(authUser, id, dto);
   }
 
-  @Authenticated()
   @Delete(':id/user/:userId')
   removeUserFromAlbum(
     @GetAuthUser() authUser: AuthUserDto,
@@ -87,7 +84,7 @@ export class AlbumController {
     return this.service.removeUser(authUser, id, userId);
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Get(':id/download')
   @ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } })
   downloadArchive(
@@ -100,7 +97,6 @@ export class AlbumController {
     return this.service.downloadArchive(authUser, id, dto).then((download) => handleDownload(download, res));
   }
 
-  @Authenticated()
   @Post('create-shared-link')
   createAlbumSharedLink(@GetAuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumSharedLinkDto) {
     return this.service.createSharedLink(authUser, dto);

+ 12 - 26
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -19,7 +19,7 @@ import {
   StreamableFile,
   ParseFilePipe,
 } from '@nestjs/common';
-import { Authenticated } from '../../decorators/authenticated.decorator';
+import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator';
 import { AssetService } from './asset.service';
 import { FileFieldsInterceptor } from '@nestjs/platform-express';
 import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
@@ -68,10 +68,11 @@ function asStreamableFile({ stream, type, length }: ImmichReadStream) {
 
 @ApiTags('Asset')
 @Controller('asset')
+@Authenticated()
 export class AssetController {
   constructor(private assetService: AssetService) {}
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Post('upload')
   @UseInterceptors(
     FileFieldsInterceptor(
@@ -116,7 +117,7 @@ export class AssetController {
     return responseDto;
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Get('/download/:assetId')
   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
   async downloadFile(
@@ -127,7 +128,7 @@ export class AssetController {
     return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile);
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Post('/download-files')
   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
   async downloadFiles(
@@ -148,7 +149,7 @@ export class AssetController {
   /**
    * Current this is not used in any UI element
    */
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Get('/download-library')
   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
   async downloadLibrary(
@@ -165,7 +166,7 @@ export class AssetController {
     return stream;
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Get('/file/:assetId')
   @Header('Cache-Control', 'max-age=31536000')
   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
@@ -180,7 +181,7 @@ export class AssetController {
     return this.assetService.serveFile(authUser, assetId, query, res, headers);
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Get('/thumbnail/:assetId')
   @Header('Cache-Control', 'max-age=31536000')
   @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
@@ -195,25 +196,21 @@ export class AssetController {
     return this.assetService.getAssetThumbnail(assetId, query, res, headers);
   }
 
-  @Authenticated()
   @Get('/curated-objects')
   async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
     return this.assetService.getCuratedObject(authUser);
   }
 
-  @Authenticated()
   @Get('/curated-locations')
   async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
     return this.assetService.getCuratedLocation(authUser);
   }
 
-  @Authenticated()
   @Get('/search-terms')
   async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<string[]> {
     return this.assetService.getAssetSearchTerm(authUser);
   }
 
-  @Authenticated()
   @Post('/search')
   async searchAsset(
     @GetAuthUser() authUser: AuthUserDto,
@@ -222,7 +219,6 @@ export class AssetController {
     return this.assetService.searchAsset(authUser, searchAssetDto);
   }
 
-  @Authenticated()
   @Post('/count-by-time-bucket')
   async getAssetCountByTimeBucket(
     @GetAuthUser() authUser: AuthUserDto,
@@ -231,13 +227,11 @@ export class AssetController {
     return this.assetService.getAssetCountByTimeBucket(authUser, getAssetCountByTimeGroupDto);
   }
 
-  @Authenticated()
   @Get('/count-by-user-id')
   async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
     return this.assetService.getAssetCountByUserId(authUser);
   }
 
-  @Authenticated()
   @Get('/stat/archive')
   async getArchivedAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
     return this.assetService.getArchivedAssetCountByUserId(authUser);
@@ -245,7 +239,6 @@ export class AssetController {
   /**
    * Get all AssetEntity belong to the user
    */
-  @Authenticated()
   @Get('/')
   @ApiHeader({
     name: 'if-none-match',
@@ -260,7 +253,6 @@ export class AssetController {
     return this.assetService.getAllAssets(authUser, dto);
   }
 
-  @Authenticated()
   @Post('/time-bucket')
   async getAssetByTimeBucket(
     @GetAuthUser() authUser: AuthUserDto,
@@ -272,7 +264,6 @@ export class AssetController {
   /**
    * Get all asset of a device that are in the database, ID only.
    */
-  @Authenticated()
   @Get('/:deviceId')
   async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) {
     return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
@@ -281,7 +272,7 @@ export class AssetController {
   /**
    * Get a single asset's information
    */
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Get('/assetById/:assetId')
   async getAssetById(
     @GetAuthUser() authUser: AuthUserDto,
@@ -294,7 +285,6 @@ export class AssetController {
   /**
    * Update an asset
    */
-  @Authenticated()
   @Put('/:assetId')
   async updateAsset(
     @GetAuthUser() authUser: AuthUserDto,
@@ -305,7 +295,6 @@ export class AssetController {
     return await this.assetService.updateAsset(authUser, assetId, dto);
   }
 
-  @Authenticated()
   @Delete('/')
   async deleteAsset(
     @GetAuthUser() authUser: AuthUserDto,
@@ -318,7 +307,7 @@ export class AssetController {
   /**
    * Check duplicated asset before uploading - for Web upload used
    */
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Post('/check')
   @HttpCode(200)
   async checkDuplicateAsset(
@@ -331,7 +320,6 @@ export class AssetController {
   /**
    * Checks if multiple assets exist on the server and returns all existing - used by background backup
    */
-  @Authenticated()
   @Post('/exist')
   @HttpCode(200)
   async checkExistingAssets(
@@ -344,7 +332,6 @@ export class AssetController {
   /**
    * Checks if assets exist by checksums
    */
-  @Authenticated()
   @Post('/bulk-upload-check')
   @HttpCode(200)
   bulkUploadCheck(
@@ -354,7 +341,6 @@ export class AssetController {
     return this.assetService.bulkUploadCheck(authUser, dto);
   }
 
-  @Authenticated()
   @Post('/shared-link')
   async createAssetsSharedLink(
     @GetAuthUser() authUser: AuthUserDto,
@@ -363,7 +349,7 @@ export class AssetController {
     return await this.assetService.createAssetsSharedLink(authUser, dto);
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Patch('/shared-link/add')
   async addAssetsToSharedLink(
     @GetAuthUser() authUser: AuthUserDto,
@@ -372,7 +358,7 @@ export class AssetController {
     return await this.assetService.addAssetsToSharedLink(authUser, dto);
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Patch('/shared-link/remove')
   async removeAssetsFromSharedLink(
     @GetAuthUser() authUser: AuthUserDto,

+ 1 - 1
server/apps/immich/src/api-v1/tag/tag.controller.ts

@@ -8,9 +8,9 @@ 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')
 @Controller('tag')
+@Authenticated()
 export class TagController {
   constructor(private readonly tagService: TagService) {}
 

+ 4 - 7
server/apps/immich/src/controllers/auth.controller.ts

@@ -19,16 +19,18 @@ import { Body, Controller, Delete, Get, Param, Post, Req, Res } from '@nestjs/co
 import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger';
 import { Request, Response } from 'express';
 import { GetAuthUser, GetLoginDetails } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
+import { Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
 import { UseValidation } from '../decorators/use-validation.decorator';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('Authentication')
 @Controller('auth')
+@Authenticated()
 @UseValidation()
 export class AuthController {
   constructor(private readonly service: AuthService) {}
 
+  @PublicRoute()
   @Post('login')
   async login(
     @Body() loginCredential: LoginCredentialDto,
@@ -40,43 +42,38 @@ export class AuthController {
     return response;
   }
 
+  @PublicRoute()
   @Post('admin-sign-up')
   @ApiBadRequestResponse({ description: 'The server already has an admin' })
   adminSignUp(@Body() signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
     return this.service.adminSignUp(signUpCredential);
   }
 
-  @Authenticated()
   @Get('devices')
   getAuthDevices(@GetAuthUser() authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> {
     return this.service.getDevices(authUser);
   }
 
-  @Authenticated()
   @Delete('devices')
   logoutAuthDevices(@GetAuthUser() authUser: AuthUserDto): Promise<void> {
     return this.service.logoutDevices(authUser);
   }
 
-  @Authenticated()
   @Delete('devices/:id')
   logoutAuthDevice(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
     return this.service.logoutDevice(authUser, id);
   }
 
-  @Authenticated()
   @Post('validateToken')
   validateAccessToken(): ValidateAccessTokenResponseDto {
     return { authStatus: true };
   }
 
-  @Authenticated()
   @Post('change-password')
   changePassword(@GetAuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
     return this.service.changePassword(authUser, dto);
   }
 
-  @Authenticated()
   @Post('logout')
   logout(
     @Req() req: Request,

+ 5 - 3
server/apps/immich/src/controllers/oauth.controller.ts

@@ -12,15 +12,17 @@ import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@ne
 import { ApiTags } from '@nestjs/swagger';
 import { Request, Response } from 'express';
 import { GetAuthUser, GetLoginDetails } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
+import { Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
 import { UseValidation } from '../decorators/use-validation.decorator';
 
 @ApiTags('OAuth')
 @Controller('oauth')
+@Authenticated()
 @UseValidation()
 export class OAuthController {
   constructor(private service: OAuthService) {}
 
+  @PublicRoute()
   @Get('mobile-redirect')
   @Redirect()
   mobileRedirect(@Req() req: Request) {
@@ -30,11 +32,13 @@ export class OAuthController {
     };
   }
 
+  @PublicRoute()
   @Post('config')
   generateConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
     return this.service.generateConfig(dto);
   }
 
+  @PublicRoute()
   @Post('callback')
   async callback(
     @Res({ passthrough: true }) res: Response,
@@ -46,13 +50,11 @@ export class OAuthController {
     return response;
   }
 
-  @Authenticated()
   @Post('link')
   link(@GetAuthUser() authUser: AuthUserDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
     return this.service.link(authUser, dto);
   }
 
-  @Authenticated()
   @Post('unlink')
   unlink(@GetAuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
     return this.service.unlink(authUser);

+ 1 - 3
server/apps/immich/src/controllers/partner.controller.ts

@@ -8,11 +8,11 @@ import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('Partner')
 @Controller('partner')
+@Authenticated()
 @UseValidation()
 export class PartnerController {
   constructor(private service: PartnerService) {}
 
-  @Authenticated()
   @Get()
   @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
   getPartners(
@@ -22,13 +22,11 @@ export class PartnerController {
     return this.service.getAll(authUser, direction);
   }
 
-  @Authenticated()
   @Post(':id')
   createPartner(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
     return this.service.create(authUser, id);
   }
 
-  @Authenticated()
   @Delete(':id')
   removePartner(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
     return this.service.remove(authUser, id);

+ 5 - 3
server/apps/immich/src/controllers/server-info.controller.ts

@@ -7,32 +7,34 @@ import {
 } from '@app/domain';
 import { Controller, Get } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { Authenticated } from '../decorators/authenticated.decorator';
+import { AdminRoute, Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
 import { UseValidation } from '../decorators/use-validation.decorator';
 
 @ApiTags('Server Info')
 @Controller('server-info')
+@Authenticated()
 @UseValidation()
 export class ServerInfoController {
   constructor(private service: ServerInfoService) {}
 
-  @Authenticated()
   @Get()
   getServerInfo(): Promise<ServerInfoResponseDto> {
     return this.service.getInfo();
   }
 
+  @PublicRoute()
   @Get('/ping')
   pingServer(): ServerPingResponse {
     return this.service.ping();
   }
 
+  @PublicRoute()
   @Get('/version')
   getServerVersion(): ServerVersionReponseDto {
     return this.service.getVersion();
   }
 
-  @Authenticated({ admin: true })
+  @AdminRoute()
   @Get('/stats')
   getStats(): Promise<ServerStatsResponseDto> {
     return this.service.getStats();

+ 3 - 6
server/apps/immich/src/controllers/shared-link.controller.ts

@@ -2,29 +2,28 @@ import { AuthUserDto, EditSharedLinkDto, SharedLinkResponseDto, ShareService } f
 import { Body, Controller, Delete, Get, Param, Patch } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { GetAuthUser } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
+import { Authenticated, SharedLinkRoute } from '../decorators/authenticated.decorator';
 import { UseValidation } from '../decorators/use-validation.decorator';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('share')
 @Controller('share')
+@Authenticated()
 @UseValidation()
 export class SharedLinkController {
   constructor(private readonly service: ShareService) {}
 
-  @Authenticated()
   @Get()
   getAllSharedLinks(@GetAuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
     return this.service.getAll(authUser);
   }
 
-  @Authenticated({ isShared: true })
+  @SharedLinkRoute()
   @Get('me')
   getMySharedLink(@GetAuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
     return this.service.getMine(authUser);
   }
 
-  @Authenticated()
   @Get(':id')
   getSharedLinkById(
     @GetAuthUser() authUser: AuthUserDto,
@@ -33,13 +32,11 @@ export class SharedLinkController {
     return this.service.getById(authUser, id, true);
   }
 
-  @Authenticated()
   @Delete(':id')
   removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
     return this.service.remove(authUser, id);
   }
 
-  @Authenticated()
   @Patch(':id')
   editSharedLink(
     @GetAuthUser() authUser: AuthUserDto,

+ 6 - 10
server/apps/immich/src/controllers/user.controller.ts

@@ -14,7 +14,7 @@ import {
   Header,
 } from '@nestjs/common';
 import { UserService } from '@app/domain';
-import { Authenticated } from '../decorators/authenticated.decorator';
+import { AdminRoute, Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
 import { AuthUserDto, GetAuthUser } from '../decorators/auth-user.decorator';
 import { CreateUserDto } from '@app/domain';
 import { UpdateUserDto } from '@app/domain';
@@ -32,58 +32,55 @@ import { UserIdDto } from '@app/domain/user/dto/user-id.dto';
 
 @ApiTags('User')
 @Controller('user')
+@Authenticated()
 @UseValidation()
 export class UserController {
   constructor(private service: UserService) {}
 
-  @Authenticated()
   @Get()
   getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
     return this.service.getAllUsers(authUser, isAll);
   }
 
-  @Authenticated()
   @Get('/info/:userId')
   getUserById(@Param() { userId }: UserIdDto): Promise<UserResponseDto> {
     return this.service.getUserById(userId);
   }
 
-  @Authenticated()
   @Get('me')
   getMyUserInfo(@GetAuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
     return this.service.getUserInfo(authUser);
   }
 
-  @Authenticated({ admin: true })
+  @AdminRoute()
   @Post()
   createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
     return this.service.createUser(createUserDto);
   }
 
+  @PublicRoute()
   @Get('/count')
   getUserCount(@Query() dto: UserCountDto): Promise<UserCountResponseDto> {
     return this.service.getUserCount(dto);
   }
 
-  @Authenticated({ admin: true })
+  @AdminRoute()
   @Delete('/:userId')
   deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param() { userId }: UserIdDto): Promise<UserResponseDto> {
     return this.service.deleteUser(authUser, userId);
   }
 
-  @Authenticated({ admin: true })
+  @AdminRoute()
   @Post('/:userId/restore')
   restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param() { userId }: UserIdDto): Promise<UserResponseDto> {
     return this.service.restoreUser(authUser, userId);
   }
 
-  @Authenticated()
   @Put()
   updateUser(@GetAuthUser() authUser: AuthUserDto, @Body() updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
     return this.service.updateUser(authUser, updateUserDto);
   }
 
-  @Authenticated()
   @UseInterceptors(FileInterceptor('file', profileImageUploadOption))
   @ApiConsumes('multipart/form-data')
   @ApiBody({
@@ -98,7 +95,6 @@ export class UserController {
     return this.service.createProfileImage(authUser, fileInfo);
   }
 
-  @Authenticated()
   @Get('/profile-image/:userId')
   @Header('Cache-Control', 'max-age=600')
   async getProfileImage(@Param() { userId }: UserIdDto, @Response({ passthrough: true }) res: Res): Promise<any> {

+ 15 - 3
server/apps/immich/src/decorators/authenticated.decorator.ts

@@ -11,8 +11,16 @@ export enum Metadata {
   AUTH_ROUTE = 'auth_route',
   ADMIN_ROUTE = 'admin_route',
   SHARED_ROUTE = 'shared_route',
+  PUBLIC_SECURITY = 'public_security',
 }
 
+const adminDecorator = SetMetadata(Metadata.ADMIN_ROUTE, true);
+
+const sharedLinkDecorators = [
+  SetMetadata(Metadata.SHARED_ROUTE, true),
+  ApiQuery({ name: 'key', type: String, required: false }),
+];
+
 export const Authenticated = (options: AuthenticatedOptions = {}) => {
   const decorators: MethodDecorator[] = [
     ApiBearerAuth(),
@@ -22,13 +30,17 @@ export const Authenticated = (options: AuthenticatedOptions = {}) => {
   ];
 
   if (options.admin) {
-    decorators.push(SetMetadata(Metadata.ADMIN_ROUTE, true));
+    decorators.push(adminDecorator);
   }
 
   if (options.isShared) {
-    decorators.push(SetMetadata(Metadata.SHARED_ROUTE, true));
-    decorators.push(ApiQuery({ name: 'key', type: String, required: false }));
+    decorators.push(...sharedLinkDecorators);
   }
 
   return applyDecorators(...decorators);
 };
+
+export const PublicRoute = () =>
+  applyDecorators(SetMetadata(Metadata.AUTH_ROUTE, false), ApiSecurity(Metadata.PUBLIC_SECURITY));
+export const SharedLinkRoute = () => applyDecorators(...sharedLinkDecorators);
+export const AdminRoute = () => adminDecorator;

+ 5 - 0
server/apps/immich/src/utils/patch-open-api.util.ts

@@ -1,4 +1,5 @@
 import { OpenAPIObject } from '@nestjs/swagger';
+import { Metadata } from '../decorators/authenticated.decorator';
 
 export function patchOpenAPI(document: OpenAPIObject) {
   for (const path of Object.values(document.paths)) {
@@ -18,6 +19,10 @@ export function patchOpenAPI(document: OpenAPIObject) {
         continue;
       }
 
+      if ((operation.security || []).find((item) => !!item[Metadata.PUBLIC_SECURITY])) {
+        delete operation.security;
+      }
+
       if (operation.summary === '') {
         delete operation.summary;
       }