Browse Source

refactor(server): guards, decorators, and utils (#3060)

Jason Rasmussen 2 years ago
parent
commit
d69fa3ceae
54 changed files with 243 additions and 255 deletions
  1. 1 1
      server/e2e/album.e2e-spec.ts
  2. 1 1
      server/src/domain/album/dto/album-add-users.dto.ts
  3. 1 1
      server/src/domain/album/dto/album-create.dto.ts
  4. 1 1
      server/src/domain/album/dto/album-update.dto.ts
  5. 1 2
      server/src/domain/album/dto/get-albums.dto.ts
  6. 1 1
      server/src/domain/asset/dto/asset-ids.dto.ts
  7. 1 1
      server/src/domain/asset/dto/download.dto.ts
  8. 1 1
      server/src/domain/asset/dto/map-marker.dto.ts
  9. 35 0
      server/src/domain/domain.util.ts
  10. 1 1
      server/src/domain/search/dto/search.dto.ts
  11. 1 1
      server/src/domain/shared-link/shared-link.dto.ts
  12. 1 1
      server/src/domain/user/dto/create-user.dto.ts
  13. 1 1
      server/src/domain/user/dto/update-user.dto.ts
  14. 3 4
      server/src/immich/api-v1/album/album.controller.ts
  15. 1 2
      server/src/immich/api-v1/album/album.service.spec.ts
  16. 1 2
      server/src/immich/api-v1/album/album.service.ts
  17. 1 1
      server/src/immich/api-v1/album/dto/add-assets.dto.ts
  18. 1 1
      server/src/immich/api-v1/album/dto/add-users.dto.ts
  19. 1 1
      server/src/immich/api-v1/album/dto/remove-assets.dto.ts
  20. 2 3
      server/src/immich/api-v1/asset/asset.controller.ts
  21. 1 1
      server/src/immich/api-v1/asset/dto/asset-search.dto.ts
  22. 1 1
      server/src/immich/api-v1/asset/dto/create-asset.dto.ts
  23. 1 1
      server/src/immich/api-v1/asset/dto/get-asset-by-time-bucket.dto.ts
  24. 1 1
      server/src/immich/api-v1/asset/dto/get-asset-count-by-time-bucket.dto.ts
  25. 1 1
      server/src/immich/api-v1/asset/dto/serve-file.dto.ts
  26. 118 0
      server/src/immich/app.guard.ts
  27. 3 3
      server/src/immich/app.module.ts
  28. 18 1
      server/src/immich/app.utils.ts
  29. 1 1
      server/src/immich/config/asset-upload.config.spec.ts
  30. 3 3
      server/src/immich/config/asset-upload.config.ts
  31. 1 1
      server/src/immich/config/profile-image-upload.config.spec.ts
  32. 3 3
      server/src/immich/config/profile-image-upload.config.ts
  33. 2 3
      server/src/immich/controllers/album.controller.ts
  34. 2 3
      server/src/immich/controllers/api-key.controller.ts
  35. 2 4
      server/src/immich/controllers/asset.controller.ts
  36. 2 3
      server/src/immich/controllers/auth.controller.ts
  37. 2 2
      server/src/immich/controllers/job.controller.ts
  38. 2 3
      server/src/immich/controllers/oauth.controller.ts
  39. 3 4
      server/src/immich/controllers/partner.controller.ts
  40. 2 3
      server/src/immich/controllers/person.controller.ts
  41. 2 3
      server/src/immich/controllers/search.controller.ts
  42. 2 2
      server/src/immich/controllers/server-info.controller.ts
  43. 2 3
      server/src/immich/controllers/shared-link.controller.ts
  44. 2 2
      server/src/immich/controllers/system-config.controller.ts
  45. 3 3
      server/src/immich/controllers/tag.controller.ts
  46. 3 3
      server/src/immich/controllers/user.controller.ts
  47. 0 25
      server/src/immich/decorators/auth-user.decorator.ts
  48. 0 46
      server/src/immich/decorators/authenticated.decorator.ts
  49. 0 12
      server/src/immich/decorators/use-validation.decorator.ts
  50. 0 17
      server/src/immich/decorators/validate-uuid.decorator.ts
  51. 0 46
      server/src/immich/middlewares/auth.guard.ts
  52. 0 3
      server/src/immich/utils/path-form-data.util.ts
  53. 0 18
      server/src/immich/utils/transform.util.ts
  54. 3 3
      server/test/test-utils.ts

+ 1 - 1
server/e2e/album.e2e-spec.ts

@@ -1,13 +1,13 @@
 import {
   AlbumResponseDto,
   AuthService,
+  AuthUserDto,
   CreateAlbumDto,
   SharedLinkCreateDto,
   SharedLinkResponseDto,
   UserService,
 } from '@app/domain';
 import { AppModule } from '@app/immich/app.module';
-import { AuthUserDto } from '@app/immich/decorators/auth-user.decorator';
 import { SharedLinkType } from '@app/infra/entities';
 import { INestApplication } from '@nestjs/common';
 import { Test, TestingModule } from '@nestjs/testing';

+ 1 - 1
server/src/domain/album/dto/album-add-users.dto.ts

@@ -1,5 +1,5 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
 import { ArrayNotEmpty } from 'class-validator';
+import { ValidateUUID } from '../../domain.util';
 
 export class AddUsersDto {
   @ValidateUUID({ each: true })

+ 1 - 1
server/src/domain/album/dto/album-create.dto.ts

@@ -1,6 +1,6 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
 import { ApiProperty } from '@nestjs/swagger';
 import { IsNotEmpty, IsString } from 'class-validator';
+import { ValidateUUID } from '../../domain.util';
 
 export class CreateAlbumDto {
   @IsNotEmpty()

+ 1 - 1
server/src/domain/album/dto/album-update.dto.ts

@@ -1,6 +1,6 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
 import { ApiProperty } from '@nestjs/swagger';
 import { IsOptional } from 'class-validator';
+import { ValidateUUID } from '../../domain.util';
 
 export class UpdateAlbumDto {
   @IsOptional()

+ 1 - 2
server/src/domain/album/dto/get-albums.dto.ts

@@ -1,8 +1,7 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
-import { toBoolean } from '@app/immich/utils/transform.util';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
 import { IsBoolean, IsOptional } from 'class-validator';
+import { toBoolean, ValidateUUID } from '../../domain.util';
 
 export class GetAlbumsDto {
   @IsOptional()

+ 1 - 1
server/src/domain/asset/dto/asset-ids.dto.ts

@@ -1,4 +1,4 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
+import { ValidateUUID } from '../../domain.util';
 
 export class AssetIdsDto {
   @ValidateUUID({ each: true })

+ 1 - 1
server/src/domain/asset/dto/download.dto.ts

@@ -1,6 +1,6 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
 import { ApiProperty } from '@nestjs/swagger';
 import { IsInt, IsOptional, IsPositive } from 'class-validator';
+import { ValidateUUID } from '../../domain.util';
 
 export class DownloadDto {
   @ValidateUUID({ each: true, optional: true })

+ 1 - 1
server/src/domain/asset/dto/map-marker.dto.ts

@@ -1,7 +1,7 @@
-import { toBoolean } from '@app/immich/utils/transform.util';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform, Type } from 'class-transformer';
 import { IsBoolean, IsDate, IsOptional } from 'class-validator';
+import { toBoolean } from '../../domain.util';
 
 export class MapMarkerDto {
   @ApiProperty()

+ 35 - 0
server/src/domain/domain.util.ts

@@ -1,4 +1,39 @@
+import { applyDecorators } from '@nestjs/common';
+import { ApiProperty } from '@nestjs/swagger';
+import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
 import { basename, extname } from 'node:path';
+import sanitize from 'sanitize-filename';
+
+export type Options = {
+  optional?: boolean;
+  each?: boolean;
+};
+
+export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) {
+  return applyDecorators(
+    IsUUID('4', { each }),
+    ApiProperty({ format: 'uuid' }),
+    optional ? IsOptional() : IsNotEmpty(),
+    each ? IsArray() : IsString(),
+  );
+}
+
+interface IValue {
+  value?: string;
+}
+
+export const toBoolean = ({ value }: IValue) => {
+  if (value == 'true') {
+    return true;
+  } else if (value == 'false') {
+    return false;
+  }
+  return value;
+};
+
+export const toEmail = ({ value }: IValue) => value?.toLowerCase();
+
+export const toSanitized = ({ value }: IValue) => sanitize((value || '').replace(/\./g, ''));
 
 export function getFileNameWithoutExtension(path: string): string {
   return basename(path, extname(path));

+ 1 - 1
server/src/domain/search/dto/search.dto.ts

@@ -1,7 +1,7 @@
-import { toBoolean } from '@app/immich/utils/transform.util';
 import { AssetType } from '@app/infra/entities';
 import { Transform } from 'class-transformer';
 import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
+import { toBoolean } from '../../domain.util';
 
 export class SearchDto {
   @IsString()

+ 1 - 1
server/src/domain/shared-link/shared-link.dto.ts

@@ -2,7 +2,7 @@ import { SharedLinkType } from '@app/infra/entities';
 import { ApiProperty } from '@nestjs/swagger';
 import { Type } from 'class-transformer';
 import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator';
-import { ValidateUUID } from '../../immich/decorators/validate-uuid.decorator';
+import { ValidateUUID } from '../domain.util';
 
 export class SharedLinkCreateDto {
   @IsEnum(SharedLinkType)

+ 1 - 1
server/src/domain/user/dto/create-user.dto.ts

@@ -1,6 +1,6 @@
-import { toEmail, toSanitized } from '@app/immich/utils/transform.util';
 import { Transform } from 'class-transformer';
 import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
+import { toEmail, toSanitized } from '../../domain.util';
 
 export class CreateUserDto {
   @IsEmail({ require_tld: false })

+ 1 - 1
server/src/domain/user/dto/update-user.dto.ts

@@ -1,7 +1,7 @@
-import { toEmail, toSanitized } from '@app/immich/utils/transform.util';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
 import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
+import { toEmail, toSanitized } from '../../domain.util';
 
 export class UpdateUserDto {
   @IsOptional()

+ 3 - 4
server/src/immich/api-v1/album/album.controller.ts

@@ -1,10 +1,9 @@
-import { AlbumResponseDto } from '@app/domain';
+import { AlbumResponseDto, AuthUserDto } from '@app/domain';
 import { Body, Controller, Delete, Get, Param, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
+import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard';
+import { UseValidation } from '../../app.utils';
 import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
-import { AuthUser, AuthUserDto } from '../../decorators/auth-user.decorator';
-import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator';
-import { UseValidation } from '../../decorators/use-validation.decorator';
 import { AlbumService } from './album.service';
 import { AddAssetsDto } from './dto/add-assets.dto';
 import { RemoveAssetsDto } from './dto/remove-assets.dto';

+ 1 - 2
server/src/immich/api-v1/album/album.service.spec.ts

@@ -1,8 +1,7 @@
-import { AlbumResponseDto, mapUser } from '@app/domain';
+import { AlbumResponseDto, AuthUserDto, mapUser } from '@app/domain';
 import { AlbumEntity, UserEntity } from '@app/infra/entities';
 import { ForbiddenException, NotFoundException } from '@nestjs/common';
 import { userEntityStub } from '@test';
-import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { IAlbumRepository } from './album-repository';
 import { AlbumService } from './album.service';
 import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';

+ 1 - 2
server/src/immich/api-v1/album/album.service.ts

@@ -1,7 +1,6 @@
-import { AlbumResponseDto, mapAlbum } from '@app/domain';
+import { AlbumResponseDto, AuthUserDto, mapAlbum } from '@app/domain';
 import { AlbumEntity } from '@app/infra/entities';
 import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
-import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { IAlbumRepository } from './album-repository';
 import { AddAssetsDto } from './dto/add-assets.dto';
 import { RemoveAssetsDto } from './dto/remove-assets.dto';

+ 1 - 1
server/src/immich/api-v1/album/dto/add-assets.dto.ts

@@ -1,4 +1,4 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
+import { ValidateUUID } from '@app/domain';
 
 export class AddAssetsDto {
   @ValidateUUID({ each: true })

+ 1 - 1
server/src/immich/api-v1/album/dto/add-users.dto.ts

@@ -1,4 +1,4 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
+import { ValidateUUID } from '@app/domain';
 
 export class AddUsersDto {
   @ValidateUUID({ each: true })

+ 1 - 1
server/src/immich/api-v1/album/dto/remove-assets.dto.ts

@@ -1,4 +1,4 @@
-import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
+import { ValidateUUID } from '@app/domain';
 
 export class RemoveAssetsDto {
   @ValidateUUID({ each: true })

+ 2 - 3
server/src/immich/api-v1/asset/asset.controller.ts

@@ -1,4 +1,4 @@
-import { AssetResponseDto } from '@app/domain';
+import { AssetResponseDto, AuthUserDto } from '@app/domain';
 import {
   Body,
   Controller,
@@ -21,10 +21,9 @@ import {
 import { FileFieldsInterceptor } from '@nestjs/platform-express';
 import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
 import { Response as Res } from 'express';
+import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard';
 import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
 import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
-import { AuthUser, AuthUserDto } from '../../decorators/auth-user.decorator';
-import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator';
 import FileNotEmptyValidator from '../validation/file-not-empty-validator';
 import { AssetService } from './asset.service';
 import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';

+ 1 - 1
server/src/immich/api-v1/asset/dto/asset-search.dto.ts

@@ -1,7 +1,7 @@
+import { toBoolean } from '@app/domain';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
 import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator';
-import { toBoolean } from '../../../utils/transform.util';
 
 export class AssetSearchDto {
   @IsOptional()

+ 1 - 1
server/src/immich/api-v1/asset/dto/create-asset.dto.ts

@@ -1,9 +1,9 @@
+import { toBoolean, toSanitized } from '@app/domain';
 import { AssetType } from '@app/infra/entities';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
 import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
 import { ImmichFile } from '../../../config/asset-upload.config';
-import { toBoolean, toSanitized } from '../../../utils/transform.util';
 
 export class CreateAssetBase {
   @IsNotEmpty()

+ 1 - 1
server/src/immich/api-v1/asset/dto/get-asset-by-time-bucket.dto.ts

@@ -1,7 +1,7 @@
+import { toBoolean } from '@app/domain';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
 import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
-import { toBoolean } from '../../../utils/transform.util';
 
 export class GetAssetByTimeBucketDto {
   @IsNotEmpty()

+ 1 - 1
server/src/immich/api-v1/asset/dto/get-asset-count-by-time-bucket.dto.ts

@@ -1,7 +1,7 @@
+import { toBoolean } from '@app/domain';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
 import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
-import { toBoolean } from '../../../utils/transform.util';
 
 export enum TimeGroupEnum {
   Day = 'day',

+ 1 - 1
server/src/immich/api-v1/asset/dto/serve-file.dto.ts

@@ -1,7 +1,7 @@
+import { toBoolean } from '@app/domain';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
 import { IsBoolean, IsOptional } from 'class-validator';
-import { toBoolean } from '../../../utils/transform.util';
 
 export class ServeFileDto {
   @IsOptional()

+ 118 - 0
server/src/immich/app.guard.ts

@@ -0,0 +1,118 @@
+import { AuthService, AuthUserDto, IMMICH_API_KEY_NAME, LoginDetails } from '@app/domain';
+import {
+  applyDecorators,
+  CanActivate,
+  createParamDecorator,
+  ExecutionContext,
+  Injectable,
+  Logger,
+  SetMetadata,
+} from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { ApiBearerAuth, ApiCookieAuth, ApiQuery, ApiSecurity } from '@nestjs/swagger';
+import { Request } from 'express';
+import { UAParser } from 'ua-parser-js';
+
+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 interface AuthenticatedOptions {
+  admin?: boolean;
+  isShared?: boolean;
+}
+
+export const Authenticated = (options: AuthenticatedOptions = {}) => {
+  const decorators: MethodDecorator[] = [
+    ApiBearerAuth(),
+    ApiCookieAuth(),
+    ApiSecurity(IMMICH_API_KEY_NAME),
+    SetMetadata(Metadata.AUTH_ROUTE, true),
+  ];
+
+  if (options.admin) {
+    decorators.push(adminDecorator);
+  }
+
+  if (options.isShared) {
+    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;
+
+export const AuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => {
+  return ctx.switchToHttp().getRequest<{ user: AuthUserDto }>().user;
+});
+
+export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => {
+  const req = ctx.switchToHttp().getRequest();
+  const userAgent = UAParser(req.headers['user-agent']);
+
+  return {
+    clientIp: req.clientIp,
+    isSecure: req.secure,
+    deviceType: userAgent.browser.name || userAgent.device.type || req.headers.devicemodel || '',
+    deviceOS: userAgent.os.name || req.headers.devicetype || '',
+  };
+});
+
+export interface AuthRequest extends Request {
+  user?: AuthUserDto;
+}
+
+@Injectable()
+export class AppGuard implements CanActivate {
+  private logger = new Logger(AppGuard.name);
+
+  constructor(private reflector: Reflector, private authService: AuthService) {}
+
+  async canActivate(context: ExecutionContext): Promise<boolean> {
+    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);
+
+    if (!isAuthRoute) {
+      return true;
+    }
+
+    const req = context.switchToHttp().getRequest<AuthRequest>();
+
+    const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>);
+    if (!authDto) {
+      this.logger.warn(`Denied access to authenticated route: ${req.path}`);
+      return false;
+    }
+
+    if (authDto.isPublicUser && !isSharedRoute) {
+      this.logger.warn(`Denied access to non-shared route: ${req.path}`);
+      return false;
+    }
+
+    if (isAdminRoute && !authDto.isAdmin) {
+      this.logger.warn(`Denied access to admin only route: ${req.path}`);
+      return false;
+    }
+
+    req.user = authDto;
+
+    return true;
+  }
+}

+ 3 - 3
server/src/immich/app.module.ts

@@ -5,6 +5,7 @@ import { APP_GUARD } from '@nestjs/core';
 import { ScheduleModule } from '@nestjs/schedule';
 import { AlbumModule } from './api-v1/album/album.module';
 import { AssetModule } from './api-v1/asset/asset.module';
+import { AppGuard } from './app.guard';
 import { AppService } from './app.service';
 import {
   AlbumController,
@@ -23,7 +24,6 @@ import {
   TagController,
   UserController,
 } from './controllers';
-import { AuthGuard } from './middlewares/auth.guard';
 
 @Module({
   imports: [
@@ -52,8 +52,8 @@ import { AuthGuard } from './middlewares/auth.guard';
   ],
   providers: [
     //
-    { provide: APP_GUARD, useExisting: AuthGuard },
-    AuthGuard,
+    { provide: APP_GUARD, useExisting: AppGuard },
+    AppGuard,
     AppService,
   ],
 })

+ 18 - 1
server/src/immich/app.utils.ts

@@ -15,12 +15,29 @@ import {
 } from '@nestjs/swagger';
 import { writeFileSync } from 'fs';
 import path from 'path';
-import { Metadata } from './decorators/authenticated.decorator';
+
+import { applyDecorators, UsePipes, ValidationPipe } from '@nestjs/common';
+import { Metadata } from './app.guard';
+
+export function UseValidation() {
+  return applyDecorators(
+    UsePipes(
+      new ValidationPipe({
+        transform: true,
+        whitelist: true,
+      }),
+    ),
+  );
+}
 
 export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
   return new StreamableFile(stream, { type, length });
 };
 
+export function patchFormData(latin1: string) {
+  return Buffer.from(latin1, 'latin1').toString('utf8');
+}
+
 function sortKeys<T extends object>(obj: T): T {
   if (!obj) {
     return obj;

+ 1 - 1
server/src/immich/config/asset-upload.config.spec.ts

@@ -1,6 +1,6 @@
 import { Request } from 'express';
 import * as fs from 'fs';
-import { AuthRequest } from '../decorators/auth-user.decorator';
+import { AuthRequest } from '../app.guard';
 import { multerUtils } from './asset-upload.config';
 
 const { fileFilter, destination, filename } = multerUtils;

+ 3 - 3
server/src/immich/config/asset-upload.config.ts

@@ -1,4 +1,4 @@
-import { isSidecarFileType, isSupportedFileType } from '@app/domain';
+import { AuthUserDto, isSidecarFileType, isSupportedFileType } from '@app/domain';
 import { StorageCore, StorageFolder } from '@app/domain/storage';
 import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
 import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
@@ -7,8 +7,8 @@ import { existsSync, mkdirSync } from 'fs';
 import { diskStorage, StorageEngine } from 'multer';
 import { extname } from 'path';
 import sanitize from 'sanitize-filename';
-import { AuthRequest, AuthUserDto } from '../decorators/auth-user.decorator';
-import { patchFormData } from '../utils/path-form-data.util';
+import { AuthRequest } from '../app.guard';
+import { patchFormData } from '../app.utils';
 
 export interface ImmichFile extends Express.Multer.File {
   /** sha1 hash of file */

+ 1 - 1
server/src/immich/config/profile-image-upload.config.spec.ts

@@ -1,6 +1,6 @@
 import { Request } from 'express';
 import * as fs from 'fs';
-import { AuthRequest } from '../decorators/auth-user.decorator';
+import { AuthRequest } from '../app.guard';
 import { multerUtils } from './profile-image-upload.config';
 
 const { fileFilter, destination, filename } = multerUtils;

+ 3 - 3
server/src/immich/config/profile-image-upload.config.ts

@@ -1,12 +1,12 @@
-import { StorageCore, StorageFolder } from '@app/domain/storage';
+import { AuthUserDto, StorageCore, StorageFolder } from '@app/domain';
 import { BadRequestException, UnauthorizedException } from '@nestjs/common';
 import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
 import { existsSync, mkdirSync } from 'fs';
 import { diskStorage } from 'multer';
 import { extname } from 'path';
 import sanitize from 'sanitize-filename';
-import { AuthRequest, AuthUserDto } from '../decorators/auth-user.decorator';
-import { patchFormData } from '../utils/path-form-data.util';
+import { AuthRequest } from '../app.guard';
+import { patchFormData } from '../app.utils';
 
 export const profileImageUploadOption: MulterOptions = {
   fileFilter,

+ 2 - 3
server/src/immich/controllers/album.controller.ts

@@ -10,9 +10,8 @@ import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto';
 import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe';
-import { AuthUser } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser } from '../app.guard';
+import { UseValidation } from '../app.utils';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('Album')

+ 2 - 3
server/src/immich/controllers/api-key.controller.ts

@@ -8,9 +8,8 @@ import {
 } from '@app/domain';
 import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { AuthUser } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser } from '../app.guard';
+import { UseValidation } from '../app.utils';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('API Key')

+ 2 - 4
server/src/immich/controllers/asset.controller.ts

@@ -11,10 +11,8 @@ import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
 import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto';
 import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, StreamableFile } from '@nestjs/common';
 import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
-import { asStreamableFile } from '../app.utils';
-import { AuthUser } from '../decorators/auth-user.decorator';
-import { Authenticated, SharedLinkRoute } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard';
+import { asStreamableFile, UseValidation } from '../app.utils';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('Asset')

+ 2 - 3
server/src/immich/controllers/auth.controller.ts

@@ -18,9 +18,8 @@ import {
 import { Body, Controller, Delete, Get, Param, Post, Req, Res } from '@nestjs/common';
 import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger';
 import { Request, Response } from 'express';
-import { AuthUser, GetLoginDetails } from '../decorators/auth-user.decorator';
-import { Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser, GetLoginDetails, PublicRoute } from '../app.guard';
+import { UseValidation } from '../app.utils';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('Authentication')

+ 2 - 2
server/src/immich/controllers/job.controller.ts

@@ -1,8 +1,8 @@
 import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobService, JobStatusDto } from '@app/domain';
 import { Body, Controller, Get, Param, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { Authenticated } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated } from '../app.guard';
+import { UseValidation } from '../app.utils';
 
 @ApiTags('Job')
 @Controller('jobs')

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

@@ -11,9 +11,8 @@ import {
 import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import { Request, Response } from 'express';
-import { AuthUser, GetLoginDetails } from '../decorators/auth-user.decorator';
-import { Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser, GetLoginDetails, PublicRoute } from '../app.guard';
+import { UseValidation } from '../app.utils';
 
 @ApiTags('OAuth')
 @Controller('oauth')

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

@@ -1,9 +1,8 @@
-import { PartnerDirection, PartnerService, UserResponseDto } from '@app/domain';
+import { AuthUserDto, PartnerDirection, PartnerService, UserResponseDto } from '@app/domain';
 import { Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
 import { ApiQuery, ApiTags } from '@nestjs/swagger';
-import { AuthUser, AuthUserDto } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser } from '../app.guard';
+import { UseValidation } from '../app.utils';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('Partner')

+ 2 - 3
server/src/immich/controllers/person.controller.ts

@@ -8,9 +8,8 @@ import {
 } from '@app/domain';
 import { Body, Controller, Get, Param, Put, StreamableFile } from '@nestjs/common';
 import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
-import { AuthUser } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser } from '../app.guard';
+import { UseValidation } from '../app.utils';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 function asStreamableFile({ stream, type, length }: ImmichReadStream) {

+ 2 - 3
server/src/immich/controllers/search.controller.ts

@@ -8,9 +8,8 @@ import {
 } from '@app/domain';
 import { Controller, Get, Query } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { AuthUser } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser } from '../app.guard';
+import { UseValidation } from '../app.utils';
 
 @ApiTags('Search')
 @Controller('search')

+ 2 - 2
server/src/immich/controllers/server-info.controller.ts

@@ -7,8 +7,8 @@ import {
 } from '@app/domain';
 import { Controller, Get } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { AdminRoute, Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { AdminRoute, Authenticated, PublicRoute } from '../app.guard';
+import { UseValidation } from '../app.utils';
 
 @ApiTags('Server Info')
 @Controller('server-info')

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

@@ -9,9 +9,8 @@ import {
 } from '@app/domain';
 import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { AuthUser } from '../decorators/auth-user.decorator';
-import { Authenticated, SharedLinkRoute } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard';
+import { UseValidation } from '../app.utils';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('Shared Link')

+ 2 - 2
server/src/immich/controllers/system-config.controller.ts

@@ -1,8 +1,8 @@
 import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain';
 import { Body, Controller, Get, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { Authenticated } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated } from '../app.guard';
+import { UseValidation } from '../app.utils';
 
 @ApiTags('System Config')
 @Controller('system-config')

+ 3 - 3
server/src/immich/controllers/tag.controller.ts

@@ -2,6 +2,7 @@ import {
   AssetIdsDto,
   AssetIdsResponseDto,
   AssetResponseDto,
+  AuthUserDto,
   CreateTagDto,
   TagResponseDto,
   TagService,
@@ -9,9 +10,8 @@ import {
 } from '@app/domain';
 import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { AuthUser, AuthUserDto } from '../decorators/auth-user.decorator';
-import { Authenticated } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
+import { Authenticated, AuthUser } from '../app.guard';
+import { UseValidation } from '../app.utils';
 import { UUIDParamDto } from './dto/uuid-param.dto';
 
 @ApiTags('Tag')

+ 3 - 3
server/src/immich/controllers/user.controller.ts

@@ -1,4 +1,5 @@
 import {
+  AuthUserDto,
   CreateProfileImageDto,
   CreateProfileImageResponseDto,
   CreateUserDto,
@@ -27,10 +28,9 @@ import {
 import { FileInterceptor } from '@nestjs/platform-express';
 import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
 import { Response as Res } from 'express';
+import { AdminRoute, Authenticated, AuthUser, PublicRoute } from '../app.guard';
+import { UseValidation } from '../app.utils';
 import { profileImageUploadOption } from '../config/profile-image-upload.config';
-import { AuthUser, AuthUserDto } from '../decorators/auth-user.decorator';
-import { AdminRoute, Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
-import { UseValidation } from '../decorators/use-validation.decorator';
 
 @ApiTags('User')
 @Controller('user')

+ 0 - 25
server/src/immich/decorators/auth-user.decorator.ts

@@ -1,25 +0,0 @@
-export { AuthUserDto } from '@app/domain';
-import { AuthUserDto, LoginDetails } from '@app/domain';
-import { createParamDecorator, ExecutionContext } from '@nestjs/common';
-import { Request } from 'express';
-import { UAParser } from 'ua-parser-js';
-
-export interface AuthRequest extends Request {
-  user?: AuthUserDto;
-}
-
-export const AuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => {
-  return ctx.switchToHttp().getRequest<{ user: AuthUserDto }>().user;
-});
-
-export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => {
-  const req = ctx.switchToHttp().getRequest();
-  const userAgent = UAParser(req.headers['user-agent']);
-
-  return {
-    clientIp: req.clientIp,
-    isSecure: req.secure,
-    deviceType: userAgent.browser.name || userAgent.device.type || req.headers.devicemodel || '',
-    deviceOS: userAgent.os.name || req.headers.devicetype || '',
-  };
-});

+ 0 - 46
server/src/immich/decorators/authenticated.decorator.ts

@@ -1,46 +0,0 @@
-import { IMMICH_API_KEY_NAME } from '@app/domain';
-import { applyDecorators, SetMetadata } from '@nestjs/common';
-import { ApiBearerAuth, ApiCookieAuth, ApiQuery, ApiSecurity } from '@nestjs/swagger';
-
-interface AuthenticatedOptions {
-  admin?: boolean;
-  isShared?: boolean;
-}
-
-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(),
-    ApiCookieAuth(),
-    ApiSecurity(IMMICH_API_KEY_NAME),
-    SetMetadata(Metadata.AUTH_ROUTE, true),
-  ];
-
-  if (options.admin) {
-    decorators.push(adminDecorator);
-  }
-
-  if (options.isShared) {
-    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;

+ 0 - 12
server/src/immich/decorators/use-validation.decorator.ts

@@ -1,12 +0,0 @@
-import { applyDecorators, UsePipes, ValidationPipe } from '@nestjs/common';
-
-export function UseValidation() {
-  return applyDecorators(
-    UsePipes(
-      new ValidationPipe({
-        transform: true,
-        whitelist: true,
-      }),
-    ),
-  );
-}

+ 0 - 17
server/src/immich/decorators/validate-uuid.decorator.ts

@@ -1,17 +0,0 @@
-import { applyDecorators } from '@nestjs/common';
-import { ApiProperty } from '@nestjs/swagger';
-import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
-
-export type Options = {
-  optional?: boolean;
-  each?: boolean;
-};
-
-export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) {
-  return applyDecorators(
-    IsUUID('4', { each }),
-    ApiProperty({ format: 'uuid' }),
-    optional ? IsOptional() : IsNotEmpty(),
-    each ? IsArray() : IsString(),
-  );
-}

+ 0 - 46
server/src/immich/middlewares/auth.guard.ts

@@ -1,46 +0,0 @@
-import { AuthService } from '@app/domain';
-import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
-import { Reflector } from '@nestjs/core';
-import { AuthRequest } from '../decorators/auth-user.decorator';
-import { Metadata } from '../decorators/authenticated.decorator';
-
-@Injectable()
-export class AuthGuard implements CanActivate {
-  private logger = new Logger(AuthGuard.name);
-
-  constructor(private reflector: Reflector, private authService: AuthService) {}
-
-  async canActivate(context: ExecutionContext): Promise<boolean> {
-    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);
-
-    if (!isAuthRoute) {
-      return true;
-    }
-
-    const req = context.switchToHttp().getRequest<AuthRequest>();
-
-    const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>);
-    if (!authDto) {
-      this.logger.warn(`Denied access to authenticated route: ${req.path}`);
-      return false;
-    }
-
-    if (authDto.isPublicUser && !isSharedRoute) {
-      this.logger.warn(`Denied access to non-shared route: ${req.path}`);
-      return false;
-    }
-
-    if (isAdminRoute && !authDto.isAdmin) {
-      this.logger.warn(`Denied access to admin only route: ${req.path}`);
-      return false;
-    }
-
-    req.user = authDto;
-
-    return true;
-  }
-}

+ 0 - 3
server/src/immich/utils/path-form-data.util.ts

@@ -1,3 +0,0 @@
-export function patchFormData(latin1: string) {
-  return Buffer.from(latin1, 'latin1').toString('utf8');
-}

+ 0 - 18
server/src/immich/utils/transform.util.ts

@@ -1,18 +0,0 @@
-import sanitize from 'sanitize-filename';
-
-interface IValue {
-  value?: string;
-}
-
-export const toBoolean = ({ value }: IValue) => {
-  if (value == 'true') {
-    return true;
-  } else if (value == 'false') {
-    return false;
-  }
-  return value;
-};
-
-export const toEmail = ({ value }: IValue) => value?.toLowerCase();
-
-export const toSanitized = ({ value }: IValue) => sanitize((value || '').replace(/\./g, ''));

+ 3 - 3
server/test/test-utils.ts

@@ -1,5 +1,5 @@
-import { AuthUserDto } from '@app/immich/decorators/auth-user.decorator';
-import { AuthGuard } from '@app/immich/middlewares/auth.guard';
+import { AuthUserDto } from '@app/domain';
+import { AppGuard } from '@app/immich/app.guard';
 import { CanActivate, ExecutionContext } from '@nestjs/common';
 import { TestingModuleBuilder } from '@nestjs/testing';
 import { DataSource } from 'typeorm';
@@ -34,5 +34,5 @@ export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCa
       return true;
     },
   };
-  return builder.overrideProvider(AuthGuard).useValue(canActivate);
+  return builder.overrideProvider(AppGuard).useValue(canActivate);
 }