config: splits config in multiple files

splits the big appConfig in multiple configs
adds media.config.mock.ts

Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2021-01-15 16:57:04 +01:00
parent 4f6d15439c
commit 2c4098dc55
18 changed files with 287 additions and 174 deletions

View file

@ -8,6 +8,7 @@ import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import appConfigMock from '../../../config/app.config.mock'; import appConfigMock from '../../../config/app.config.mock';
import mediaConfigMock from '../../../config/media.config.mock';
import { LoggerModule } from '../../../logger/logger.module'; import { LoggerModule } from '../../../logger/logger.module';
import { MediaUpload } from '../../../media/media-upload.entity'; import { MediaUpload } from '../../../media/media-upload.entity';
import { MediaModule } from '../../../media/media.module'; import { MediaModule } from '../../../media/media.module';
@ -31,7 +32,7 @@ describe('Media Controller', () => {
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
load: [appConfigMock], load: [appConfigMock, mediaConfigMock],
}), }),
LoggerModule, LoggerModule,
MediaModule, MediaModule,

View file

@ -19,6 +19,11 @@ import { PermissionsModule } from './permissions/permissions.module';
import { RevisionsModule } from './revisions/revisions.module'; import { RevisionsModule } from './revisions/revisions.module';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
import appConfig from './config/app.config'; import appConfig from './config/app.config';
import mediaConfig from './config/media.config';
import hstsConfig from './config/hsts.config';
import cspConfig from './config/csp.config';
import databaseConfig from './config/database.config';
import authConfig from './config/auth.config';
@Module({ @Module({
imports: [ imports: [
@ -29,7 +34,14 @@ import appConfig from './config/app.config';
synchronize: true, synchronize: true,
}), }),
ConfigModule.forRoot({ ConfigModule.forRoot({
load: [appConfig], load: [
appConfig,
mediaConfig,
hstsConfig,
cspConfig,
databaseConfig,
authConfig,
],
isGlobal: true, isGlobal: true,
}), }),
NotesModule, NotesModule,

View file

@ -8,12 +8,4 @@ import { registerAs } from '@nestjs/config';
export default registerAs('appConfig', () => ({ export default registerAs('appConfig', () => ({
port: 3000, port: 3000,
media: {
backend: {
use: 'filesystem',
filesystem: {
uploadPath: 'uploads',
},
},
},
})); }));

View file

@ -7,6 +7,7 @@
import * as Joi from 'joi'; import * as Joi from 'joi';
import { GitlabScope, GitlabVersion } from './gitlab.enum'; import { GitlabScope, GitlabVersion } from './gitlab.enum';
import { toArrayConfig } from './utils'; import { toArrayConfig } from './utils';
import { registerAs } from '@nestjs/config';
export interface AuthConfig { export interface AuthConfig {
email: { email: {
@ -99,7 +100,7 @@ export interface AuthConfig {
]; ];
} }
export const authSchema = Joi.object({ const authSchema = Joi.object({
email: { email: {
enableLogin: Joi.boolean().default(false).optional(), enableLogin: Joi.boolean().default(false).optional(),
enableRegister: Joi.boolean().default(false).optional(), enableRegister: Joi.boolean().default(false).optional(),
@ -297,7 +298,9 @@ const oauth2s = oauth2Names.map((oauth2Name) => {
}; };
}); });
export const appConfigAuth = { export default registerAs('authConfig', async () => {
const authConfig = authSchema.validate(
{
email: { email: {
enableLogin: process.env.HD_AUTH_EMAIL_ENABLE_LOGIN, enableLogin: process.env.HD_AUTH_EMAIL_ENABLE_LOGIN,
enableRegister: process.env.HD_AUTH_EMAIL_ENABLE_REGISTER, enableRegister: process.env.HD_AUTH_EMAIL_ENABLE_REGISTER,
@ -328,4 +331,14 @@ export const appConfigAuth = {
ldap: ldaps, ldap: ldaps,
saml: samls, saml: samls,
oauth2: oauth2s, oauth2: oauth2s,
}; },
{
abortEarly: false,
presence: 'required',
},
);
if (authConfig.error) {
throw new Error(authConfig.error.toString());
}
return authConfig.value;
});

View file

@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Joi from 'joi';
export interface CspConfig {
enable: boolean;
maxAgeSeconds: number;
includeSubdomains: boolean;
preload: boolean;
}
export const cspSchema = Joi.object({
enable: Joi.boolean().default(true).optional(),
reportURI: Joi.string().optional(),
});
export const appConfigCsp = {
enable: process.env.HD_CSP_ENABLE || true,
reportURI: process.env.HD_CSP_REPORTURI,
};

37
src/config/csp.config.ts Normal file
View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Joi from 'joi';
import { registerAs } from '@nestjs/config';
export interface CspConfig {
enable: boolean;
maxAgeSeconds: number;
includeSubdomains: boolean;
preload: boolean;
}
const cspSchema = Joi.object({
enable: Joi.boolean().default(true).optional(),
reportURI: Joi.string().optional(),
});
export default registerAs('cspConfig', async () => {
const cspConfig = cspSchema.validate(
{
enable: process.env.HD_CSP_ENABLE || true,
reportURI: process.env.HD_CSP_REPORTURI,
},
{
abortEarly: false,
presence: 'required',
},
);
if (cspConfig.error) {
throw new Error(cspConfig.error.toString());
}
return cspConfig.value;
});

View file

@ -1,42 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Joi from 'joi';
import { DatabaseDialect } from './database-dialect.enum';
export interface DatabaseConfig {
username: string;
password: string;
database: string;
host: string;
port: number;
storage: string;
dialect: DatabaseDialect;
}
export const databaseSchema = Joi.object({
username: Joi.string(),
password: Joi.string(),
database: Joi.string(),
host: Joi.string(),
port: Joi.number(),
storage: Joi.when('...dialect', {
is: Joi.valid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
dialect: Joi.string().valid(...Object.values(DatabaseDialect)),
});
export const appConfigDatabase = {
username: process.env.HD_DATABASE_USER,
password: process.env.HD_DATABASE_PASS,
database: process.env.HD_DATABASE_NAME,
host: process.env.HD_DATABASE_HOST,
port: parseInt(process.env.HD_DATABASE_PORT) || undefined,
storage: process.env.HD_DATABASE_STORAGE,
dialect: process.env.HD_DATABASE_DIALECT,
};

View file

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Joi from 'joi';
import { DatabaseDialect } from './database-dialect.enum';
import { registerAs } from '@nestjs/config';
export interface DatabaseConfig {
username: string;
password: string;
database: string;
host: string;
port: number;
storage: string;
dialect: DatabaseDialect;
}
const databaseSchema = Joi.object({
username: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
password: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
database: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
host: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
port: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.number(),
otherwise: Joi.optional(),
}),
storage: Joi.when('dialect', {
is: Joi.valid(DatabaseDialect.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}),
dialect: Joi.string().valid(...Object.values(DatabaseDialect)),
});
export default registerAs('databaseConfig', async () => {
const databaseConfig = databaseSchema.validate(
{
username: process.env.HD_DATABASE_USER,
password: process.env.HD_DATABASE_PASS,
database: process.env.HD_DATABASE_NAME,
host: process.env.HD_DATABASE_HOST,
port: parseInt(process.env.HD_DATABASE_PORT) || undefined,
storage: process.env.HD_DATABASE_STORAGE,
dialect: process.env.HD_DATABASE_DIALECT,
},
{
abortEarly: false,
presence: 'required',
},
);
if (databaseConfig.error) {
throw new Error(databaseConfig.error.toString());
}
return databaseConfig.value;
});

View file

@ -1,30 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Joi from 'joi';
export interface HstsConfig {
enable: boolean;
maxAgeSeconds: number;
includeSubdomains: boolean;
preload: boolean;
}
export const hstsSchema = Joi.object({
enable: Joi.boolean().default(true).optional(),
maxAgeSeconds: Joi.number()
.default(60 * 60 * 24 * 365)
.optional(),
includeSubdomains: Joi.boolean().default(true).optional(),
preload: Joi.boolean().default(true).optional(),
});
export const appConfigHsts = {
enable: process.env.HD_HSTS_ENABLE,
maxAgeSeconds: parseInt(process.env.HD_HSTS_MAX_AGE) || undefined,
includeSubdomains: process.env.HD_HSTS_INCLUDE_SUBDOMAINS,
preload: process.env.HD_HSTS_PRELOAD,
};

43
src/config/hsts.config.ts Normal file
View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Joi from 'joi';
import { registerAs } from '@nestjs/config';
export interface HstsConfig {
enable: boolean;
maxAgeSeconds: number;
includeSubdomains: boolean;
preload: boolean;
}
const hstsSchema = Joi.object({
enable: Joi.boolean().default(true).optional(),
maxAgeSeconds: Joi.number()
.default(60 * 60 * 24 * 365)
.optional(),
includeSubdomains: Joi.boolean().default(true).optional(),
preload: Joi.boolean().default(true).optional(),
});
export default registerAs('hstsConfig', async () => {
const hstsConfig = hstsSchema.validate(
{
enable: process.env.HD_HSTS_ENABLE,
maxAgeSeconds: parseInt(process.env.HD_HSTS_MAX_AGE) || undefined,
includeSubdomains: process.env.HD_HSTS_INCLUDE_SUBDOMAINS,
preload: process.env.HD_HSTS_PRELOAD,
},
{
abortEarly: false,
presence: 'required',
},
);
if (hstsConfig.error) {
throw new Error(hstsConfig.error.toString());
}
return hstsConfig.value;
});

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
export default registerAs('mediaConfig', () => ({
backend: {
use: 'filesystem',
filesystem: {
uploadPath: 'uploads',
},
},
}));

View file

@ -6,6 +6,7 @@
import * as Joi from 'joi'; import * as Joi from 'joi';
import { BackendType } from '../media/backends/backend-type.enum'; import { BackendType } from '../media/backends/backend-type.enum';
import { registerAs } from '@nestjs/config';
export interface MediaConfig { export interface MediaConfig {
backend: { backend: {
@ -30,7 +31,7 @@ export interface MediaConfig {
}; };
} }
export const mediaSchema = Joi.object({ const mediaSchema = Joi.object({
backend: { backend: {
use: Joi.string().valid(...Object.values(BackendType)), use: Joi.string().valid(...Object.values(BackendType)),
filesystem: { filesystem: {
@ -69,7 +70,9 @@ export const mediaSchema = Joi.object({
}, },
}); });
export const appConfigMedia = { export default registerAs('mediaConfig', async () => {
const mediaConfig = mediaSchema.validate(
{
backend: { backend: {
use: process.env.HD_MEDIA_BACKEND, use: process.env.HD_MEDIA_BACKEND,
filesystem: { filesystem: {
@ -83,11 +86,22 @@ export const appConfigMedia = {
port: parseInt(process.env.HD_MEDIA_BACKEND_S3_PORT) || undefined, port: parseInt(process.env.HD_MEDIA_BACKEND_S3_PORT) || undefined,
}, },
azure: { azure: {
connectionString: process.env.HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING, connectionString:
process.env.HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING,
container: process.env.HD_MEDIA_BACKEND_AZURE_CONTAINER, container: process.env.HD_MEDIA_BACKEND_AZURE_CONTAINER,
}, },
imgur: { imgur: {
clientID: process.env.HD_MEDIA_BACKEND_IMGUR_CLIENTID, clientID: process.env.HD_MEDIA_BACKEND_IMGUR_CLIENTID,
}, },
}, },
}; },
{
abortEarly: false,
presence: 'required',
},
);
if (mediaConfig.error) {
throw new Error(mediaConfig.error.toString());
}
return mediaConfig.value;
});

View file

@ -12,6 +12,7 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { AppConfig } from './config/app.config'; import { AppConfig } from './config/app.config';
import { NestConsoleLoggerService } from './logger/nest-console-logger.service'; import { NestConsoleLoggerService } from './logger/nest-console-logger.service';
import { MediaConfig } from './config/media.config';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule);
@ -20,6 +21,7 @@ async function bootstrap() {
app.useLogger(logger); app.useLogger(logger);
const configService = app.get(ConfigService); const configService = app.get(ConfigService);
const appConfig = configService.get<AppConfig>('appConfig'); const appConfig = configService.get<AppConfig>('appConfig');
const mediaConfig = configService.get<MediaConfig>('mediaConfig');
const swaggerOptions = new DocumentBuilder() const swaggerOptions = new DocumentBuilder()
.setTitle('HedgeDoc') .setTitle('HedgeDoc')
@ -35,9 +37,9 @@ async function bootstrap() {
transform: true, transform: true,
}), }),
); );
if (appConfig.media.backend.use === 'filesystem') { if (mediaConfig.backend.use === 'filesystem') {
app.useStaticAssets('uploads', { app.useStaticAssets('uploads', {
prefix: appConfig.media.backend.filesystem.uploadPath, prefix: mediaConfig.backend.filesystem.uploadPath,
}); });
} }
await app.listen(appConfig.port); await app.listen(appConfig.port);

View file

@ -7,10 +7,11 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { join } from 'path'; import { join } from 'path';
import applicationConfig, { AppConfig } from '../../config/app.config'; import mediaConfiguration from '../../config/media.config';
import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface'; import { MediaBackend } from '../media-backend.interface';
import { BackendData } from '../media-upload.entity'; import { BackendData } from '../media-upload.entity';
import { MediaConfig } from '../../config/media.config';
@Injectable() @Injectable()
export class FilesystemBackend implements MediaBackend { export class FilesystemBackend implements MediaBackend {
@ -18,11 +19,11 @@ export class FilesystemBackend implements MediaBackend {
constructor( constructor(
private readonly logger: ConsoleLoggerService, private readonly logger: ConsoleLoggerService,
@Inject(applicationConfig.KEY) @Inject(mediaConfiguration.KEY)
private appConfig: AppConfig, private mediaConfig: MediaConfig,
) { ) {
this.logger.setContext(FilesystemBackend.name); this.logger.setContext(FilesystemBackend.name);
this.uploadDirectory = appConfig.media.backend.filesystem.uploadPath; this.uploadDirectory = mediaConfig.backend.filesystem.uploadPath;
} }
async saveFile( async saveFile(

View file

@ -8,6 +8,7 @@ import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import appConfigMock from '../config/app.config.mock'; import appConfigMock from '../config/app.config.mock';
import mediaConfigMock from '../config/media.config.mock';
import { LoggerModule } from '../logger/logger.module'; import { LoggerModule } from '../logger/logger.module';
import { AuthorColor } from '../notes/author-color.entity'; import { AuthorColor } from '../notes/author-color.entity';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
@ -39,7 +40,7 @@ describe('MediaService', () => {
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
load: [appConfigMock], load: [appConfigMock, mediaConfigMock],
}), }),
LoggerModule, LoggerModule,
NotesModule, NotesModule,

View file

@ -9,7 +9,7 @@ import { ModuleRef } from '@nestjs/core';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import * as FileType from 'file-type'; import * as FileType from 'file-type';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import applicationConfig, { AppConfig } from '../config/app.config'; import mediaConfiguration, { MediaConfig } from '../config/media.config';
import { ClientError, NotInDBError, PermissionError } from '../errors/errors'; import { ClientError, NotInDBError, PermissionError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { NotesService } from '../notes/notes.service'; import { NotesService } from '../notes/notes.service';
@ -31,8 +31,8 @@ export class MediaService {
private notesService: NotesService, private notesService: NotesService,
private usersService: UsersService, private usersService: UsersService,
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@Inject(applicationConfig.KEY) @Inject(mediaConfiguration.KEY)
private appConfig: AppConfig, private mediaConfig: MediaConfig,
) { ) {
this.logger.setContext(MediaService.name); this.logger.setContext(MediaService.name);
this.mediaBackendType = this.chooseBackendType(); this.mediaBackendType = this.chooseBackendType();
@ -120,7 +120,7 @@ export class MediaService {
} }
private chooseBackendType(): BackendType { private chooseBackendType(): BackendType {
switch (this.appConfig.media.backend.use) { switch (this.mediaConfig.backend.use) {
case 'filesystem': case 'filesystem':
return BackendType.FILESYSTEM; return BackendType.FILESYSTEM;
} }

View file

@ -12,6 +12,7 @@ import { promises as fs } from 'fs';
import * as request from 'supertest'; import * as request from 'supertest';
import { PublicApiModule } from '../../src/api/public/public-api.module'; import { PublicApiModule } from '../../src/api/public/public-api.module';
import appConfigMock from '../../src/config/app.config.mock'; import appConfigMock from '../../src/config/app.config.mock';
import mediaConfigMock from '../../src/config/media.config.mock';
import { GroupsModule } from '../../src/groups/groups.module'; import { GroupsModule } from '../../src/groups/groups.module';
import { LoggerModule } from '../../src/logger/logger.module'; import { LoggerModule } from '../../src/logger/logger.module';
import { NestConsoleLoggerService } from '../../src/logger/nest-console-logger.service'; import { NestConsoleLoggerService } from '../../src/logger/nest-console-logger.service';
@ -31,7 +32,7 @@ describe('Notes', () => {
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
load: [appConfigMock], load: [appConfigMock, mediaConfigMock],
}), }),
PublicApiModule, PublicApiModule,
MediaModule, MediaModule,

View file

@ -5,12 +5,13 @@
*/ */
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { ConfigModule, registerAs } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import * as request from 'supertest'; import * as request from 'supertest';
import { PublicApiModule } from '../../src/api/public/public-api.module'; import { PublicApiModule } from '../../src/api/public/public-api.module';
import appConfigMock from '../../src/config/app.config.mock'; import appConfigMock from '../../src/config/app.config.mock';
import mediaConfigMock from '../../src/config/media.config.mock';
import { NotInDBError } from '../../src/errors/errors'; import { NotInDBError } from '../../src/errors/errors';
import { GroupsModule } from '../../src/groups/groups.module'; import { GroupsModule } from '../../src/groups/groups.module';
import { LoggerModule } from '../../src/logger/logger.module'; import { LoggerModule } from '../../src/logger/logger.module';
@ -27,7 +28,7 @@ describe('Notes', () => {
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
load: [appConfigMock], load: [appConfigMock, mediaConfigMock],
}), }),
PublicApiModule, PublicApiModule,
NotesModule, NotesModule,