mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
refactor(server): guards, decorators, and utils (#3060)
This commit is contained in:
parent
f55b3add80
commit
d69fa3ceae
@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AuthService,
|
AuthService,
|
||||||
|
AuthUserDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
UserService,
|
UserService,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { AppModule } from '@app/immich/app.module';
|
import { AppModule } from '@app/immich/app.module';
|
||||||
import { AuthUserDto } from '@app/immich/decorators/auth-user.decorator';
|
|
||||||
import { SharedLinkType } from '@app/infra/entities';
|
import { SharedLinkType } from '@app/infra/entities';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
|
|
||||||
import { ArrayNotEmpty } from 'class-validator';
|
import { ArrayNotEmpty } from 'class-validator';
|
||||||
|
import { ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class AddUsersDto {
|
export class AddUsersDto {
|
||||||
@ValidateUUID({ each: true })
|
@ValidateUUID({ each: true })
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class CreateAlbumDto {
|
export class CreateAlbumDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsOptional } from 'class-validator';
|
import { IsOptional } from 'class-validator';
|
||||||
|
import { ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class UpdateAlbumDto {
|
export class UpdateAlbumDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -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 { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsOptional } from 'class-validator';
|
import { IsBoolean, IsOptional } from 'class-validator';
|
||||||
|
import { toBoolean, ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class GetAlbumsDto {
|
export class GetAlbumsDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
|
import { ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class AssetIdsDto {
|
export class AssetIdsDto {
|
||||||
@ValidateUUID({ each: true })
|
@ValidateUUID({ each: true })
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsInt, IsOptional, IsPositive } from 'class-validator';
|
import { IsInt, IsOptional, IsPositive } from 'class-validator';
|
||||||
|
import { ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class DownloadDto {
|
export class DownloadDto {
|
||||||
@ValidateUUID({ each: true, optional: true })
|
@ValidateUUID({ each: true, optional: true })
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { toBoolean } from '@app/immich/utils/transform.util';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform, Type } from 'class-transformer';
|
import { Transform, Type } from 'class-transformer';
|
||||||
import { IsBoolean, IsDate, IsOptional } from 'class-validator';
|
import { IsBoolean, IsDate, IsOptional } from 'class-validator';
|
||||||
|
import { toBoolean } from '../../domain.util';
|
||||||
|
|
||||||
export class MapMarkerDto {
|
export class MapMarkerDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
@ -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 { 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 {
|
export function getFileNameWithoutExtension(path: string): string {
|
||||||
return basename(path, extname(path));
|
return basename(path, extname(path));
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { toBoolean } from '@app/immich/utils/transform.util';
|
|
||||||
import { AssetType } from '@app/infra/entities';
|
import { AssetType } from '@app/infra/entities';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { toBoolean } from '../../domain.util';
|
||||||
|
|
||||||
export class SearchDto {
|
export class SearchDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@ -2,7 +2,7 @@ import { SharedLinkType } from '@app/infra/entities';
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator';
|
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 {
|
export class SharedLinkCreateDto {
|
||||||
@IsEnum(SharedLinkType)
|
@IsEnum(SharedLinkType)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { toEmail, toSanitized } from '@app/immich/utils/transform.util';
|
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { toEmail, toSanitized } from '../../domain.util';
|
||||||
|
|
||||||
export class CreateUserDto {
|
export class CreateUserDto {
|
||||||
@IsEmail({ require_tld: false })
|
@IsEmail({ require_tld: false })
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { toEmail, toSanitized } from '@app/immich/utils/transform.util';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
|
import { toEmail, toSanitized } from '../../domain.util';
|
||||||
|
|
||||||
export class UpdateUserDto {
|
export class UpdateUserDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -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 { Body, Controller, Delete, Get, Param, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
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 { 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 { AlbumService } from './album.service';
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
|
@ -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 { AlbumEntity, UserEntity } from '@app/infra/entities';
|
||||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||||
import { userEntityStub } from '@test';
|
import { userEntityStub } from '@test';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|
||||||
import { IAlbumRepository } from './album-repository';
|
import { IAlbumRepository } from './album-repository';
|
||||||
import { AlbumService } from './album.service';
|
import { AlbumService } from './album.service';
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { AlbumResponseDto, mapAlbum } from '@app/domain';
|
import { AlbumResponseDto, AuthUserDto, mapAlbum } from '@app/domain';
|
||||||
import { AlbumEntity } from '@app/infra/entities';
|
import { AlbumEntity } from '@app/infra/entities';
|
||||||
import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|
||||||
import { IAlbumRepository } from './album-repository';
|
import { IAlbumRepository } from './album-repository';
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
|
import { ValidateUUID } from '@app/domain';
|
||||||
|
|
||||||
export class AddAssetsDto {
|
export class AddAssetsDto {
|
||||||
@ValidateUUID({ each: true })
|
@ValidateUUID({ each: true })
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
|
import { ValidateUUID } from '@app/domain';
|
||||||
|
|
||||||
export class AddUsersDto {
|
export class AddUsersDto {
|
||||||
@ValidateUUID({ each: true })
|
@ValidateUUID({ each: true })
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ValidateUUID } from '@app/immich/decorators/validate-uuid.decorator';
|
import { ValidateUUID } from '@app/domain';
|
||||||
|
|
||||||
export class RemoveAssetsDto {
|
export class RemoveAssetsDto {
|
||||||
@ValidateUUID({ each: true })
|
@ValidateUUID({ each: true })
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AssetResponseDto } from '@app/domain';
|
import { AssetResponseDto, AuthUserDto } from '@app/domain';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -21,10 +21,9 @@ import {
|
|||||||
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
||||||
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { Response as Res } from 'express';
|
import { Response as Res } from 'express';
|
||||||
|
import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard';
|
||||||
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
||||||
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
|
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 FileNotEmptyValidator from '../validation/file-not-empty-validator';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { toBoolean } from '@app/domain';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator';
|
import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator';
|
||||||
import { toBoolean } from '../../../utils/transform.util';
|
|
||||||
|
|
||||||
export class AssetSearchDto {
|
export class AssetSearchDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
import { toBoolean, toSanitized } from '@app/domain';
|
||||||
import { AssetType } from '@app/infra/entities';
|
import { AssetType } from '@app/infra/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
import { ImmichFile } from '../../../config/asset-upload.config';
|
import { ImmichFile } from '../../../config/asset-upload.config';
|
||||||
import { toBoolean, toSanitized } from '../../../utils/transform.util';
|
|
||||||
|
|
||||||
export class CreateAssetBase {
|
export class CreateAssetBase {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { toBoolean } from '@app/domain';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
||||||
import { toBoolean } from '../../../utils/transform.util';
|
|
||||||
|
|
||||||
export class GetAssetByTimeBucketDto {
|
export class GetAssetByTimeBucketDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { toBoolean } from '@app/domain';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
||||||
import { toBoolean } from '../../../utils/transform.util';
|
|
||||||
|
|
||||||
export enum TimeGroupEnum {
|
export enum TimeGroupEnum {
|
||||||
Day = 'day',
|
Day = 'day',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { toBoolean } from '@app/domain';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsOptional } from 'class-validator';
|
import { IsBoolean, IsOptional } from 'class-validator';
|
||||||
import { toBoolean } from '../../../utils/transform.util';
|
|
||||||
|
|
||||||
export class ServeFileDto {
|
export class ServeFileDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
118
server/src/immich/app.guard.ts
Normal file
118
server/src/immich/app.guard.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import { APP_GUARD } from '@nestjs/core';
|
|||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { AlbumModule } from './api-v1/album/album.module';
|
import { AlbumModule } from './api-v1/album/album.module';
|
||||||
import { AssetModule } from './api-v1/asset/asset.module';
|
import { AssetModule } from './api-v1/asset/asset.module';
|
||||||
|
import { AppGuard } from './app.guard';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import {
|
import {
|
||||||
AlbumController,
|
AlbumController,
|
||||||
@ -23,7 +24,6 @@ import {
|
|||||||
TagController,
|
TagController,
|
||||||
UserController,
|
UserController,
|
||||||
} from './controllers';
|
} from './controllers';
|
||||||
import { AuthGuard } from './middlewares/auth.guard';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -52,8 +52,8 @@ import { AuthGuard } from './middlewares/auth.guard';
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
//
|
//
|
||||||
{ provide: APP_GUARD, useExisting: AuthGuard },
|
{ provide: APP_GUARD, useExisting: AppGuard },
|
||||||
AuthGuard,
|
AppGuard,
|
||||||
AppService,
|
AppService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -15,12 +15,29 @@ import {
|
|||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { writeFileSync } from 'fs';
|
import { writeFileSync } from 'fs';
|
||||||
import path from 'path';
|
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) => {
|
export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
|
||||||
return new StreamableFile(stream, { type, length });
|
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 {
|
function sortKeys<T extends object>(obj: T): T {
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
return obj;
|
return obj;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { AuthRequest } from '../decorators/auth-user.decorator';
|
import { AuthRequest } from '../app.guard';
|
||||||
import { multerUtils } from './asset-upload.config';
|
import { multerUtils } from './asset-upload.config';
|
||||||
|
|
||||||
const { fileFilter, destination, filename } = multerUtils;
|
const { fileFilter, destination, filename } = multerUtils;
|
||||||
|
@ -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 { StorageCore, StorageFolder } from '@app/domain/storage';
|
||||||
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
||||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
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 { diskStorage, StorageEngine } from 'multer';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { AuthRequest, AuthUserDto } from '../decorators/auth-user.decorator';
|
import { AuthRequest } from '../app.guard';
|
||||||
import { patchFormData } from '../utils/path-form-data.util';
|
import { patchFormData } from '../app.utils';
|
||||||
|
|
||||||
export interface ImmichFile extends Express.Multer.File {
|
export interface ImmichFile extends Express.Multer.File {
|
||||||
/** sha1 hash of file */
|
/** sha1 hash of file */
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { AuthRequest } from '../decorators/auth-user.decorator';
|
import { AuthRequest } from '../app.guard';
|
||||||
import { multerUtils } from './profile-image-upload.config';
|
import { multerUtils } from './profile-image-upload.config';
|
||||||
|
|
||||||
const { fileFilter, destination, filename } = multerUtils;
|
const { fileFilter, destination, filename } = multerUtils;
|
||||||
|
@ -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 { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||||
import { existsSync, mkdirSync } from 'fs';
|
import { existsSync, mkdirSync } from 'fs';
|
||||||
import { diskStorage } from 'multer';
|
import { diskStorage } from 'multer';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { AuthRequest, AuthUserDto } from '../decorators/auth-user.decorator';
|
import { AuthRequest } from '../app.guard';
|
||||||
import { patchFormData } from '../utils/path-form-data.util';
|
import { patchFormData } from '../app.utils';
|
||||||
|
|
||||||
export const profileImageUploadOption: MulterOptions = {
|
export const profileImageUploadOption: MulterOptions = {
|
||||||
fileFilter,
|
fileFilter,
|
||||||
|
@ -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 { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe';
|
import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe';
|
||||||
import { AuthUser } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser } from '../app.guard';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('Album')
|
@ApiTags('Album')
|
||||||
|
@ -8,9 +8,8 @@ import {
|
|||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUser } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser } from '../app.guard';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('API Key')
|
@ApiTags('API Key')
|
||||||
|
@ -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 { 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 { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, StreamableFile } from '@nestjs/common';
|
||||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { asStreamableFile } from '../app.utils';
|
import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard';
|
||||||
import { AuthUser } from '../decorators/auth-user.decorator';
|
import { asStreamableFile, UseValidation } from '../app.utils';
|
||||||
import { Authenticated, SharedLinkRoute } from '../decorators/authenticated.decorator';
|
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('Asset')
|
@ApiTags('Asset')
|
||||||
|
@ -18,9 +18,8 @@ import {
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Req, Res } from '@nestjs/common';
|
||||||
import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AuthUser, GetLoginDetails } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser, GetLoginDetails, PublicRoute } from '../app.guard';
|
||||||
import { Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('Authentication')
|
@ApiTags('Authentication')
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobService, JobStatusDto } from '@app/domain';
|
import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobService, JobStatusDto } from '@app/domain';
|
||||||
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { Authenticated } from '../app.guard';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
|
|
||||||
@ApiTags('Job')
|
@ApiTags('Job')
|
||||||
@Controller('jobs')
|
@Controller('jobs')
|
||||||
|
@ -11,9 +11,8 @@ import {
|
|||||||
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AuthUser, GetLoginDetails } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser, GetLoginDetails, PublicRoute } from '../app.guard';
|
||||||
import { Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
|
|
||||||
@ApiTags('OAuth')
|
@ApiTags('OAuth')
|
||||||
@Controller('oauth')
|
@Controller('oauth')
|
||||||
|
@ -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 { Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
|
||||||
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUser, AuthUserDto } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser } from '../app.guard';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('Partner')
|
@ApiTags('Partner')
|
||||||
|
@ -8,9 +8,8 @@ import {
|
|||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Body, Controller, Get, Param, Put, StreamableFile } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Put, StreamableFile } from '@nestjs/common';
|
||||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUser } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser } from '../app.guard';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
|
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
|
||||||
|
@ -8,9 +8,8 @@ import {
|
|||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUser } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser } from '../app.guard';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
|
|
||||||
@ApiTags('Search')
|
@ApiTags('Search')
|
||||||
@Controller('search')
|
@Controller('search')
|
||||||
|
@ -7,8 +7,8 @@ import {
|
|||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AdminRoute, Authenticated, PublicRoute } from '../decorators/authenticated.decorator';
|
import { AdminRoute, Authenticated, PublicRoute } from '../app.guard';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
|
|
||||||
@ApiTags('Server Info')
|
@ApiTags('Server Info')
|
||||||
@Controller('server-info')
|
@Controller('server-info')
|
||||||
|
@ -9,9 +9,8 @@ import {
|
|||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUser } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard';
|
||||||
import { Authenticated, SharedLinkRoute } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('Shared Link')
|
@ApiTags('Shared Link')
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain';
|
import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain';
|
||||||
import { Body, Controller, Get, Put } from '@nestjs/common';
|
import { Body, Controller, Get, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { Authenticated } from '../app.guard';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
|
|
||||||
@ApiTags('System Config')
|
@ApiTags('System Config')
|
||||||
@Controller('system-config')
|
@Controller('system-config')
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
AssetIdsDto,
|
AssetIdsDto,
|
||||||
AssetIdsResponseDto,
|
AssetIdsResponseDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
|
AuthUserDto,
|
||||||
CreateTagDto,
|
CreateTagDto,
|
||||||
TagResponseDto,
|
TagResponseDto,
|
||||||
TagService,
|
TagService,
|
||||||
@ -9,9 +10,8 @@ import {
|
|||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUser, AuthUserDto } from '../decorators/auth-user.decorator';
|
import { Authenticated, AuthUser } from '../app.guard';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('Tag')
|
@ApiTags('Tag')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
AuthUserDto,
|
||||||
CreateProfileImageDto,
|
CreateProfileImageDto,
|
||||||
CreateProfileImageResponseDto,
|
CreateProfileImageResponseDto,
|
||||||
CreateUserDto,
|
CreateUserDto,
|
||||||
@ -27,10 +28,9 @@ import {
|
|||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
import { Response as Res } from 'express';
|
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 { 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')
|
@ApiTags('User')
|
||||||
@Controller('user')
|
@Controller('user')
|
||||||
|
@ -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 || '',
|
|
||||||
};
|
|
||||||
});
|
|
@ -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;
|
|
@ -1,12 +0,0 @@
|
|||||||
import { applyDecorators, UsePipes, ValidationPipe } from '@nestjs/common';
|
|
||||||
|
|
||||||
export function UseValidation() {
|
|
||||||
return applyDecorators(
|
|
||||||
UsePipes(
|
|
||||||
new ValidationPipe({
|
|
||||||
transform: true,
|
|
||||||
whitelist: true,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export function patchFormData(latin1: string) {
|
|
||||||
return Buffer.from(latin1, 'latin1').toString('utf8');
|
|
||||||
}
|
|
@ -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, ''));
|
|
@ -1,5 +1,5 @@
|
|||||||
import { AuthUserDto } from '@app/immich/decorators/auth-user.decorator';
|
import { AuthUserDto } from '@app/domain';
|
||||||
import { AuthGuard } from '@app/immich/middlewares/auth.guard';
|
import { AppGuard } from '@app/immich/app.guard';
|
||||||
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
import { TestingModuleBuilder } from '@nestjs/testing';
|
import { TestingModuleBuilder } from '@nestjs/testing';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
@ -34,5 +34,5 @@ export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCa
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return builder.overrideProvider(AuthGuard).useValue(canActivate);
|
return builder.overrideProvider(AppGuard).useValue(canActivate);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user