feat: upload module (#486)
* feat: wip upload module * feat: local storage and serve local images * feat: protect against injections * feat: server local and s3 files * fix: use storage location when serving local files * feat: cross field env validation
This commit is contained in:
@ -0,0 +1,26 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
export class IsAWSRegionConstraint implements ValidatorConstraintInterface {
|
||||
validate(region: string) {
|
||||
const regex = /^[a-z]{2}-[a-z]+-\d{1}$/;
|
||||
return regex.test(region); // Returns true if region matches regex
|
||||
}
|
||||
}
|
||||
|
||||
export function IsAWSRegion(validationOptions?: ValidationOptions) {
|
||||
return function (object: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsAWSRegionConstraint,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
export class IsDurationConstraint implements ValidatorConstraintInterface {
|
||||
validate(duration: string) {
|
||||
const regex =
|
||||
/^-?[0-9]+(.[0-9]+)?(m(illiseconds?)?|s(econds?)?|h((ou)?rs?)?|d(ays?)?|w(eeks?)?|M(onths?)?|y(ears?)?)?$/;
|
||||
return regex.test(duration); // Returns true if duration matches regex
|
||||
}
|
||||
}
|
||||
|
||||
export function IsDuration(validationOptions?: ValidationOptions) {
|
||||
return function (object: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsDurationConstraint,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
||||
|
||||
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
|
||||
new ConfigurableModuleBuilder({
|
||||
moduleName: 'Environment',
|
||||
})
|
||||
.setClassMethodName('forRoot')
|
||||
.build();
|
||||
19
server/src/integrations/environment/environment.module.ts
Normal file
19
server/src/integrations/environment/environment.module.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { EnvironmentService } from './environment.service';
|
||||
import { ConfigurableModuleClass } from './environment.module-definition';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { validate } from './environment.validation';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
expandVariables: true,
|
||||
validate,
|
||||
}),
|
||||
],
|
||||
providers: [EnvironmentService],
|
||||
exports: [EnvironmentService],
|
||||
})
|
||||
export class EnvironmentModule extends ConfigurableModuleClass {}
|
||||
@ -0,0 +1,25 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { EnvironmentService } from './environment.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
describe('EnvironmentService', () => {
|
||||
let service: EnvironmentService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EnvironmentService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EnvironmentService>(EnvironmentService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
66
server/src/integrations/environment/environment.service.ts
Normal file
66
server/src/integrations/environment/environment.service.ts
Normal file
@ -0,0 +1,66 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AwsRegion } from './interfaces/aws-region.interface';
|
||||
import { StorageType } from './interfaces/storage.interface';
|
||||
|
||||
@Injectable()
|
||||
export class EnvironmentService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
getPGDatabaseUrl(): string {
|
||||
return this.configService.get<string>('PG_DATABASE_URL')!;
|
||||
}
|
||||
|
||||
getAccessTokenSecret(): string {
|
||||
return this.configService.get<string>('ACCESS_TOKEN_SECRET')!;
|
||||
}
|
||||
|
||||
getAccessTokenExpiresIn(): string {
|
||||
return this.configService.get<string>('ACCESS_TOKEN_EXPIRES_IN')!;
|
||||
}
|
||||
|
||||
getRefreshTokenSecret(): string {
|
||||
return this.configService.get<string>('REFRESH_TOKEN_SECRET')!;
|
||||
}
|
||||
|
||||
getRefreshTokenExpiresIn(): string {
|
||||
return this.configService.get<string>('REFRESH_TOKEN_EXPIRES_IN')!;
|
||||
}
|
||||
|
||||
getLoginTokenSecret(): string {
|
||||
return this.configService.get<string>('LOGIN_TOKEN_SECRET')!;
|
||||
}
|
||||
|
||||
getLoginTokenExpiresIn(): string {
|
||||
return this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN')!;
|
||||
}
|
||||
|
||||
getFrontAuthCallbackUrl(): string {
|
||||
return this.configService.get<string>('FRONT_AUTH_CALLBACK_URL')!;
|
||||
}
|
||||
|
||||
getAuthGoogleClientId(): string | undefined {
|
||||
return this.configService.get<string>('AUTH_GOOGLE_CLIENT_ID');
|
||||
}
|
||||
|
||||
getAuthGoogleClientSecret(): string | undefined {
|
||||
return this.configService.get<string>('AUTH_GOOGLE_CLIENT_SECRET');
|
||||
}
|
||||
|
||||
getAuthGoogleCallbackUrl(): string | undefined {
|
||||
return this.configService.get<string>('AUTH_GOOGLE_CALLBACK_URL');
|
||||
}
|
||||
|
||||
getStorageType(): StorageType | undefined {
|
||||
return this.configService.get<StorageType>('STORAGE_TYPE');
|
||||
}
|
||||
|
||||
getStorageRegion(): AwsRegion | undefined {
|
||||
return this.configService.get<AwsRegion>('STORAGE_REGION');
|
||||
}
|
||||
|
||||
getStorageLocation(): string {
|
||||
return this.configService.get<string>('STORAGE_LOCATION')!;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import {
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateIf,
|
||||
validateSync,
|
||||
} from 'class-validator';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { IsDuration } from './decorators/is-duration.decorator';
|
||||
import { StorageType } from './interfaces/storage.interface';
|
||||
import { AwsRegion } from './interfaces/aws-region.interface';
|
||||
import { IsAWSRegion } from './decorators/is-aws-region.decorator';
|
||||
|
||||
export class EnvironmentVariables {
|
||||
// Database
|
||||
@IsUrl({ protocols: ['postgres'], require_tld: false })
|
||||
PG_DATABASE_URL: string;
|
||||
|
||||
// Json Web Token
|
||||
@IsString()
|
||||
ACCESS_TOKEN_SECRET: string;
|
||||
@IsDuration()
|
||||
ACCESS_TOKEN_EXPIRES_IN: string;
|
||||
|
||||
@IsString()
|
||||
REFRESH_TOKEN_SECRET: string;
|
||||
@IsDuration()
|
||||
REFRESH_TOKEN_EXPIRES_IN: string;
|
||||
|
||||
@IsString()
|
||||
LOGIN_TOKEN_SECRET: string;
|
||||
@IsDuration()
|
||||
LOGIN_TOKEN_EXPIRES_IN: string;
|
||||
|
||||
// Auth
|
||||
@IsUrl({ require_tld: false })
|
||||
FRONT_AUTH_CALLBACK_URL: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
AUTH_GOOGLE_CLIENT_ID?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
AUTH_GOOGLE_CLIENT_SECRET?: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
@IsOptional()
|
||||
AUTH_GOOGLE_CALLBACK_URL?: string;
|
||||
|
||||
// Storage
|
||||
@IsEnum(StorageType)
|
||||
@IsOptional()
|
||||
STORAGE_TYPE?: StorageType;
|
||||
|
||||
@ValidateIf((_, value) => value === StorageType.S3)
|
||||
@IsAWSRegion()
|
||||
STORAGE_REGION?: AwsRegion;
|
||||
|
||||
@IsString()
|
||||
STORAGE_LOCATION: string;
|
||||
}
|
||||
|
||||
export function validate(config: Record<string, unknown>) {
|
||||
const validatedConfig = plainToClass(EnvironmentVariables, config, {
|
||||
enableImplicitConversion: true,
|
||||
});
|
||||
|
||||
const errors = validateSync(validatedConfig, {
|
||||
skipMissingProperties: false,
|
||||
});
|
||||
assert(!errors.length, errors.toString());
|
||||
|
||||
return validatedConfig;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export type AwsRegion = `${string}-${string}-${number}`;
|
||||
@ -0,0 +1,4 @@
|
||||
export enum StorageType {
|
||||
S3 = 's3',
|
||||
Local = 'local',
|
||||
}
|
||||
Reference in New Issue
Block a user