Merge pull request #733 from hedgedoc/config/split

This commit is contained in:
David Mehren 2021-01-19 12:58:23 +01:00 committed by GitHub
commit 9b552a6ead
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 289 additions and 273 deletions

View file

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

View file

@ -19,6 +19,11 @@ import { PermissionsModule } from './permissions/permissions.module';
import { RevisionsModule } from './revisions/revisions.module';
import { UsersModule } from './users/users.module';
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({
imports: [
@ -29,7 +34,14 @@ import appConfig from './config/app.config';
synchronize: true,
}),
ConfigModule.forRoot({
load: [appConfig],
load: [
appConfig,
mediaConfig,
hstsConfig,
cspConfig,
databaseConfig,
authConfig,
],
isGlobal: true,
}),
NotesModule,

View file

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

View file

@ -7,46 +7,11 @@
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { Loglevel } from './loglevel.enum';
import { appConfigHsts, HstsConfig, hstsSchema } from './hsts-config';
import { appConfigCsp, CspConfig, cspSchema } from './csp-config';
import { appConfigMedia, MediaConfig, mediaSchema } from './media-config';
import {
appConfigDatabase,
DatabaseConfig,
databaseSchema,
} from './database-config';
import { appConfigAuth, AuthConfig, authSchema } from './auth-config';
// import { LinkifyHeaderStyle } from './linkify-header-style';
export interface AppConfig {
domain: string;
port: number;
loglevel: Loglevel;
/*linkifyHeaderStyle: LinkifyHeaderStyle;
sourceURL: string;
urlPath: string;
host: string;
path: string;
urlAddPort: boolean;
cookiePolicy: string;
protocolUseSSL: boolean;
allowOrigin: string[];
useCDN: boolean;
enableAnonymous: boolean;
enableAnonymousEdits: boolean;
enableFreeURL: boolean;
forbiddenNoteIDs: string[];
defaultPermission: string;
sessionSecret: string;
sessionLife: number;
tooBusyLag: number;
enableGravatar: boolean;*/
hsts: HstsConfig;
csp: CspConfig;
media: MediaConfig;
database: DatabaseConfig;
auth: AuthConfig;
}
const schema = Joi.object({
@ -56,30 +21,6 @@ const schema = Joi.object({
.valid(...Object.values(Loglevel))
.default(Loglevel.WARN)
.optional(),
/*linkifyHeaderStyle: Joi.string().valid(...Object.values(LinkifyHeaderStyle)).default(LinkifyHeaderStyle.GFM).optional(),
sourceURL: Joi.string(),
urlPath: Joi.string(),
host: Joi.string().default('::').optional(),
path: Joi.string(),
urlAddPort: Joi.boolean().default(false).optional(),
cookiePolicy: Joi.string(),
protocolUseSSL: Joi.boolean().default(true).optional(),
allowOrigin: Joi.array().items(Joi.string()),
useCDN: Joi.boolean().default(false).optional(),
enableAnonymous: Joi.boolean().default(true).optional(),
enableAnonymousEdits: Joi.boolean().default(false).optional(),
enableFreeURL: Joi.boolean().default(false).optional(),
forbiddenNoteIDs: Joi.array().items(Joi.string()),
defaultPermission: Joi.string(),
sessionSecret: Joi.string(),
sessionLife: Joi.number().default(14 * 24 * 60 * 60 * 1000).optional(),
tooBusyLag: Joi.number().default(70).optional(),
enableGravatar: Joi.boolean().default(true).optional(),*/
hsts: hstsSchema,
csp: cspSchema,
media: mediaSchema,
database: databaseSchema,
auth: authSchema,
});
export default registerAs('appConfig', async () => {
@ -87,31 +28,7 @@ export default registerAs('appConfig', async () => {
{
domain: process.env.HD_DOMAIN,
port: parseInt(process.env.PORT) || undefined,
loglevel: process.env.HD_LOGLEVEL, //|| Loglevel.WARN,
/*linkifyHeaderStyle: process.env.HD_LINKIFY_HEADER_STYLE,
sourceURL: process.env.HD_SOURCE_URL,
urlPath: process.env.HD_URL_PATH,
host: process.env.HD_HOST || '::',
path: process.env.HD_PATH,
urlAddPort: process.env.HD_URL_ADDPORT,
cookiePolicy: process.env.HD_COOKIE_POLICY,
protocolUseSSL: process.env.HD_PROTOCOL_USESSL || true,
allowOrigin: toArrayConfig(process.env.HD_ALLOW_ORIGIN),
useCDN: process.env.HD_USECDN,
enableAnonymous: process.env.HD_ENABLE_ANONYMOUS || true,
enableAnonymousEdits: process.env.HD_ENABLE_ANONYMOUS_EDITS,
enableFreeURL: process.env.HD_ENABLE_FREEURL,
forbiddenNoteIDs: toArrayConfig(process.env.HD_FORBIDDEN_NOTE_IDS),
defaultPermission: process.env.HD_DEFAULT_PERMISSION,
sessionSecret: process.env.HD_SESSION_SECRET,
sessionLife: parseInt(process.env.HD_SESSION_LIFE) || 14 * 24 * 60 * 60 * 1000,
tooBusyLag: parseInt(process.env.HD_TOOBUSY_LAG) || 70,
enableGravatar: process.env.HD_ENABLE_GRAVATAR || true,*/
hsts: appConfigHsts,
csp: appConfigCsp,
media: appConfigMedia,
database: appConfigDatabase,
auth: appConfigAuth,
loglevel: process.env.HD_LOGLEVEL,
},
{
abortEarly: false,

View file

@ -7,6 +7,7 @@
import * as Joi from 'joi';
import { GitlabScope, GitlabVersion } from './gitlab.enum';
import { toArrayConfig } from './utils';
import { registerAs } from '@nestjs/config';
export interface AuthConfig {
email: {
@ -99,7 +100,7 @@ export interface AuthConfig {
];
}
export const authSchema = Joi.object({
const authSchema = Joi.object({
email: {
enableLogin: 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: {
enableLogin: process.env.HD_AUTH_EMAIL_ENABLE_LOGIN,
enableRegister: process.env.HD_AUTH_EMAIL_ENABLE_REGISTER,
@ -328,4 +331,14 @@ export const appConfigAuth = {
ldap: ldaps,
saml: samls,
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

@ -1,11 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum LinkifyHeaderStyle {
KEEP_CASE = 'keep-case',
LOWER_CASE = 'lower-case',
GFM = 'gfm',
}

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 { BackendType } from '../media/backends/backend-type.enum';
import { registerAs } from '@nestjs/config';
export interface MediaConfig {
backend: {
@ -30,7 +31,7 @@ export interface MediaConfig {
};
}
export const mediaSchema = Joi.object({
const mediaSchema = Joi.object({
backend: {
use: Joi.string().valid(...Object.values(BackendType)),
filesystem: {
@ -69,7 +70,9 @@ export const mediaSchema = Joi.object({
},
});
export const appConfigMedia = {
export default registerAs('mediaConfig', async () => {
const mediaConfig = mediaSchema.validate(
{
backend: {
use: process.env.HD_MEDIA_BACKEND,
filesystem: {
@ -83,11 +86,22 @@ export const appConfigMedia = {
port: parseInt(process.env.HD_MEDIA_BACKEND_S3_PORT) || undefined,
},
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,
},
imgur: {
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 { AppConfig } from './config/app.config';
import { NestConsoleLoggerService } from './logger/nest-console-logger.service';
import { MediaConfig } from './config/media.config';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
@ -20,6 +21,7 @@ async function bootstrap() {
app.useLogger(logger);
const configService = app.get(ConfigService);
const appConfig = configService.get<AppConfig>('appConfig');
const mediaConfig = configService.get<MediaConfig>('mediaConfig');
const swaggerOptions = new DocumentBuilder()
.setTitle('HedgeDoc')
@ -35,9 +37,9 @@ async function bootstrap() {
transform: true,
}),
);
if (appConfig.media.backend.use === 'filesystem') {
if (mediaConfig.backend.use === 'filesystem') {
app.useStaticAssets('uploads', {
prefix: appConfig.media.backend.filesystem.uploadPath,
prefix: mediaConfig.backend.filesystem.uploadPath,
});
}
await app.listen(appConfig.port);

View file

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

View file

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

View file

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

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigModule, registerAs } from '@nestjs/config';
import { ConfigModule } from '@nestjs/config';
import { NestExpressApplication } from '@nestjs/platform-express';
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import * as request from 'supertest';
import { PublicApiModule } from '../../src/api/public/public-api.module';
import appConfigMock from '../../src/config/app.config.mock';
import mediaConfigMock from '../../src/config/media.config.mock';
import { GroupsModule } from '../../src/groups/groups.module';
import { LoggerModule } from '../../src/logger/logger.module';
import { NestConsoleLoggerService } from '../../src/logger/nest-console-logger.service';
@ -31,7 +31,7 @@ describe('Notes', () => {
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [appConfigMock],
load: [mediaConfigMock],
}),
PublicApiModule,
MediaModule,

View file

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