refactor(server): auth decorator (#2588)

This commit is contained in:
Jason Rasmussen 2023-05-28 12:30:01 -04:00 committed by GitHub
parent e7ad622c02
commit ffe397247e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 62 additions and 71 deletions

View file

@ -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);

View file

@ -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,

View file

@ -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) {}

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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();

View file

@ -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,

View file

@ -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> {

View file

@ -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;

View file

@ -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;
}