From 2b186e82b193cab41968d642ae6050720a7ebf7b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 8 May 2024 19:47:43 -0400 Subject: [PATCH] refactor(server): auth route metadata --- open-api/immich-openapi-specs.json | 18 -------- server/src/controllers/activity.controller.ts | 5 ++- server/src/controllers/album.controller.ts | 16 +++++-- server/src/controllers/api-key.controller.ts | 6 ++- server/src/controllers/app.controller.ts | 2 - server/src/controllers/asset-v1.controller.ts | 17 ++++---- server/src/controllers/asset.controller.ts | 15 +++++-- server/src/controllers/audit.controller.ts | 2 +- server/src/controllers/auth.controller.ts | 8 ++-- server/src/controllers/download.controller.ts | 9 ++-- server/src/controllers/face.controller.ts | 3 +- .../src/controllers/file-report.controller.ts | 13 +++--- server/src/controllers/job.controller.ts | 3 +- server/src/controllers/library.controller.ts | 13 ++++-- server/src/controllers/memory.controller.ts | 8 +++- server/src/controllers/oauth.controller.ts | 8 ++-- server/src/controllers/partner.controller.ts | 5 ++- server/src/controllers/person.controller.ts | 11 ++++- server/src/controllers/search.controller.ts | 8 +++- .../src/controllers/server-info.controller.ts | 12 ++---- server/src/controllers/session.controller.ts | 4 +- .../src/controllers/shared-link.controller.ts | 14 +++--- server/src/controllers/sync.controller.ts | 3 +- .../controllers/system-config.controller.ts | 10 +++-- .../controllers/system-metadata.controller.ts | 4 +- server/src/controllers/tag.controller.ts | 9 +++- server/src/controllers/timeline.controller.ts | 5 +-- server/src/controllers/trash.controller.ts | 4 +- server/src/controllers/user.controller.ts | 16 ++++--- server/src/dtos/auth.dto.ts | 6 ++- server/src/middleware/auth.guard.ts | 43 ++++++------------- server/src/services/auth.service.ts | 2 +- server/src/utils/misc.ts | 4 -- 33 files changed, 171 insertions(+), 135 deletions(-) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 993705f97aecf..eea90fb1c98e3 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -5806,15 +5806,6 @@ } }, "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - }, { "bearer": [] }, @@ -5942,15 +5933,6 @@ } }, "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - }, { "bearer": [] }, diff --git a/server/src/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts index a65b284ca1e91..9a5fc4188599f 100644 --- a/server/src/controllers/activity.controller.ts +++ b/server/src/controllers/activity.controller.ts @@ -15,21 +15,23 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('Activity') @Controller('activity') -@Authenticated() export class ActivityController { constructor(private service: ActivityService) {} @Get() + @Authenticated() getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise { return this.service.getAll(auth, dto); } @Get('statistics') + @Authenticated() getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { return this.service.getStatistics(auth, dto); } @Post() + @Authenticated() async createActivity( @Auth() auth: AuthDto, @Body() dto: ActivityCreateDto, @@ -44,6 +46,7 @@ export class ActivityController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 0e8d954ab88ae..c38f733b420ee 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -12,32 +12,34 @@ import { } from 'src/dtos/album.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AlbumService } from 'src/services/album.service'; import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; @ApiTags('Album') @Controller('album') -@Authenticated() export class AlbumController { constructor(private service: AlbumService) {} @Get('count') + @Authenticated() getAlbumCount(@Auth() auth: AuthDto): Promise { return this.service.getCount(auth); } @Get() + @Authenticated() getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { return this.service.getAll(auth, query); } @Post() + @Authenticated() createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise { return this.service.create(auth, dto); } - @SharedLinkRoute() + @Authenticated({ sharedLink: true }) @Get(':id') getAlbumInfo( @Auth() auth: AuthDto, @@ -48,6 +50,7 @@ export class AlbumController { } @Patch(':id') + @Authenticated() updateAlbumInfo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -57,12 +60,13 @@ export class AlbumController { } @Delete(':id') + @Authenticated() deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { return this.service.delete(auth, id); } - @SharedLinkRoute() @Put(':id/assets') + @Authenticated({ sharedLink: true }) addAssetsToAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -72,6 +76,7 @@ export class AlbumController { } @Delete(':id/assets') + @Authenticated() removeAssetFromAlbum( @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, @@ -81,6 +86,7 @@ export class AlbumController { } @Put(':id/users') + @Authenticated() addUsersToAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -90,6 +96,7 @@ export class AlbumController { } @Put(':id/user/:userId') + @Authenticated() updateAlbumUser( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -100,6 +107,7 @@ export class AlbumController { } @Delete(':id/user/:userId') + @Authenticated() removeUserFromAlbum( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index 564b90387412c..e0b07ede50447 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -8,26 +8,29 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('API Key') @Controller('api-key') -@Authenticated() export class APIKeyController { constructor(private service: APIKeyService) {} @Post() + @Authenticated() createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise { return this.service.create(auth, dto); } @Get() + @Authenticated() getApiKeys(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Get(':id') + @Authenticated() getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') + @Authenticated() updateApiKey( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -37,6 +40,7 @@ export class APIKeyController { } @Delete(':id') + @Authenticated() deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/app.controller.ts b/server/src/controllers/app.controller.ts index 472d0da3f7749..3fe9b493686aa 100644 --- a/server/src/controllers/app.controller.ts +++ b/server/src/controllers/app.controller.ts @@ -1,6 +1,5 @@ import { Controller, Get, Header } from '@nestjs/common'; import { ApiExcludeEndpoint } from '@nestjs/swagger'; -import { PublicRoute } from 'src/middleware/auth.guard'; import { SystemConfigService } from 'src/services/system-config.service'; @Controller() @@ -18,7 +17,6 @@ export class AppController { } @ApiExcludeEndpoint() - @PublicRoute() @Get('custom.css') @Header('Content-Type', 'text/css') getCustomCss() { diff --git a/server/src/controllers/asset-v1.controller.ts b/server/src/controllers/asset-v1.controller.ts index 908569b58f9c1..2f093e1d1f7e5 100644 --- a/server/src/controllers/asset-v1.controller.ts +++ b/server/src/controllers/asset-v1.controller.ts @@ -31,7 +31,7 @@ import { } from 'src/dtos/asset-v1.dto'; import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; -import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor'; import { AssetServiceV1 } from 'src/services/asset-v1.service'; import { sendFile } from 'src/utils/file'; @@ -45,11 +45,9 @@ interface UploadFiles { @ApiTags('Asset') @Controller(Route.ASSET) -@Authenticated() export class AssetControllerV1 { constructor(private service: AssetServiceV1) {} - @SharedLinkRoute() @Post('upload') @UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor) @ApiConsumes('multipart/form-data') @@ -58,10 +56,8 @@ export class AssetControllerV1 { description: 'sha1 checksum that can be used for duplicate detection before the file is uploaded', required: false, }) - @ApiBody({ - description: 'Asset Upload Information', - type: CreateAssetDto, - }) + @ApiBody({ description: 'Asset Upload Information', type: CreateAssetDto }) + @Authenticated({ sharedLink: true }) async uploadFile( @Auth() auth: AuthDto, @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles, @@ -89,9 +85,9 @@ export class AssetControllerV1 { return responseDto; } - @SharedLinkRoute() @Get('/file/:id') @FileResponse() + @Authenticated({ sharedLink: true }) async serveFile( @Res() res: Response, @Next() next: NextFunction, @@ -102,9 +98,9 @@ export class AssetControllerV1 { await sendFile(res, next, () => this.service.serveFile(auth, id, dto)); } - @SharedLinkRoute() @Get('/thumbnail/:id') @FileResponse() + @Authenticated({ sharedLink: true }) async getAssetThumbnail( @Res() res: Response, @Next() next: NextFunction, @@ -125,6 +121,7 @@ export class AssetControllerV1 { required: false, schema: { type: 'string' }, }) + @Authenticated() getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise { return this.service.getAllAssets(auth, dto); } @@ -134,6 +131,7 @@ export class AssetControllerV1 { */ @Post('/exist') @HttpCode(HttpStatus.OK) + @Authenticated() checkExistingAssets( @Auth() auth: AuthDto, @Body() dto: CheckExistingAssetsDto, @@ -146,6 +144,7 @@ export class AssetControllerV1 { */ @Post('/bulk-upload-check') @HttpCode(HttpStatus.OK) + @Authenticated() checkBulkUpload( @Auth() auth: AuthDto, @Body() dto: AssetBulkUploadCheckDto, diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 9db27998d2850..f2d076e17b173 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -14,28 +14,30 @@ import { import { AuthDto } from 'src/dtos/auth.dto'; import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto'; import { UpdateStackParentDto } from 'src/dtos/stack.dto'; -import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Route } from 'src/middleware/file-upload.interceptor'; import { AssetService } from 'src/services/asset.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Asset') @Controller(Route.ASSET) -@Authenticated() export class AssetController { constructor(private service: AssetService) {} @Get('map-marker') + @Authenticated() getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise { return this.service.getMapMarkers(auth, options); } @Get('memory-lane') + @Authenticated() getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise { return this.service.getMemoryLane(auth, dto); } @Get('random') + @Authenticated() getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise { return this.service.getRandom(auth, dto.count ?? 1); } @@ -44,46 +46,53 @@ export class AssetController { * Get all asset of a device that are in the database, ID only. */ @Get('/device/:deviceId') + @Authenticated() getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) { return this.service.getUserAssetsByDeviceId(auth, deviceId); } @Get('statistics') + @Authenticated() getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise { return this.service.getStatistics(auth, dto); } @Post('jobs') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise { return this.service.run(auth, dto); } @Put() @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise { return this.service.updateAll(auth, dto); } @Delete() @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise { return this.service.deleteAll(auth, dto); } @Put('stack/parent') @HttpCode(HttpStatus.OK) + @Authenticated() updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise { return this.service.updateStackParent(auth, dto); } - @SharedLinkRoute() @Get(':id') + @Authenticated({ sharedLink: true }) getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id) as Promise; } @Put(':id') + @Authenticated() updateAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/audit.controller.ts b/server/src/controllers/audit.controller.ts index 8eea6a6e3e098..856a1cc7550d9 100644 --- a/server/src/controllers/audit.controller.ts +++ b/server/src/controllers/audit.controller.ts @@ -7,11 +7,11 @@ import { AuditService } from 'src/services/audit.service'; @ApiTags('Audit') @Controller('audit') -@Authenticated() export class AuditController { constructor(private service: AuditService) {} @Get('deletes') + @Authenticated() getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise { return this.service.getDeletes(auth, dto); } diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index a4c7494f2b72e..40fdf909167d4 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -13,17 +13,15 @@ import { ValidateAccessTokenResponseDto, } from 'src/dtos/auth.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; @ApiTags('Authentication') @Controller('auth') -@Authenticated() export class AuthController { constructor(private service: AuthService) {} - @PublicRoute() @Post('login') async login( @Body() loginCredential: LoginCredentialDto, @@ -41,7 +39,6 @@ export class AuthController { }); } - @PublicRoute() @Post('admin-sign-up') signUpAdmin(@Body() dto: SignUpDto): Promise { return this.service.adminSignUp(dto); @@ -49,18 +46,21 @@ export class AuthController { @Post('validateToken') @HttpCode(HttpStatus.OK) + @Authenticated() validateAccessToken(): ValidateAccessTokenResponseDto { return { authStatus: true }; } @Post('change-password') @HttpCode(HttpStatus.OK) + @Authenticated() changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise { return this.service.changePassword(auth, dto).then(mapUser); } @Post('logout') @HttpCode(HttpStatus.OK) + @Authenticated() async logout( @Req() request: Request, @Res({ passthrough: true }) res: Response, diff --git a/server/src/controllers/download.controller.ts b/server/src/controllers/download.controller.ts index 4e4bf09d11b4f..c9f26864affb1 100644 --- a/server/src/controllers/download.controller.ts +++ b/server/src/controllers/download.controller.ts @@ -4,35 +4,34 @@ import { NextFunction, Response } from 'express'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; -import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { DownloadService } from 'src/services/download.service'; import { asStreamableFile, sendFile } from 'src/utils/file'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Download') @Controller('download') -@Authenticated() export class DownloadController { constructor(private service: DownloadService) {} - @SharedLinkRoute() @Post('info') + @Authenticated({ sharedLink: true }) getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise { return this.service.getDownloadInfo(auth, dto); } - @SharedLinkRoute() @Post('archive') @HttpCode(HttpStatus.OK) @FileResponse() + @Authenticated({ sharedLink: true }) downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise { return this.service.downloadArchive(auth, dto).then(asStreamableFile); } - @SharedLinkRoute() @Post('asset/:id') @HttpCode(HttpStatus.OK) @FileResponse() + @Authenticated({ sharedLink: true }) async downloadFile( @Res() res: Response, @Next() next: NextFunction, diff --git a/server/src/controllers/face.controller.ts b/server/src/controllers/face.controller.ts index a3f33fb867498..5b454329442a7 100644 --- a/server/src/controllers/face.controller.ts +++ b/server/src/controllers/face.controller.ts @@ -8,16 +8,17 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('Face') @Controller('face') -@Authenticated() export class FaceController { constructor(private service: PersonService) {} @Get() + @Authenticated() getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise { return this.service.getFacesById(auth, dto); } @Put(':id') + @Authenticated() reassignFacesById( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/file-report.controller.ts b/server/src/controllers/file-report.controller.ts index 6bdf72607365d..1f9ebe52dd80a 100644 --- a/server/src/controllers/file-report.controller.ts +++ b/server/src/controllers/file-report.controller.ts @@ -1,29 +1,28 @@ import { Body, Controller, Get, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto'; -import { AdminRoute, Authenticated } from 'src/middleware/auth.guard'; +import { Authenticated } from 'src/middleware/auth.guard'; import { AuditService } from 'src/services/audit.service'; @ApiTags('File Report') @Controller('report') -@Authenticated() export class ReportController { constructor(private service: AuditService) {} - @AdminRoute() @Get() + @Authenticated({ admin: true }) getAuditFiles(): Promise { return this.service.getFileReport(); } - @AdminRoute() - @Post('/checksum') + @Post('checksum') + @Authenticated({ admin: true }) getFileChecksums(@Body() dto: FileChecksumDto): Promise { return this.service.getChecksums(dto); } - @AdminRoute() - @Post('/fix') + @Post('fix') + @Authenticated({ admin: true }) fixAuditFiles(@Body() dto: FileReportFixDto): Promise { return this.service.fixItems(dto.items); } diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts index d6bd45b1e8460..2d23263221d39 100644 --- a/server/src/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -6,16 +6,17 @@ import { JobService } from 'src/services/job.service'; @ApiTags('Job') @Controller('jobs') -@Authenticated({ admin: true }) export class JobController { constructor(private service: JobService) {} @Get() + @Authenticated({ admin: true }) getAllJobsStatus(): Promise { return this.service.getAllJobsStatus(); } @Put(':id') + @Authenticated({ admin: true }) sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise { return this.service.handleCommand(id, dto); } diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index 70d357187ef9c..74a4f73d2abce 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -10,39 +10,42 @@ import { ValidateLibraryDto, ValidateLibraryResponseDto, } from 'src/dtos/library.dto'; -import { AdminRoute, Authenticated } from 'src/middleware/auth.guard'; +import { Authenticated } from 'src/middleware/auth.guard'; import { LibraryService } from 'src/services/library.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Library') @Controller('library') -@Authenticated() -@AdminRoute() export class LibraryController { constructor(private service: LibraryService) {} @Get() + @Authenticated({ admin: true }) getAllLibraries(@Query() dto: SearchLibraryDto): Promise { return this.service.getAll(dto); } @Post() + @Authenticated({ admin: true }) createLibrary(@Body() dto: CreateLibraryDto): Promise { return this.service.create(dto); } @Put(':id') + @Authenticated({ admin: true }) updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise { return this.service.update(id, dto); } @Get(':id') + @Authenticated({ admin: true }) getLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } @Post(':id/validate') @HttpCode(200) + @Authenticated({ admin: true }) // TODO: change endpoint to validate current settings instead validate(@Param() { id }: UUIDParamDto, @Body() dto: ValidateLibraryDto): Promise { return this.service.validate(id, dto); @@ -50,23 +53,27 @@ export class LibraryController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ admin: true }) deleteLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.delete(id); } @Get(':id/statistics') + @Authenticated({ admin: true }) getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(id); } @Post(':id/scan') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ admin: true }) scanLibrary(@Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) { return this.service.queueScan(id, dto); } @Post(':id/removeOffline') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ admin: true }) removeOfflineFiles(@Param() { id }: UUIDParamDto) { return this.service.queueRemoveOffline(id); } diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts index 771d70594233a..4b779c60f234e 100644 --- a/server/src/controllers/memory.controller.ts +++ b/server/src/controllers/memory.controller.ts @@ -9,26 +9,29 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('Memory') @Controller('memories') -@Authenticated() export class MemoryController { constructor(private service: MemoryService) {} @Get() + @Authenticated() searchMemories(@Auth() auth: AuthDto): Promise { return this.service.search(auth); } @Post() + @Authenticated() createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise { return this.service.create(auth, dto); } @Get(':id') + @Authenticated() getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') + @Authenticated() updateMemory( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -39,11 +42,13 @@ export class MemoryController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } @Put(':id/assets') + @Authenticated() addMemoryAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -54,6 +59,7 @@ export class MemoryController { @Delete(':id/assets') @HttpCode(HttpStatus.OK) + @Authenticated() removeMemoryAssets( @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index d87fb11d88126..3b498c7ddde35 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -11,17 +11,15 @@ import { OAuthConfigDto, } from 'src/dtos/auth.dto'; import { UserResponseDto } from 'src/dtos/user.dto'; -import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { respondWithCookie } from 'src/utils/response'; @ApiTags('OAuth') @Controller('oauth') -@Authenticated() export class OAuthController { constructor(private service: AuthService) {} - @PublicRoute() @Get('mobile-redirect') @Redirect() redirectOAuthToMobile(@Req() request: Request) { @@ -31,13 +29,11 @@ export class OAuthController { }; } - @PublicRoute() @Post('authorize') startOAuth(@Body() dto: OAuthConfigDto): Promise { return this.service.authorize(dto); } - @PublicRoute() @Post('callback') async finishOAuth( @Res({ passthrough: true }) res: Response, @@ -56,11 +52,13 @@ export class OAuthController { } @Post('link') + @Authenticated() linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise { return this.service.link(auth, dto); } @Post('unlink') + @Authenticated() unlinkOAuthAccount(@Auth() auth: AuthDto): Promise { return this.service.unlink(auth); } diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts index f654a726375cc..1faf82898cc87 100644 --- a/server/src/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -9,23 +9,25 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('Partner') @Controller('partner') -@Authenticated() export class PartnerController { constructor(private service: PartnerService) {} @Get() @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true }) + @Authenticated() // TODO: remove 'direction' and convert to full query dto getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise { return this.service.getAll(auth, direction); } @Post(':id') + @Authenticated() createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.create(auth, id); } @Put(':id') + @Authenticated() updatePartner( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -35,6 +37,7 @@ export class PartnerController { } @Delete(':id') + @Authenticated() removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index c9128a1f7fa03..dc87825927ac8 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -22,31 +22,35 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('Person') @Controller('person') -@Authenticated() export class PersonController { constructor(private service: PersonService) {} @Get() + @Authenticated() getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise { return this.service.getAll(auth, withHidden); } @Post() + @Authenticated() createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise { return this.service.create(auth, dto); } @Put() + @Authenticated() updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise { return this.service.updateAll(auth, dto); } @Get(':id') + @Authenticated() getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') + @Authenticated() updatePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -56,12 +60,14 @@ export class PersonController { } @Get(':id/statistics') + @Authenticated() getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(auth, id); } @Get(':id/thumbnail') @FileResponse() + @Authenticated() async getPersonThumbnail( @Res() res: Response, @Next() next: NextFunction, @@ -72,11 +78,13 @@ export class PersonController { } @Get(':id/assets') + @Authenticated() getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getAssets(auth, id); } @Put(':id/reassign') + @Authenticated() reassignFaces( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -86,6 +94,7 @@ export class PersonController { } @Post(':id/merge') + @Authenticated() mergePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index ce0d0f646df40..688ff1c138987 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -18,43 +18,49 @@ import { SearchService } from 'src/services/search.service'; @ApiTags('Search') @Controller('search') -@Authenticated() export class SearchController { constructor(private service: SearchService) {} @Post('metadata') @HttpCode(HttpStatus.OK) + @Authenticated() searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise { return this.service.searchMetadata(auth, dto); } @Post('smart') @HttpCode(HttpStatus.OK) + @Authenticated() searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise { return this.service.searchSmart(auth, dto); } @Get('explore') + @Authenticated() getExploreData(@Auth() auth: AuthDto): Promise { return this.service.getExploreData(auth) as Promise; } @Get('person') + @Authenticated() searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise { return this.service.searchPerson(auth, dto); } @Get('places') + @Authenticated() searchPlaces(@Query() dto: SearchPlacesDto): Promise { return this.service.searchPlaces(dto); } @Get('cities') + @Authenticated() getAssetsByCity(@Auth() auth: AuthDto): Promise { return this.service.getAssetsByCity(auth); } @Get('suggestions') + @Authenticated() getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise { return this.service.getSearchSuggestions(auth, dto); } diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index 35e5e17594b2b..758c50299a518 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -10,57 +10,51 @@ import { ServerThemeDto, ServerVersionResponseDto, } from 'src/dtos/server-info.dto'; -import { AdminRoute, Authenticated, PublicRoute } from 'src/middleware/auth.guard'; +import { Authenticated } from 'src/middleware/auth.guard'; import { ServerInfoService } from 'src/services/server-info.service'; @ApiTags('Server Info') @Controller('server-info') -@Authenticated() export class ServerInfoController { constructor(private service: ServerInfoService) {} @Get() + @Authenticated() getServerInfo(): Promise { return this.service.getInfo(); } - @PublicRoute() @Get('ping') pingServer(): ServerPingResponse { return this.service.ping(); } - @PublicRoute() @Get('version') getServerVersion(): ServerVersionResponseDto { return this.service.getVersion(); } - @PublicRoute() @Get('features') getServerFeatures(): Promise { return this.service.getFeatures(); } - @PublicRoute() @Get('theme') getTheme(): Promise { return this.service.getTheme(); } - @PublicRoute() @Get('config') getServerConfig(): Promise { return this.service.getConfig(); } - @AdminRoute() + @Authenticated({ admin: true }) @Get('statistics') getServerStatistics(): Promise { return this.service.getStatistics(); } - @PublicRoute() @Get('media-types') getSupportedMediaTypes(): ServerMediaTypesResponseDto { return this.service.getSupportedMediaTypes(); diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts index 552afcdf5aebe..a1fb4779a587c 100644 --- a/server/src/controllers/session.controller.ts +++ b/server/src/controllers/session.controller.ts @@ -8,23 +8,25 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('Sessions') @Controller('sessions') -@Authenticated() export class SessionController { constructor(private service: SessionService) {} @Get() + @Authenticated() getSessions(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Delete() @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() deleteAllSessions(@Auth() auth: AuthDto): Promise { return this.service.deleteAll(auth); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index 58f2939b934da..64d3e38e7ef81 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -10,7 +10,7 @@ import { SharedLinkPasswordDto, SharedLinkResponseDto, } from 'src/dtos/shared-link.dto'; -import { Auth, Authenticated, GetLoginDetails, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { LoginDetails } from 'src/services/auth.service'; import { SharedLinkService } from 'src/services/shared-link.service'; import { respondWithCookie } from 'src/utils/response'; @@ -18,17 +18,17 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('Shared Link') @Controller('shared-link') -@Authenticated() export class SharedLinkController { constructor(private service: SharedLinkService) {} @Get() + @Authenticated() getAllSharedLinks(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } - @SharedLinkRoute() @Get('me') + @Authenticated({ sharedLink: true }) async getMySharedLink( @Auth() auth: AuthDto, @Query() dto: SharedLinkPasswordDto, @@ -48,16 +48,19 @@ export class SharedLinkController { } @Get(':id') + @Authenticated() getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Post() + @Authenticated() createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { return this.service.create(auth, dto); } @Patch(':id') + @Authenticated() updateSharedLink( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -67,12 +70,13 @@ export class SharedLinkController { } @Delete(':id') + @Authenticated() removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } - @SharedLinkRoute() @Put(':id/assets') + @Authenticated({ sharedLink: true }) addSharedLinkAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -81,8 +85,8 @@ export class SharedLinkController { return this.service.addAssets(auth, id, dto); } - @SharedLinkRoute() @Delete(':id/assets') + @Authenticated({ sharedLink: true }) removeSharedLinkAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/sync.controller.ts b/server/src/controllers/sync.controller.ts index 63757f73f3e60..4d970a7102a8f 100644 --- a/server/src/controllers/sync.controller.ts +++ b/server/src/controllers/sync.controller.ts @@ -8,18 +8,19 @@ import { SyncService } from 'src/services/sync.service'; @ApiTags('Sync') @Controller('sync') -@Authenticated() export class SyncController { constructor(private service: SyncService) {} @Post('full-sync') @HttpCode(HttpStatus.OK) + @Authenticated() getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise { return this.service.getFullSync(auth, dto); } @Post('delta-sync') @HttpCode(HttpStatus.OK) + @Authenticated() getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise { return this.service.getDeltaSync(auth, dto); } diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index 08da7431910a8..bf9e8495f7c5f 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -1,37 +1,39 @@ import { Body, Controller, Get, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; -import { AdminRoute, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Authenticated } from 'src/middleware/auth.guard'; import { SystemConfigService } from 'src/services/system-config.service'; @ApiTags('System Config') @Controller('system-config') -@Authenticated({ admin: true }) export class SystemConfigController { constructor(private service: SystemConfigService) {} @Get() + @Authenticated({ admin: true }) getConfig(): Promise { return this.service.getConfig(); } @Get('defaults') + @Authenticated({ admin: true }) getConfigDefaults(): SystemConfigDto { return this.service.getDefaults(); } @Put() + @Authenticated({ admin: true }) updateConfig(@Body() dto: SystemConfigDto): Promise { return this.service.updateConfig(dto); } @Get('storage-template-options') + @Authenticated({ admin: true }) getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { return this.service.getStorageTemplateOptions(); } - @AdminRoute(false) - @SharedLinkRoute() + @Authenticated({ sharedLink: true }) @Get('map/style.json') getMapStyle(@Query() dto: MapThemeDto) { return this.service.getMapStyle(dto.theme); diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts index 7f186fec031d5..90e9f5b6a8aab 100644 --- a/server/src/controllers/system-metadata.controller.ts +++ b/server/src/controllers/system-metadata.controller.ts @@ -6,22 +6,24 @@ import { SystemMetadataService } from 'src/services/system-metadata.service'; @ApiTags('System Metadata') @Controller('system-metadata') -@Authenticated({ admin: true }) export class SystemMetadataController { constructor(private service: SystemMetadataService) {} @Get('admin-onboarding') + @Authenticated({ admin: true }) getAdminOnboarding(): Promise { return this.service.getAdminOnboarding(); } @Post('admin-onboarding') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ admin: true }) updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { return this.service.updateAdminOnboarding(dto); } @Get('reverse-geocoding-state') + @Authenticated({ admin: true }) getReverseGeocodingState(): Promise { return this.service.getReverseGeocodingState(); } diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts index 1caed8d528b83..2a46fdec71b67 100644 --- a/server/src/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -11,41 +11,47 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('Tag') @Controller('tag') -@Authenticated() export class TagController { constructor(private service: TagService) {} @Post() + @Authenticated() createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise { return this.service.create(auth, dto); } @Get() + @Authenticated() getAllTags(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Get(':id') + @Authenticated() getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Patch(':id') + @Authenticated() updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise { return this.service.update(auth, id, dto); } @Delete(':id') + @Authenticated() deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } @Get(':id/assets') + @Authenticated() getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getAssets(auth, id); } @Put(':id/assets') + @Authenticated() tagAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -55,6 +61,7 @@ export class TagController { } @Delete(':id/assets') + @Authenticated() untagAssets( @Auth() auth: AuthDto, @Body() dto: AssetIdsDto, diff --git a/server/src/controllers/timeline.controller.ts b/server/src/controllers/timeline.controller.ts index 173c6738de62c..53c62f70edd28 100644 --- a/server/src/controllers/timeline.controller.ts +++ b/server/src/controllers/timeline.controller.ts @@ -8,17 +8,16 @@ import { TimelineService } from 'src/services/timeline.service'; @ApiTags('Timeline') @Controller('timeline') -@Authenticated() export class TimelineController { constructor(private service: TimelineService) {} - @Authenticated({ isShared: true }) + @Authenticated({ sharedLink: true }) @Get('buckets') getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise { return this.service.getTimeBuckets(auth, dto); } - @Authenticated({ isShared: true }) + @Authenticated({ sharedLink: true }) @Get('bucket') getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise { return this.service.getTimeBucket(auth, dto) as Promise; diff --git a/server/src/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts index 25df3543cc547..eae49d4ad1f47 100644 --- a/server/src/controllers/trash.controller.ts +++ b/server/src/controllers/trash.controller.ts @@ -7,24 +7,26 @@ import { TrashService } from 'src/services/trash.service'; @ApiTags('Trash') @Controller('trash') -@Authenticated() export class TrashController { constructor(private service: TrashService) {} @Post('empty') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() emptyTrash(@Auth() auth: AuthDto): Promise { return this.service.empty(auth); } @Post('restore') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() restoreTrash(@Auth() auth: AuthDto): Promise { return this.service.restore(auth); } @Post('restore/assets') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.restoreAssets(auth, dto); } diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index c108e88527504..8b1abbb26d72b 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -19,7 +19,7 @@ import { NextFunction, Response } from 'express'; import { AuthDto } from 'src/dtos/auth.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto'; -import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor'; import { UserService } from 'src/services/user.service'; import { sendFile } from 'src/utils/file'; @@ -27,39 +27,42 @@ import { UUIDParamDto } from 'src/validation'; @ApiTags('User') @Controller(Route.USER) -@Authenticated() export class UserController { constructor(private service: UserService) {} @Get() + @Authenticated() getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise { return this.service.getAll(auth, isAll); } @Get('info/:id') + @Authenticated() getUserById(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } @Get('me') + @Authenticated() getMyUserInfo(@Auth() auth: AuthDto): Promise { return this.service.getMe(auth); } - @AdminRoute() @Post() + @Authenticated({ admin: true }) createUser(@Body() createUserDto: CreateUserDto): Promise { return this.service.create(createUserDto); } @Delete('profile-image') @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated() deleteProfileImage(@Auth() auth: AuthDto): Promise { return this.service.deleteProfileImage(auth); } - @AdminRoute() @Delete(':id') + @Authenticated({ admin: true }) deleteUser( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -68,14 +71,15 @@ export class UserController { return this.service.delete(auth, id, dto); } - @AdminRoute() @Post(':id/restore') + @Authenticated({ admin: true }) restoreUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.restore(auth, id); } // TODO: replace with @Put(':id') @Put() + @Authenticated() updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateUserDto): Promise { return this.service.update(auth, updateUserDto); } @@ -84,6 +88,7 @@ export class UserController { @ApiConsumes('multipart/form-data') @ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto }) @Post('profile-image') + @Authenticated() createProfileImage( @Auth() auth: AuthDto, @UploadedFile() fileInfo: Express.Multer.File, @@ -93,6 +98,7 @@ export class UserController { @Get('profile-image/:id') @FileResponse() + @Authenticated() async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) { await sendFile(res, next, () => this.service.getProfileImage(id)); } diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 73843729b96a7..c7f7893e208e8 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -17,10 +17,14 @@ export enum ImmichHeader { API_KEY = 'x-api-key', USER_TOKEN = 'x-immich-user-token', SESSION_TOKEN = 'x-immich-session-token', - SHARED_LINK_TOKEN = 'x-immich-share-key', + SHARED_LINK_KEY = 'x-immich-share-key', CHECKSUM = 'x-immich-checksum', } +export enum ImmichQuery { + SHARED_LINK_KEY = 'key', +} + export type CookieResponse = { isSecure: boolean; values: Array<{ key: ImmichCookie; value: string }>; diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index 59e82c00aa956..cbea439489c9d 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -10,7 +10,7 @@ import { import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; -import { AuthDto } from 'src/dtos/auth.dto'; +import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { UAParser } from 'ua-parser-js'; @@ -19,42 +19,30 @@ export enum Metadata { AUTH_ROUTE = 'auth_route', ADMIN_ROUTE = 'admin_route', SHARED_ROUTE = 'shared_route', - PUBLIC_SECURITY = 'public_security', API_KEY_SECURITY = 'api_key', } -export interface AuthenticatedOptions { - admin?: true; - isShared?: true; -} +type AdminRoute = { admin?: true }; +type SharedLinkRoute = { sharedLink?: true }; +type AuthenticatedOptions = AdminRoute | SharedLinkRoute; -export const Authenticated = (options: AuthenticatedOptions = {}) => { +export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator => { const decorators: MethodDecorator[] = [ ApiBearerAuth(), ApiCookieAuth(), ApiSecurity(Metadata.API_KEY_SECURITY), - SetMetadata(Metadata.AUTH_ROUTE, true), + SetMetadata(Metadata.AUTH_ROUTE, options || {}), ]; - if (options.admin) { - decorators.push(AdminRoute()); - } - - if (options.isShared) { - decorators.push(SharedLinkRoute()); + if ((options as SharedLinkRoute)?.sharedLink) { + decorators.push(ApiQuery({ name: ImmichQuery.SHARED_LINK_KEY, type: String, required: false })); } return applyDecorators(...decorators); }; -export const PublicRoute = () => - applyDecorators(SetMetadata(Metadata.AUTH_ROUTE, false), ApiSecurity(Metadata.PUBLIC_SECURITY)); -export const SharedLinkRoute = () => - applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false })); -export const AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value); - export const Auth = createParamDecorator((data, context: ExecutionContext): AuthDto => { - return context.switchToHttp().getRequest<{ user: AuthDto }>().user; + return context.switchToHttp().getRequest().user; }); export const FileResponse = () => @@ -93,25 +81,22 @@ export class AuthGuard implements CanActivate { } async canActivate(context: ExecutionContext): Promise { - const targets = [context.getHandler(), context.getClass()]; - - const isAuthRoute = this.reflector.getAllAndOverride(Metadata.AUTH_ROUTE, targets); - const isAdminRoute = this.reflector.getAllAndOverride(Metadata.ADMIN_ROUTE, targets); - const isSharedRoute = this.reflector.getAllAndOverride(Metadata.SHARED_ROUTE, targets); + const targets = [context.getHandler()]; - if (!isAuthRoute) { + const options = this.reflector.getAllAndOverride(Metadata.AUTH_ROUTE, targets); + if (!options) { return true; } const request = context.switchToHttp().getRequest(); const authDto = await this.authService.validate(request.headers, request.query as Record); - if (authDto.sharedLink && !isSharedRoute) { + if (authDto.sharedLink && !(options as SharedLinkRoute).sharedLink) { this.logger.warn(`Denied access to non-shared route: ${request.path}`); return false; } - if (isAdminRoute && !authDto.user.isAdmin) { + if (!authDto.user.isAdmin && (options as AdminRoute).admin) { this.logger.warn(`Denied access to admin only route: ${request.path}`); return false; } diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 72fee12f45522..b2c524decd62b 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -149,7 +149,7 @@ export class AuthService { } async validate(headers: IncomingHttpHeaders, params: Record): Promise { - const shareKey = (headers[ImmichHeader.SHARED_LINK_TOKEN] || params.key) as string; + const shareKey = (headers[ImmichHeader.SHARED_LINK_KEY] || params.key) as string; const session = (headers[ImmichHeader.USER_TOKEN] || headers[ImmichHeader.SESSION_TOKEN] || params.sessionKey || diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 8262b6024b5dc..95eefe703962a 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -103,10 +103,6 @@ const patchOpenAPI = (document: OpenAPIObject) => { continue; } - if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) { - delete operation.security; - } - if (operation.summary === '') { delete operation.summary; }