mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
267 lines
6.9 KiB
TypeScript
267 lines
6.9 KiB
TypeScript
import {
|
|
ArgumentMetadata,
|
|
BadRequestException,
|
|
FileValidator,
|
|
Injectable,
|
|
ParseUUIDPipe,
|
|
applyDecorators,
|
|
} from '@nestjs/common';
|
|
import { ApiProperty } from '@nestjs/swagger';
|
|
import { Transform } from 'class-transformer';
|
|
import {
|
|
IsArray,
|
|
IsBoolean,
|
|
IsDate,
|
|
IsNotEmpty,
|
|
IsOptional,
|
|
IsString,
|
|
IsUUID,
|
|
Validate,
|
|
ValidateBy,
|
|
ValidateIf,
|
|
ValidationOptions,
|
|
ValidatorConstraint,
|
|
ValidatorConstraintInterface,
|
|
buildMessage,
|
|
isDateString,
|
|
} from 'class-validator';
|
|
import { CronJob } from 'cron';
|
|
import { DateTime } from 'luxon';
|
|
import sanitize from 'sanitize-filename';
|
|
import { isIP, isIPRange } from 'validator';
|
|
|
|
@Injectable()
|
|
export class ParseMeUUIDPipe extends ParseUUIDPipe {
|
|
async transform(value: string, metadata: ArgumentMetadata) {
|
|
if (value == 'me') {
|
|
return value;
|
|
}
|
|
return super.transform(value, metadata);
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class FileNotEmptyValidator extends FileValidator {
|
|
constructor(private requiredFields: string[]) {
|
|
super({});
|
|
this.requiredFields = requiredFields;
|
|
}
|
|
|
|
isValid(files?: any): boolean {
|
|
if (!files) {
|
|
return false;
|
|
}
|
|
|
|
return this.requiredFields.every((field) => files[field]);
|
|
}
|
|
|
|
buildErrorMessage(): string {
|
|
return `Field(s) ${this.requiredFields.join(', ')} should not be empty`;
|
|
}
|
|
}
|
|
|
|
export class UUIDParamDto {
|
|
@IsNotEmpty()
|
|
@IsUUID('4')
|
|
@ApiProperty({ format: 'uuid' })
|
|
id!: string;
|
|
}
|
|
|
|
export interface OptionalOptions extends ValidationOptions {
|
|
nullable?: boolean;
|
|
/** convert empty strings to null */
|
|
emptyToNull?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Checks if value is missing and if so, ignores all validators.
|
|
*
|
|
* @param validationOptions {@link OptionalOptions}
|
|
*
|
|
* @see IsOptional exported from `class-validator.
|
|
*/
|
|
// https://stackoverflow.com/a/71353929
|
|
export function Optional({ nullable, emptyToNull, ...validationOptions }: OptionalOptions = {}) {
|
|
const decorators: PropertyDecorator[] = [];
|
|
|
|
if (nullable === true) {
|
|
decorators.push(IsOptional(validationOptions));
|
|
} else {
|
|
decorators.push(ValidateIf((object: any, v: any) => v !== undefined, validationOptions));
|
|
}
|
|
|
|
if (emptyToNull) {
|
|
decorators.push(Transform(({ value }) => (value === '' ? null : value)));
|
|
}
|
|
|
|
return applyDecorators(...decorators);
|
|
}
|
|
|
|
type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean };
|
|
export const ValidateUUID = (options?: UUIDOptions) => {
|
|
const { optional, each, nullable } = { optional: false, each: false, nullable: false, ...options };
|
|
return applyDecorators(
|
|
IsUUID('4', { each }),
|
|
ApiProperty({ format: 'uuid' }),
|
|
optional ? Optional({ nullable }) : IsNotEmpty(),
|
|
each ? IsArray() : IsString(),
|
|
);
|
|
};
|
|
|
|
type DateOptions = { optional?: boolean; nullable?: boolean; format?: 'date' | 'date-time' };
|
|
export const ValidateDate = (options?: DateOptions) => {
|
|
const { optional, nullable, format } = { optional: false, nullable: false, format: 'date-time', ...options };
|
|
|
|
const decorators = [
|
|
ApiProperty({ format }),
|
|
IsDate(),
|
|
optional ? Optional({ nullable: true }) : IsNotEmpty(),
|
|
Transform(({ key, value }) => {
|
|
if (value === null || value === undefined) {
|
|
return value;
|
|
}
|
|
|
|
if (!isDateString(value)) {
|
|
throw new BadRequestException(`${key} must be a date string`);
|
|
}
|
|
|
|
return new Date(value as string);
|
|
}),
|
|
];
|
|
|
|
if (optional) {
|
|
decorators.push(Optional({ nullable }));
|
|
}
|
|
|
|
return applyDecorators(...decorators);
|
|
};
|
|
|
|
type BooleanOptions = { optional?: boolean };
|
|
export const ValidateBoolean = (options?: BooleanOptions) => {
|
|
const { optional } = { optional: false, ...options };
|
|
const decorators = [
|
|
// ApiProperty(),
|
|
IsBoolean(),
|
|
Transform(({ value }) => {
|
|
if (value == 'true') {
|
|
return true;
|
|
} else if (value == 'false') {
|
|
return false;
|
|
}
|
|
return value;
|
|
}),
|
|
];
|
|
|
|
if (optional) {
|
|
decorators.push(Optional());
|
|
}
|
|
|
|
return applyDecorators(...decorators);
|
|
};
|
|
|
|
@ValidatorConstraint({ name: 'cronValidator' })
|
|
class CronValidator implements ValidatorConstraintInterface {
|
|
validate(expression: string): boolean {
|
|
try {
|
|
new CronJob(expression, () => {});
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const IsCronExpression = () => Validate(CronValidator, { message: 'Invalid cron expression' });
|
|
|
|
type IValue = { value: unknown };
|
|
|
|
export const toEmail = ({ value }: IValue) => (typeof value === 'string' ? value.toLowerCase() : value);
|
|
|
|
export const toSanitized = ({ value }: IValue) => {
|
|
const input = typeof value === 'string' ? value : '';
|
|
return sanitize(input.replaceAll('.', ''));
|
|
};
|
|
|
|
export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => {
|
|
const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options;
|
|
return Number.isInteger(value) && value >= min && value <= max;
|
|
};
|
|
|
|
export function isDateStringFormat(value: unknown, format: string) {
|
|
if (typeof value !== 'string') {
|
|
return false;
|
|
}
|
|
return DateTime.fromFormat(value, format, { zone: 'utc' }).isValid;
|
|
}
|
|
|
|
export function IsDateStringFormat(format: string, validationOptions?: ValidationOptions) {
|
|
return ValidateBy(
|
|
{
|
|
name: 'isDateStringFormat',
|
|
constraints: [format],
|
|
validator: {
|
|
validate(value: unknown) {
|
|
return isDateStringFormat(value, format);
|
|
},
|
|
defaultMessage: () => `$property must be a string in the format ${format}`,
|
|
},
|
|
},
|
|
validationOptions,
|
|
);
|
|
}
|
|
|
|
function maxDate(date: DateTime, maxDate: DateTime | (() => DateTime)) {
|
|
return date <= (maxDate instanceof DateTime ? maxDate : maxDate());
|
|
}
|
|
|
|
export function MaxDateString(
|
|
date: DateTime | (() => DateTime),
|
|
validationOptions?: ValidationOptions,
|
|
): PropertyDecorator {
|
|
return ValidateBy(
|
|
{
|
|
name: 'maxDateString',
|
|
constraints: [date],
|
|
validator: {
|
|
validate: (value, args) => {
|
|
const date = DateTime.fromISO(value, { zone: 'utc' });
|
|
return maxDate(date, args?.constraints[0]);
|
|
},
|
|
defaultMessage: buildMessage(
|
|
(eachPrefix) => 'maximal allowed date for ' + eachPrefix + '$property is $constraint1',
|
|
validationOptions,
|
|
),
|
|
},
|
|
},
|
|
validationOptions,
|
|
);
|
|
}
|
|
|
|
type IsIPRangeOptions = { requireCIDR?: boolean };
|
|
export function IsIPRange(options: IsIPRangeOptions, validationOptions?: ValidationOptions): PropertyDecorator {
|
|
const { requireCIDR } = { requireCIDR: true, ...options };
|
|
|
|
return ValidateBy(
|
|
{
|
|
name: 'isIPRange',
|
|
validator: {
|
|
validate: (value): boolean => {
|
|
if (isIPRange(value)) {
|
|
return true;
|
|
}
|
|
|
|
if (!requireCIDR && isIP(value)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
defaultMessage: buildMessage(
|
|
(eachPrefix) => eachPrefix + '$property must be an ip address, or ip address range',
|
|
validationOptions,
|
|
),
|
|
},
|
|
},
|
|
validationOptions,
|
|
);
|
|
}
|