migrate to native config
This commit is contained in:
parent
be38075169
commit
f38b05d2e5
|
@ -20,6 +20,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^8.4.0",
|
||||
"@nestjs/config": "^1.2.0",
|
||||
"@nestjs/core": "^8.4.0",
|
||||
"@nestjs/jwt": "^8.0.0",
|
||||
"@nestjs/passport": "^8.2.1",
|
||||
|
|
|
@ -1,30 +1,24 @@
|
|||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from './routes/api/auth/auth.module';
|
||||
import { ImageModule } from './routes/image/imageroute.module';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import Config from './env';
|
||||
import { DemoManagerModule } from './managers/demo/demomanager.module';
|
||||
import { EImageBackend } from './models/entities/image.entity';
|
||||
import { EUserBackend } from './models/entities/user.entity';
|
||||
import { PrefModule } from './routes/api/pref/pref.module';
|
||||
import { ESysPreferenceBackend } from './models/entities/syspreference.entity';
|
||||
import { TypeOrmConfigService } from './config/typeorm.config.service';
|
||||
import { PicsurConfigModule } from './config/config.module';
|
||||
import { ServeStaticConfigService } from './config/servestatic.config.service';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRoot({
|
||||
type: 'postgres',
|
||||
host: Config.database.host,
|
||||
port: Config.database.port,
|
||||
username: Config.database.username,
|
||||
password: Config.database.password,
|
||||
database: Config.database.database,
|
||||
synchronize: true,
|
||||
|
||||
entities: [EUserBackend, EImageBackend, ESysPreferenceBackend],
|
||||
TypeOrmModule.forRootAsync({
|
||||
useExisting: TypeOrmConfigService,
|
||||
imports: [PicsurConfigModule],
|
||||
}),
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: Config.static.frontendRoot,
|
||||
ServeStaticModule.forRootAsync({
|
||||
useExisting: ServeStaticConfigService,
|
||||
imports: [PicsurConfigModule],
|
||||
}),
|
||||
AuthModule,
|
||||
ImageModule,
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PicsurConfigModule } from '../../config/config.module';
|
||||
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
|
||||
import { SysPreferenceService } from './syspreferencedb.service';
|
||||
import { SysPreferenceDefaultsService } from './syspreferencedefaults.service';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ESysPreferenceBackend])],
|
||||
providers: [SysPreferenceService],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ESysPreferenceBackend]),
|
||||
PicsurConfigModule,
|
||||
],
|
||||
providers: [SysPreferenceService, SysPreferenceDefaultsService],
|
||||
exports: [SysPreferenceService],
|
||||
})
|
||||
export class SysPreferenceModule {}
|
||||
|
|
|
@ -5,8 +5,8 @@ import { validate } from 'class-validator';
|
|||
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SysPreferenceDefaults } from '../../models/dto/syspreference.dto';
|
||||
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
|
||||
import { SysPreferenceDefaultsService } from './syspreferencedefaults.service';
|
||||
|
||||
@Injectable()
|
||||
export class SysPreferenceService {
|
||||
|
@ -15,10 +15,11 @@ export class SysPreferenceService {
|
|||
constructor(
|
||||
@InjectRepository(ESysPreferenceBackend)
|
||||
private sysPreferenceRepository: Repository<ESysPreferenceBackend>,
|
||||
private defaultsService: SysPreferenceDefaultsService,
|
||||
) {}
|
||||
|
||||
public async setPreference(
|
||||
key: string,
|
||||
key: SysPreferences,
|
||||
value: string,
|
||||
): AsyncFailable<ESysPreferenceBackend> {
|
||||
let sysPreference = await this.validatePref(key, value);
|
||||
|
@ -37,16 +38,17 @@ export class SysPreferenceService {
|
|||
}
|
||||
|
||||
public async getPreference(
|
||||
key: string,
|
||||
key: SysPreferences,
|
||||
): AsyncFailable<ESysPreferenceBackend> {
|
||||
let sysPreference = await this.validatePref(key);
|
||||
if (HasFailed(sysPreference)) return sysPreference;
|
||||
|
||||
let foundSysPreference: ESysPreferenceBackend | undefined;
|
||||
try {
|
||||
foundSysPreference = await this.sysPreferenceRepository.findOne({
|
||||
key: sysPreference.key,
|
||||
});
|
||||
foundSysPreference = await this.sysPreferenceRepository.findOne(
|
||||
{ key: sysPreference.key },
|
||||
{ cache: 60000 },
|
||||
);
|
||||
} catch (e: any) {
|
||||
this.logger.warn(e);
|
||||
return Fail('Could not get preference');
|
||||
|
@ -55,7 +57,10 @@ export class SysPreferenceService {
|
|||
if (!foundSysPreference) {
|
||||
return this.saveDefault(sysPreference.key);
|
||||
} else {
|
||||
foundSysPreference = plainToClass(ESysPreferenceBackend, foundSysPreference);
|
||||
foundSysPreference = plainToClass(
|
||||
ESysPreferenceBackend,
|
||||
foundSysPreference,
|
||||
);
|
||||
const errors = await validate(foundSysPreference);
|
||||
if (errors.length > 0) {
|
||||
this.logger.warn(errors);
|
||||
|
@ -69,7 +74,7 @@ export class SysPreferenceService {
|
|||
private async saveDefault(
|
||||
key: SysPreferences,
|
||||
): AsyncFailable<ESysPreferenceBackend> {
|
||||
return this.setPreference(key, SysPreferenceDefaults[key]());
|
||||
return this.setPreference(key, this.defaultsService.defaults[key]());
|
||||
}
|
||||
|
||||
private async validatePref(
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import { generateRandomString } from 'picsur-shared/dist/util/random';
|
||||
import { EnvJwtConfigService } from '../../config/jwt.config.service';
|
||||
|
||||
@Injectable()
|
||||
export class SysPreferenceDefaultsService {
|
||||
private readonly logger = new Logger('SysPreferenceDefaultsService');
|
||||
|
||||
constructor(private jwtConfigService: EnvJwtConfigService) {}
|
||||
|
||||
public readonly defaults: {
|
||||
[key in SysPreferences]: () => string;
|
||||
} = {
|
||||
jwt_secret: () => {
|
||||
const envSecret = this.jwtConfigService.getJwtSecret();
|
||||
if (envSecret) {
|
||||
return envSecret;
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'Since no JWT secret was provided, a random one will be generated and saved',
|
||||
);
|
||||
return generateRandomString(64);
|
||||
}
|
||||
},
|
||||
jwt_expires_in: () => this.jwtConfigService.getJwtExpiresIn() ?? '7d',
|
||||
};
|
||||
}
|
15
backend/src/config/auth.config.service.ts
Normal file
15
backend/src/config/auth.config.service.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class AuthConfigService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
public getDefaultAdminPassword(): string {
|
||||
return this.configService.get<string>('DEFAULT_ADMIN_PASSWORD', 'admin');
|
||||
}
|
||||
|
||||
public getDefaultAdminUsername(): string {
|
||||
return this.configService.get<string>('DEFAULT_ADMIN_USERNAME', 'admin');
|
||||
}
|
||||
}
|
35
backend/src/config/config.module.ts
Normal file
35
backend/src/config/config.module.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmConfigService } from './typeorm.config.service';
|
||||
import { ServeStaticConfigService } from './servestatic.config.service';
|
||||
import { HostConfigService } from './host.config.service';
|
||||
import { EnvJwtConfigService } from './jwt.config.service';
|
||||
import { AuthConfigService } from './auth.config.service';
|
||||
import { MultipartConfigService } from './multipart.config.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
ignoreEnvVars: true,
|
||||
cache: true,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
EnvJwtConfigService,
|
||||
TypeOrmConfigService,
|
||||
ServeStaticConfigService,
|
||||
HostConfigService,
|
||||
AuthConfigService,
|
||||
MultipartConfigService,
|
||||
],
|
||||
exports: [
|
||||
ConfigModule,
|
||||
EnvJwtConfigService,
|
||||
TypeOrmConfigService,
|
||||
ServeStaticConfigService,
|
||||
HostConfigService,
|
||||
AuthConfigService,
|
||||
MultipartConfigService,
|
||||
],
|
||||
})
|
||||
export class PicsurConfigModule {}
|
10
backend/src/config/config.static.ts
Normal file
10
backend/src/config/config.static.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export const EnvPrefix = 'PICSUR_';
|
||||
export const DefaultName = 'picsur';
|
||||
|
||||
export const PackageRoot = resolve(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../../',
|
||||
);
|
42
backend/src/config/host.config.service.ts
Normal file
42
backend/src/config/host.config.service.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
ServeStaticModuleOptions,
|
||||
ServeStaticModuleOptionsFactory,
|
||||
} from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
import { EnvPrefix, PackageRoot } from './config.static';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class HostConfigService {
|
||||
private readonly logger = new Logger('HostConfigService');
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
public getHost(): string {
|
||||
const host = this.configService.get<string>(`${EnvPrefix}HOST`, '0.0.0.0');
|
||||
this.logger.debug('Host: ' + host);
|
||||
return host;
|
||||
}
|
||||
|
||||
public getPort(): number {
|
||||
const port = this.configService.get<number>(`${EnvPrefix}PORT`, 8080);
|
||||
this.logger.debug('Port: ' + port);
|
||||
return port;
|
||||
}
|
||||
|
||||
public isDemo() {
|
||||
const enabled = this.configService.get<boolean>(`${EnvPrefix}_DEMO`, false);
|
||||
this.logger.debug('Demo enabled: ' + enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public getDemoInterval() {
|
||||
const interval = this.configService.get<number>(
|
||||
`${EnvPrefix}_DEMO_INTERVAL`,
|
||||
1000 * 60 * 5,
|
||||
);
|
||||
this.logger.debug('Demo interval: ' + interval);
|
||||
return interval;
|
||||
}
|
||||
}
|
15
backend/src/config/jwt.config.service.ts
Normal file
15
backend/src/config/jwt.config.service.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class EnvJwtConfigService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
public getJwtSecret(): string | undefined {
|
||||
return this.configService.get<string>('JWT_SECRET');
|
||||
}
|
||||
|
||||
public getJwtExpiresIn(): string | undefined {
|
||||
return this.configService.get<string>('JWT_EXPIRES_IN');
|
||||
}
|
||||
}
|
48
backend/src/config/jwt.lateconfig.service.ts
Normal file
48
backend/src/config/jwt.lateconfig.service.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { FactoryProvider, Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtModuleOptions, JwtOptionsFactory } from '@nestjs/jwt';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SysPreferenceService } from '../collections/syspreferencesdb/syspreferencedb.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtConfigService implements JwtOptionsFactory {
|
||||
private readonly logger = new Logger('JwtConfigService');
|
||||
|
||||
constructor(private prefService: SysPreferenceService) {}
|
||||
|
||||
public async getJwtSecret(): Promise<string> {
|
||||
const secret = await this.prefService.getPreference('jwt_secret');
|
||||
if (HasFailed(secret)) {
|
||||
throw new Error('JWT secret could not be retrieved');
|
||||
}
|
||||
|
||||
this.logger.debug('JWT secret: ' + secret.value);
|
||||
return secret.value;
|
||||
}
|
||||
|
||||
public async getJwtExpiresIn(): Promise<string> {
|
||||
const expiresIn = await this.prefService.getPreference('jwt_expires_in');
|
||||
if (HasFailed(expiresIn)) {
|
||||
throw new Error('JWT expiresIn could not be retrieved');
|
||||
}
|
||||
|
||||
this.logger.debug('JWT expiresIn: ' + expiresIn.value);
|
||||
return expiresIn.value;
|
||||
}
|
||||
|
||||
public async createJwtOptions(): Promise<JwtModuleOptions> {
|
||||
return {
|
||||
secret: await this.getJwtSecret(),
|
||||
signOptions: {
|
||||
expiresIn: await this.getJwtExpiresIn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const JwtSecretProvider: FactoryProvider<Promise<string>> = {
|
||||
provide: 'JWT_SECRET',
|
||||
useFactory: async (jwtConfigService: JwtConfigService) => {
|
||||
return await jwtConfigService.getJwtSecret();
|
||||
},
|
||||
inject: [JwtConfigService],
|
||||
};
|
40
backend/src/config/lateconfig.module.ts
Normal file
40
backend/src/config/lateconfig.module.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { JwtConfigService } from './jwt.lateconfig.service';
|
||||
import { SysPreferenceModule } from '../collections/syspreferencesdb/syspreferencedb.module';
|
||||
import { PicsurConfigModule } from './config.module';
|
||||
import { EnvJwtConfigService } from './jwt.config.service';
|
||||
import { SysPreferenceService } from '../collections/syspreferencesdb/syspreferencedb.service';
|
||||
|
||||
// This module contains all configservices that depend on the syspref module
|
||||
// The syspref module can only be used when connected to the database
|
||||
// Since the syspref module requires the database config, we need this seperate
|
||||
// Otherwise we will create a circular depedency
|
||||
|
||||
@Module({
|
||||
imports: [SysPreferenceModule, PicsurConfigModule],
|
||||
providers: [JwtConfigService],
|
||||
exports: [JwtConfigService, PicsurConfigModule],
|
||||
})
|
||||
export class PicsurLateConfigModule implements OnModuleInit {
|
||||
private readonly logger = new Logger('PicsurLateConfigModule');
|
||||
|
||||
constructor(
|
||||
private envJwtConfigService: EnvJwtConfigService,
|
||||
private prefService: SysPreferenceService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
const secret = this.envJwtConfigService.getJwtSecret();
|
||||
const expiresIn = this.envJwtConfigService.getJwtExpiresIn();
|
||||
|
||||
if (secret === undefined) {
|
||||
await this.prefService.getPreference('jwt_secret');
|
||||
} else {
|
||||
await this.prefService.setPreference('jwt_secret', secret);
|
||||
}
|
||||
|
||||
if (expiresIn !== undefined) {
|
||||
await this.prefService.setPreference('jwt_expires_in', expiresIn);
|
||||
}
|
||||
}
|
||||
}
|
25
backend/src/config/multipart.config.service.ts
Normal file
25
backend/src/config/multipart.config.service.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EnvPrefix } from './config.static';
|
||||
|
||||
@Injectable()
|
||||
export class MultipartConfigService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
public getMaxFileSize(): number {
|
||||
return this.configService.get<number>(
|
||||
`${EnvPrefix}MAX_FILE_SIZE`,
|
||||
128000000,
|
||||
);
|
||||
}
|
||||
|
||||
public getLimits() {
|
||||
return {
|
||||
fieldNameSize: 128,
|
||||
fieldSize: 1024,
|
||||
fields: 16,
|
||||
files: 16,
|
||||
fileSize: this.getMaxFileSize(),
|
||||
};
|
||||
}
|
||||
}
|
36
backend/src/config/servestatic.config.service.ts
Normal file
36
backend/src/config/servestatic.config.service.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
ServeStaticModuleOptions,
|
||||
ServeStaticModuleOptionsFactory,
|
||||
} from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
import { EnvPrefix, PackageRoot } from './config.static';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class ServeStaticConfigService
|
||||
implements ServeStaticModuleOptionsFactory
|
||||
{
|
||||
private readonly logger = new Logger('ServeStaticConfigService');
|
||||
|
||||
private defaultLocation = join(PackageRoot, '../frontend/dist');
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
public getStaticDirectory(): string {
|
||||
const directory = this.configService.get<string>(
|
||||
`${EnvPrefix}STATIC_FRONTEND_ROOT`,
|
||||
this.defaultLocation,
|
||||
);
|
||||
this.logger.debug('Static directory: ' + directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
public createLoggerOptions(): ServeStaticModuleOptions[] {
|
||||
return [
|
||||
{
|
||||
rootPath: this.getStaticDirectory(),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
52
backend/src/config/typeorm.config.service.ts
Normal file
52
backend/src/config/typeorm.config.service.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
|
||||
import { EntityList } from '../models/entities';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DefaultName, EnvPrefix } from './config.static';
|
||||
|
||||
@Injectable()
|
||||
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
|
||||
private readonly logger = new Logger('TypeOrmConfigService');
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
public getTypeOrmServerOptions() {
|
||||
const varOptions = {
|
||||
host: this.configService.get<string>(`${EnvPrefix}_DB_HOST`, 'localhost'),
|
||||
port: this.configService.get<number>(`${EnvPrefix}_DB_PORT`, 5432),
|
||||
username: this.configService.get<string>(
|
||||
`${EnvPrefix}_DB_USERNAME`,
|
||||
DefaultName,
|
||||
),
|
||||
password: this.configService.get<string>(
|
||||
`${EnvPrefix}_DB_PASSWORD`,
|
||||
DefaultName,
|
||||
),
|
||||
database: this.configService.get<string>(
|
||||
`${EnvPrefix}_DB_DATABASE`,
|
||||
DefaultName,
|
||||
),
|
||||
};
|
||||
|
||||
this.logger.debug('DB host: ' + varOptions.host);
|
||||
this.logger.debug('DB port: ' + varOptions.port);
|
||||
this.logger.debug('DB username: ' + varOptions.username);
|
||||
this.logger.debug('DB password: ' + varOptions.password);
|
||||
this.logger.debug('DB database: ' + varOptions.database);
|
||||
return varOptions;
|
||||
}
|
||||
|
||||
public createTypeOrmOptions(connectionName?: string): TypeOrmModuleOptions {
|
||||
this.logger.debug('Creating TypeOrmOptions for: ' + connectionName);
|
||||
|
||||
const varOptions = this.getTypeOrmServerOptions();
|
||||
return {
|
||||
type: 'postgres',
|
||||
synchronize: true,
|
||||
|
||||
entities: EntityList,
|
||||
|
||||
...varOptions,
|
||||
};
|
||||
}
|
||||
}
|
22
backend/src/decorators/decorator.ts
Normal file
22
backend/src/decorators/decorator.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
createParamDecorator,
|
||||
ExecutionContext,
|
||||
} from '@nestjs/common';
|
||||
import { Newable } from 'picsur-shared/dist/types';
|
||||
import { MultiPartPipe } from './multipart.pipe';
|
||||
import { PostFilePipe } from './postfile.pipe';
|
||||
|
||||
const InjectRequest = createParamDecorator(
|
||||
async <T extends Object>(data: Newable<T>, ctx: ExecutionContext) => {
|
||||
return {
|
||||
req: ctx.switchToHttp().getRequest(),
|
||||
data,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const PostFile = () =>
|
||||
InjectRequest(PostFilePipe);
|
||||
|
||||
export const MultiPart = <T extends Object>(data: Newable<T>) =>
|
||||
InjectRequest(data, MultiPartPipe);
|
12
backend/src/decorators/decorators.module.ts
Normal file
12
backend/src/decorators/decorators.module.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PicsurConfigModule } from '../config/config.module';
|
||||
import { MultiPartPipe } from './multipart.pipe';
|
||||
import { PostFilePipe } from './postfile.pipe';
|
||||
|
||||
@Module({
|
||||
imports: [PicsurConfigModule],
|
||||
providers: [MultiPartPipe, PostFilePipe],
|
||||
exports: [MultiPartPipe, PostFilePipe, PicsurConfigModule],
|
||||
})
|
||||
export class DecoratorsModule {
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import {
|
||||
BadRequestException,
|
||||
createParamDecorator,
|
||||
ExecutionContext,
|
||||
Logger,
|
||||
Type,
|
||||
} from '@nestjs/common';
|
||||
import { validate } from 'class-validator';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { Multipart, MultipartFields, MultipartFile } from 'fastify-multipart';
|
||||
import { Newable } from 'picsur-shared/dist/types';
|
||||
import Config from '../env';
|
||||
import { MultiPartFieldDto, MultiPartFileDto } from '../models/dto/multipart.dto';
|
||||
|
||||
const logger = new Logger('MultiPart');
|
||||
export interface MPFile {
|
||||
fieldname: string;
|
||||
}
|
||||
|
||||
export const PostFile = createParamDecorator(
|
||||
async (data: unknown, ctx: ExecutionContext) => {
|
||||
const req: FastifyRequest = ctx.switchToHttp().getRequest();
|
||||
|
||||
if (!req.isMultipart()) throw new BadRequestException('Invalid file');
|
||||
|
||||
const file = await req.file({
|
||||
limits: {
|
||||
...Config.uploadLimits,
|
||||
files: 1,
|
||||
},
|
||||
});
|
||||
if (file === undefined) throw new BadRequestException('Invalid file');
|
||||
|
||||
const allFields: Multipart[] = Object.values(file.fields).filter(
|
||||
(entry) => entry,
|
||||
) as any;
|
||||
|
||||
const files = allFields.filter((entry) => entry.file !== undefined);
|
||||
|
||||
if (files.length !== 1) throw new BadRequestException('Invalid file');
|
||||
|
||||
try {
|
||||
return await files[0].toBuffer();
|
||||
} catch (e) {
|
||||
throw new BadRequestException('Invalid file');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const MultiPart = createParamDecorator(
|
||||
async <T extends Object>(data: Newable<T>, ctx: ExecutionContext) => {
|
||||
const req: FastifyRequest = ctx.switchToHttp().getRequest();
|
||||
const dtoClass = new data();
|
||||
|
||||
if (!req.isMultipart()) throw new BadRequestException('Invalid file');
|
||||
|
||||
let fields: MultipartFields | null = null;
|
||||
try {
|
||||
fields = (
|
||||
await req.file({
|
||||
limits: Config.uploadLimits,
|
||||
})
|
||||
).fields;
|
||||
} catch (e) {
|
||||
logger.warn(e);
|
||||
}
|
||||
if (!fields) throw new BadRequestException('Invalid file');
|
||||
|
||||
for (const key of Object.keys(fields)) {
|
||||
if (Array.isArray(fields[key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((fields[key] as any).value) {
|
||||
(dtoClass as any)[key] = new MultiPartFieldDto(fields[key] as MultipartFile);
|
||||
} else {
|
||||
(dtoClass as any)[key] = new MultiPartFileDto(
|
||||
fields[key] as MultipartFile,
|
||||
new BadRequestException('Invalid file'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const errors = await validate(dtoClass, { forbidUnknownValues: true });
|
||||
if (errors.length > 0) {
|
||||
logger.warn(errors);
|
||||
throw new BadRequestException('Invalid file');
|
||||
}
|
||||
|
||||
return dtoClass;
|
||||
},
|
||||
);
|
72
backend/src/decorators/multipart.pipe.ts
Normal file
72
backend/src/decorators/multipart.pipe.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
PipeTransform,
|
||||
Scope,
|
||||
} from '@nestjs/common';
|
||||
import { validate } from 'class-validator';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { MultipartFields, MultipartFile } from 'fastify-multipart';
|
||||
import { Newable } from 'picsur-shared/dist/types';
|
||||
import { MultipartConfigService } from '../config/multipart.config.service';
|
||||
import {
|
||||
MultiPartFieldDto,
|
||||
MultiPartFileDto,
|
||||
} from '../models/dto/multipart.dto';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class MultiPartPipe implements PipeTransform {
|
||||
private readonly logger = new Logger('MultiPartPipe');
|
||||
|
||||
constructor(private multipartConfigService: MultipartConfigService) {}
|
||||
|
||||
async transform<T extends Object>({
|
||||
req,
|
||||
data,
|
||||
}: {
|
||||
req: FastifyRequest;
|
||||
data: Newable<T>;
|
||||
}) {
|
||||
const dtoClass = new data();
|
||||
|
||||
if (!req.isMultipart()) throw new BadRequestException('Invalid file');
|
||||
|
||||
let fields: MultipartFields | null = null;
|
||||
try {
|
||||
fields = (
|
||||
await req.file({
|
||||
limits: this.multipartConfigService.getLimits(),
|
||||
})
|
||||
).fields;
|
||||
} catch (e) {
|
||||
this.logger.warn(e);
|
||||
}
|
||||
if (!fields) throw new BadRequestException('Invalid file');
|
||||
|
||||
for (const key of Object.keys(fields)) {
|
||||
if (Array.isArray(fields[key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((fields[key] as any).value) {
|
||||
(dtoClass as any)[key] = new MultiPartFieldDto(
|
||||
fields[key] as MultipartFile,
|
||||
);
|
||||
} else {
|
||||
(dtoClass as any)[key] = new MultiPartFileDto(
|
||||
fields[key] as MultipartFile,
|
||||
new BadRequestException('Invalid file'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const errors = await validate(dtoClass, { forbidUnknownValues: true });
|
||||
if (errors.length > 0) {
|
||||
this.logger.warn(errors);
|
||||
throw new BadRequestException('Invalid file');
|
||||
}
|
||||
|
||||
return dtoClass;
|
||||
}
|
||||
}
|
44
backend/src/decorators/postfile.pipe.ts
Normal file
44
backend/src/decorators/postfile.pipe.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
PipeTransform,
|
||||
Scope,
|
||||
} from '@nestjs/common';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { Multipart } from 'fastify-multipart';
|
||||
import { MultipartConfigService } from '../config/multipart.config.service';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class PostFilePipe implements PipeTransform {
|
||||
private readonly logger = new Logger('PostFilePipe');
|
||||
|
||||
constructor(private multipartConfigService: MultipartConfigService) {}
|
||||
|
||||
async transform({ req }: { req: FastifyRequest }) {
|
||||
if (!req.isMultipart()) throw new BadRequestException('Invalid file');
|
||||
|
||||
const file = await req.file({
|
||||
limits: {
|
||||
...this.multipartConfigService.getLimits(),
|
||||
files: 1,
|
||||
},
|
||||
});
|
||||
if (file === undefined) throw new BadRequestException('Invalid file');
|
||||
|
||||
const allFields: Multipart[] = Object.values(file.fields).filter(
|
||||
(entry) => entry,
|
||||
) as any;
|
||||
|
||||
const files = allFields.filter((entry) => entry.file !== undefined);
|
||||
|
||||
if (files.length !== 1) throw new BadRequestException('Invalid file');
|
||||
|
||||
try {
|
||||
return await files[0].toBuffer();
|
||||
} catch (e) {
|
||||
this.logger.warn(e);
|
||||
throw new BadRequestException('Invalid file');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import { dirname, resolve, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../');
|
||||
|
||||
const Config = {
|
||||
main: {
|
||||
host: process.env['PICSUR_HOST'] || '0.0.0.0',
|
||||
port: process.env['PICSUR_PORT'] || 8080,
|
||||
},
|
||||
database: {
|
||||
host: process.env['PICSUR_DB_HOST'] ?? 'localhost',
|
||||
port: process.env['PICSUR_DB_PORT']
|
||||
? parseInt(process.env['PICSUR_DB_PORT'])
|
||||
: 5432,
|
||||
username: process.env['PICSUR_DB_USERNAME'] ?? 'picsur',
|
||||
password: process.env['PICSUR_DB_PASSWORD'] ?? 'picsur',
|
||||
database: process.env['PICSUR_DB_DATABASE'] ?? 'picsur',
|
||||
},
|
||||
defaultAdmin: {
|
||||
username: process.env['PICSUR_ADMIN_USERNAME'] ?? 'admin',
|
||||
password: process.env['PICSUR_ADMIN_PASSWORD'] ?? 'admin',
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env['PICSUR_JWT_SECRET'] ?? 'CHANGE_ME',
|
||||
expiresIn: process.env['PICSUR_JWT_EXPIRES_IN'] ?? '1d',
|
||||
},
|
||||
uploadLimits: {
|
||||
fieldNameSize: 128,
|
||||
fieldSize: 1024,
|
||||
fields: 16,
|
||||
files: 16,
|
||||
fileSize: process.env['PICSUR_MAX_FILE_SIZE']
|
||||
? parseInt(process.env['PICSUR_MAX_FILE_SIZE'])
|
||||
: 128000000,
|
||||
},
|
||||
static: {
|
||||
packageRoot: packageRoot,
|
||||
frontendRoot:
|
||||
process.env['PICSUR_STATIC_FRONTEND_ROOT'] ??
|
||||
join(packageRoot, '../frontend/dist'),
|
||||
backendRoutes: ['i', 'api'],
|
||||
},
|
||||
demo: {
|
||||
enabled: process.env['PICSUR_DEMO']?.toLowerCase() === 'true',
|
||||
interval: process.env['PICSUR_DEMO_INTERVAL']
|
||||
? parseInt(process.env['PICSUR_DEMO_INTERVAL'])
|
||||
: 1000 * 60 * 5,
|
||||
},
|
||||
};
|
||||
|
||||
export default Config;
|
|
@ -9,12 +9,12 @@ import { AppModule } from './app.module';
|
|||
import * as multipart from 'fastify-multipart';
|
||||
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
||||
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
||||
import Config from './env';
|
||||
import { HostConfigService } from './config/host.config.service';
|
||||
|
||||
async function bootstrap() {
|
||||
const fastifyAdapter = new FastifyAdapter();
|
||||
|
||||
// Todo: generic error messages
|
||||
// TODO: generic error messages
|
||||
fastifyAdapter.register(multipart as any);
|
||||
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
|
@ -23,8 +23,15 @@ async function bootstrap() {
|
|||
);
|
||||
app.useGlobalFilters(new MainExceptionFilter());
|
||||
app.useGlobalInterceptors(new SuccessInterceptor());
|
||||
app.useGlobalPipes(new ValidationPipe({ disableErrorMessages: true, forbidUnknownValues: true }));
|
||||
await app.listen(Config.main.port, Config.main.host);
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
disableErrorMessages: true,
|
||||
forbidUnknownValues: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const hostConfigService = app.get(HostConfigService);
|
||||
await app.listen(hostConfigService.getPort(), hostConfigService.getHost());
|
||||
}
|
||||
|
||||
bootstrap().catch(console.error);
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
import { Logger, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { ImageDBModule } from '../../collections/imagedb/imagedb.module';
|
||||
import Config from '../../env';
|
||||
import { PicsurConfigModule } from '../../config/config.module';
|
||||
import { HostConfigService } from '../../config/host.config.service';
|
||||
import { DemoManagerService } from './demomanager.service';
|
||||
|
||||
@Module({
|
||||
imports: [ImageDBModule],
|
||||
imports: [ImageDBModule, PicsurConfigModule],
|
||||
providers: [DemoManagerService],
|
||||
})
|
||||
export class DemoManagerModule implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger('DemoManagerModule');
|
||||
|
||||
constructor(private readonly demoManagerService: DemoManagerService) {}
|
||||
constructor(
|
||||
private readonly demoManagerService: DemoManagerService,
|
||||
private hostConfigService: HostConfigService,
|
||||
) {}
|
||||
|
||||
private interval: NodeJS.Timeout;
|
||||
|
||||
onModuleInit() {
|
||||
if (Config.demo.enabled) {
|
||||
if (this.hostConfigService.isDemo()) {
|
||||
this.logger.log('Demo mode enabled');
|
||||
|
||||
this.interval = setInterval(
|
||||
this.demoManagerService.execute.bind(this.demoManagerService),
|
||||
Config.demo.interval,
|
||||
this.hostConfigService.getDemoInterval(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import { generateRandomString } from 'picsur-shared/dist/util/random';
|
||||
import Config from '../../env';
|
||||
|
||||
export const SysPreferenceDefaults: {
|
||||
[key in SysPreferences]: () => string;
|
||||
} = {
|
||||
jwt_secret: () => {
|
||||
if (Config.jwt.secret !== 'CHANGE_ME') return Config.jwt.secret;
|
||||
return generateRandomString(32);
|
||||
},
|
||||
};
|
5
backend/src/models/entities/index.ts
Normal file
5
backend/src/models/entities/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { EImageBackend } from './image.entity';
|
||||
import { ESysPreferenceBackend } from './syspreference.entity';
|
||||
import { EUserBackend } from './user.entity';
|
||||
|
||||
export const EntityList = [EImageBackend, EUserBackend, ESysPreferenceBackend];
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
Logger,
|
||||
Module,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { ExistingProvider, Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LocalAuthStrategy } from './localauth.strategy';
|
||||
|
@ -10,43 +6,49 @@ import { AuthController } from './auth.controller';
|
|||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { UsersModule } from '../../../collections/userdb/userdb.module';
|
||||
import Config from '../../../env';
|
||||
|
||||
import { SysPreferenceModule } from '../../../collections/syspreferencesdb/syspreferencedb.module';
|
||||
import { JwtConfigService, JwtSecretProvider } from '../../../config/jwt.lateconfig.service';
|
||||
import { PicsurLateConfigModule } from '../../../config/lateconfig.module';
|
||||
import { AuthConfigService } from '../../../config/auth.config.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: Config.jwt.secret,
|
||||
signOptions: { expiresIn: Config.jwt.expiresIn },
|
||||
SysPreferenceModule,
|
||||
PicsurLateConfigModule,
|
||||
JwtModule.registerAsync({
|
||||
useExisting: JwtConfigService,
|
||||
imports: [PicsurLateConfigModule],
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalAuthStrategy, JwtStrategy],
|
||||
providers: [
|
||||
AuthService,
|
||||
LocalAuthStrategy,
|
||||
JwtStrategy,
|
||||
JwtSecretProvider,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule implements OnModuleInit {
|
||||
private readonly logger = new Logger('AuthModule');
|
||||
|
||||
constructor(private authService: AuthService) {}
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private authConfigService: AuthConfigService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.checkJwtSecret();
|
||||
this.ensureAdminExists();
|
||||
}
|
||||
|
||||
private checkJwtSecret() {
|
||||
if (Config.jwt.secret === 'CHANGE_ME') {
|
||||
this.logger.error(
|
||||
"JWT secret is not set. Please set the 'JWT_SECRET' environment variable.",
|
||||
);
|
||||
}
|
||||
async onModuleInit() {
|
||||
await this.ensureAdminExists();
|
||||
}
|
||||
|
||||
private async ensureAdminExists() {
|
||||
const admin = Config.defaultAdmin;
|
||||
this.logger.debug(`Ensuring admin user ${admin.username} exists`);
|
||||
const username = this.authConfigService.getDefaultAdminUsername();
|
||||
const password = this.authConfigService.getDefaultAdminPassword();
|
||||
this.logger.debug(`Ensuring admin user "${username}" exists`);
|
||||
|
||||
await this.authService.createUser(admin.username, admin.password);
|
||||
await this.authService.makeAdmin(admin.username);
|
||||
await this.authService.createUser(username, password);
|
||||
await this.authService.makeAdmin(username);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import Config from '../../../env';
|
||||
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
|
||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
private readonly logger = new Logger('JwtStrategy');
|
||||
|
||||
constructor() {
|
||||
constructor(@Inject('JWT_SECRET') private jwtSecret: string) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: Config.jwt.secret,
|
||||
secretOrKey: jwtSecret,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,10 @@ import {
|
|||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { UpdateSysPreferenceRequest } from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import {
|
||||
SysPreferences,
|
||||
UpdateSysPreferenceRequest,
|
||||
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
|
||||
import { AdminGuard } from '../auth/admin.guard';
|
||||
|
@ -21,7 +24,9 @@ export class PrefController {
|
|||
|
||||
@Get('sys/:key')
|
||||
async getSysPref(@Param('key') key: string) {
|
||||
const returned = await this.prefService.getPreference(key);
|
||||
const returned = await this.prefService.getPreference(
|
||||
key as SysPreferences,
|
||||
);
|
||||
if (HasFailed(returned)) {
|
||||
console.warn(returned.getReason());
|
||||
throw new InternalServerErrorException('Could not get preference');
|
||||
|
@ -36,7 +41,10 @@ export class PrefController {
|
|||
@Body() body: UpdateSysPreferenceRequest,
|
||||
) {
|
||||
const value = body.value;
|
||||
const returned = await this.prefService.setPreference(key, value);
|
||||
const returned = await this.prefService.setPreference(
|
||||
key as SysPreferences,
|
||||
value,
|
||||
);
|
||||
if (HasFailed(returned)) {
|
||||
console.warn(returned.getReason());
|
||||
throw new InternalServerErrorException('Could not set preference');
|
||||
|
|
|
@ -11,10 +11,10 @@ import {
|
|||
} from '@nestjs/common';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { MultiPart } from '../../decorators/multipart.decorator';
|
||||
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
|
||||
import { isHash } from 'class-validator';
|
||||
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
|
||||
import { MultiPart } from '../../decorators/decorator';
|
||||
@Controller('i')
|
||||
export class ImageController {
|
||||
constructor(private readonly imagesService: ImageManagerService) {}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { DecoratorsModule } from '../../decorators/decorators.module';
|
||||
import { ImageManagerModule } from '../../managers/imagemanager/imagemanager.module';
|
||||
import { ImageController } from './imageroute.controller';
|
||||
|
||||
@Module({
|
||||
imports: [ImageManagerModule],
|
||||
imports: [ImageManagerModule, DecoratorsModule],
|
||||
controllers: [ImageController],
|
||||
})
|
||||
export class ImageModule {}
|
||||
|
|
|
@ -3,7 +3,7 @@ import tuple from '../types/tuple';
|
|||
import { randomBytes } from 'crypto';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
const SysPreferencesTuple = tuple('jwt_secret');
|
||||
const SysPreferencesTuple = tuple('jwt_secret', 'jwt_expires_in');
|
||||
|
||||
export const SysPreferences: string[] = SysPreferencesTuple;
|
||||
export type SysPreferences = typeof SysPreferencesTuple[number];
|
||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -1409,6 +1409,16 @@
|
|||
tslib "2.3.1"
|
||||
uuid "8.3.2"
|
||||
|
||||
"@nestjs/config@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-1.2.0.tgz#a4eb58390dd8145b761ee0c8e98e78c8471cabb1"
|
||||
integrity sha512-GGZOj2g6EMZ23orsQqeD2Vs5E2ZrmAiB0qCGvERv+5nQmZjY4nKkisG4awQsym1uotmmzgtsd9lOiKqTIFONhA==
|
||||
dependencies:
|
||||
dotenv "16.0.0"
|
||||
dotenv-expand "8.0.1"
|
||||
lodash "4.17.21"
|
||||
uuid "8.3.2"
|
||||
|
||||
"@nestjs/core@^8.4.0":
|
||||
version "8.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-8.4.0.tgz#f4c1840b9b233e3985407f496b7c1e78e0169c70"
|
||||
|
@ -3298,6 +3308,16 @@ domutils@^2.8.0:
|
|||
domelementtype "^2.2.0"
|
||||
domhandler "^4.2.0"
|
||||
|
||||
dotenv-expand@8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.1.tgz#332aa17c14b12e28e2e230f8d183eecc1c014fdc"
|
||||
integrity sha512-j/Ih7bIERDR5PzI89Zu8ayd3tXZ6E3dbY0ljQ9Db0K87qBO8zdLsi2dIvDHMWtjC3Yxb8XixOTHAtia0fDHRpg==
|
||||
|
||||
dotenv@16.0.0:
|
||||
version "16.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411"
|
||||
integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==
|
||||
|
||||
dotenv@^8.2.0:
|
||||
version "8.6.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
|
||||
|
@ -5164,7 +5184,7 @@ lodash.once@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
|
||||
|
||||
lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
|
||||
lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
|
Loading…
Reference in a new issue