mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-09 13:51:57 -04:00
refactor(backend): config validation joi to zod
Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
91ebd519a8
commit
748702daf5
37 changed files with 1299 additions and 994 deletions
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -19,7 +19,9 @@ import appConfig from './config/app.config';
|
||||||
import authConfig from './config/auth.config';
|
import authConfig from './config/auth.config';
|
||||||
import cspConfig from './config/csp.config';
|
import cspConfig from './config/csp.config';
|
||||||
import customizationConfig from './config/customization.config';
|
import customizationConfig from './config/customization.config';
|
||||||
import databaseConfig, { DatabaseConfig } from './config/database.config';
|
import databaseConfig, {
|
||||||
|
PostgresDatabaseConfig,
|
||||||
|
} from './config/database.config';
|
||||||
import externalConfig from './config/external-services.config';
|
import externalConfig from './config/external-services.config';
|
||||||
import mediaConfig from './config/media.config';
|
import mediaConfig from './config/media.config';
|
||||||
import noteConfig from './config/note.config';
|
import noteConfig from './config/note.config';
|
||||||
|
@ -63,7 +65,7 @@ const routes: Routes = [
|
||||||
imports: [ConfigModule, LoggerModule],
|
imports: [ConfigModule, LoggerModule],
|
||||||
inject: [databaseConfig.KEY, TypeormLoggerService],
|
inject: [databaseConfig.KEY, TypeormLoggerService],
|
||||||
useFactory: (
|
useFactory: (
|
||||||
databaseConfig: DatabaseConfig,
|
databaseConfig: PostgresDatabaseConfig,
|
||||||
logger: TypeormLoggerService,
|
logger: TypeormLoggerService,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
|
@ -72,7 +74,7 @@ const routes: Routes = [
|
||||||
port: databaseConfig.port,
|
port: databaseConfig.port,
|
||||||
username: databaseConfig.username,
|
username: databaseConfig.username,
|
||||||
password: databaseConfig.password,
|
password: databaseConfig.password,
|
||||||
database: databaseConfig.database,
|
database: databaseConfig.name,
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
logging: true,
|
logging: true,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|
|
@ -16,7 +16,7 @@ import LdapAuth from 'ldapauth-fork';
|
||||||
|
|
||||||
import authConfiguration, {
|
import authConfiguration, {
|
||||||
AuthConfig,
|
AuthConfig,
|
||||||
LDAPConfig,
|
LdapConfig,
|
||||||
} from '../../config/auth.config';
|
} from '../../config/auth.config';
|
||||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export class LdapService {
|
||||||
/**
|
/**
|
||||||
* Try to log in the user with the given credentials.
|
* Try to log in the user with the given credentials.
|
||||||
*
|
*
|
||||||
* @param ldapConfig {LDAPConfig} - the ldap config to use
|
* @param ldapConfig {LdapConfig} - the ldap config to use
|
||||||
* @param username {string} - the username to log in with
|
* @param username {string} - the username to log in with
|
||||||
* @param password {string} - the password to log in with
|
* @param password {string} - the password to log in with
|
||||||
* @returns {FullUserInfoWithIdDto} - the user info of the user that logged in
|
* @returns {FullUserInfoWithIdDto} - the user info of the user that logged in
|
||||||
|
@ -56,7 +56,7 @@ export class LdapService {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
getUserInfoFromLdap(
|
getUserInfoFromLdap(
|
||||||
ldapConfig: LDAPConfig,
|
ldapConfig: LdapConfig,
|
||||||
username: string, // This is not of type Username, because LDAP server may use mixed case usernames
|
username: string, // This is not of type Username, because LDAP server may use mixed case usernames
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<FullUserInfoWithIdDto> {
|
): Promise<FullUserInfoWithIdDto> {
|
||||||
|
@ -120,12 +120,12 @@ export class LdapService {
|
||||||
/**
|
/**
|
||||||
* Get and return the correct ldap config from the list of available configs.
|
* Get and return the correct ldap config from the list of available configs.
|
||||||
* @param {string} ldapIdentifier the identifier for the ldap config to be used
|
* @param {string} ldapIdentifier the identifier for the ldap config to be used
|
||||||
* @returns {LDAPConfig} - the ldap config with the given identifier
|
* @returns {LdapConfig} - the ldap config with the given identifier
|
||||||
* @throws {NotFoundException} - there is no ldap config with the given identifier
|
* @throws {NotFoundException} - there is no ldap config with the given identifier
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
getLdapConfig(ldapIdentifier: string): LDAPConfig {
|
getLdapConfig(ldapIdentifier: string): LdapConfig {
|
||||||
const ldapConfig: LDAPConfig | undefined = this.authConfig.ldap.find(
|
const ldapConfig = this.authConfig.ldap.find(
|
||||||
(config) => config.identifier === ldapIdentifier,
|
(config) => config.identifier === ldapIdentifier,
|
||||||
);
|
);
|
||||||
if (!ldapConfig) {
|
if (!ldapConfig) {
|
||||||
|
|
|
@ -94,7 +94,7 @@ export class OidcService {
|
||||||
const redirectUri = `${this.appConfig.baseUrl}/api/private/auth/oidc/${oidcConfig.identifier}/callback`;
|
const redirectUri = `${this.appConfig.baseUrl}/api/private/auth/oidc/${oidcConfig.identifier}/callback`;
|
||||||
const client = new issuer.Client({
|
const client = new issuer.Client({
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
client_id: oidcConfig.clientID,
|
client_id: oidcConfig.clientId,
|
||||||
client_secret: oidcConfig.clientSecret,
|
client_secret: oidcConfig.clientSecret,
|
||||||
redirect_uris: [redirectUri],
|
redirect_uris: [redirectUri],
|
||||||
response_types: ['code'],
|
response_types: ['code'],
|
||||||
|
@ -205,7 +205,7 @@ export class OidcService {
|
||||||
);
|
);
|
||||||
const username = OidcService.getResponseFieldValue(
|
const username = OidcService.getResponseFieldValue(
|
||||||
userInfoResponse,
|
userInfoResponse,
|
||||||
oidcConfig.userNameField,
|
oidcConfig.usernameField,
|
||||||
userId,
|
userId,
|
||||||
).toLowerCase() as Lowercase<string>;
|
).toLowerCase() as Lowercase<string>;
|
||||||
const displayName = OidcService.getResponseFieldValue(
|
const displayName = OidcService.getResponseFieldValue(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -42,7 +42,7 @@ describe('appConfig', () => {
|
||||||
const config = appConfig();
|
const config = appConfig();
|
||||||
expect(config.baseUrl).toEqual(baseUrl);
|
expect(config.baseUrl).toEqual(baseUrl);
|
||||||
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
||||||
expect(config.port).toEqual(port);
|
expect(config.backendPort).toEqual(port);
|
||||||
expect(config.loglevel).toEqual(loglevel);
|
expect(config.loglevel).toEqual(loglevel);
|
||||||
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
||||||
expect(config.persistInterval).toEqual(100);
|
expect(config.persistInterval).toEqual(100);
|
||||||
|
@ -67,7 +67,7 @@ describe('appConfig', () => {
|
||||||
const config = appConfig();
|
const config = appConfig();
|
||||||
expect(config.baseUrl).toEqual(baseUrl);
|
expect(config.baseUrl).toEqual(baseUrl);
|
||||||
expect(config.rendererBaseUrl).toEqual(baseUrl);
|
expect(config.rendererBaseUrl).toEqual(baseUrl);
|
||||||
expect(config.port).toEqual(port);
|
expect(config.backendPort).toEqual(port);
|
||||||
expect(config.loglevel).toEqual(loglevel);
|
expect(config.loglevel).toEqual(loglevel);
|
||||||
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
||||||
expect(config.persistInterval).toEqual(100);
|
expect(config.persistInterval).toEqual(100);
|
||||||
|
@ -92,7 +92,7 @@ describe('appConfig', () => {
|
||||||
const config = appConfig();
|
const config = appConfig();
|
||||||
expect(config.baseUrl).toEqual(baseUrl);
|
expect(config.baseUrl).toEqual(baseUrl);
|
||||||
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
||||||
expect(config.port).toEqual(3000);
|
expect(config.backendPort).toEqual(3000);
|
||||||
expect(config.loglevel).toEqual(loglevel);
|
expect(config.loglevel).toEqual(loglevel);
|
||||||
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
||||||
expect(config.persistInterval).toEqual(100);
|
expect(config.persistInterval).toEqual(100);
|
||||||
|
@ -117,7 +117,7 @@ describe('appConfig', () => {
|
||||||
const config = appConfig();
|
const config = appConfig();
|
||||||
expect(config.baseUrl).toEqual(baseUrl);
|
expect(config.baseUrl).toEqual(baseUrl);
|
||||||
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
||||||
expect(config.port).toEqual(port);
|
expect(config.backendPort).toEqual(port);
|
||||||
expect(config.loglevel).toEqual(Loglevel.WARN);
|
expect(config.loglevel).toEqual(Loglevel.WARN);
|
||||||
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
||||||
expect(config.persistInterval).toEqual(100);
|
expect(config.persistInterval).toEqual(100);
|
||||||
|
@ -142,7 +142,7 @@ describe('appConfig', () => {
|
||||||
const config = appConfig();
|
const config = appConfig();
|
||||||
expect(config.baseUrl).toEqual(baseUrl);
|
expect(config.baseUrl).toEqual(baseUrl);
|
||||||
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
||||||
expect(config.port).toEqual(port);
|
expect(config.backendPort).toEqual(port);
|
||||||
expect(config.loglevel).toEqual(Loglevel.TRACE);
|
expect(config.loglevel).toEqual(Loglevel.TRACE);
|
||||||
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
||||||
expect(config.persistInterval).toEqual(10);
|
expect(config.persistInterval).toEqual(10);
|
||||||
|
@ -168,7 +168,7 @@ describe('appConfig', () => {
|
||||||
const config = appConfig();
|
const config = appConfig();
|
||||||
expect(config.baseUrl).toEqual(baseUrl);
|
expect(config.baseUrl).toEqual(baseUrl);
|
||||||
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
||||||
expect(config.port).toEqual(port);
|
expect(config.backendPort).toEqual(port);
|
||||||
expect(config.loglevel).toEqual(Loglevel.TRACE);
|
expect(config.loglevel).toEqual(Loglevel.TRACE);
|
||||||
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
expect(config.showLogTimestamp).toEqual(showLogTimestamp);
|
||||||
expect(config.persistInterval).toEqual(0);
|
expect(config.persistInterval).toEqual(0);
|
||||||
|
@ -192,7 +192,7 @@ describe('appConfig', () => {
|
||||||
const config = appConfig();
|
const config = appConfig();
|
||||||
expect(config.baseUrl).toEqual(baseUrl);
|
expect(config.baseUrl).toEqual(baseUrl);
|
||||||
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
|
||||||
expect(config.port).toEqual(port);
|
expect(config.backendPort).toEqual(port);
|
||||||
expect(config.loglevel).toEqual(Loglevel.TRACE);
|
expect(config.loglevel).toEqual(Loglevel.TRACE);
|
||||||
expect(config.showLogTimestamp).toEqual(true);
|
expect(config.showLogTimestamp).toEqual(true);
|
||||||
expect(config.persistInterval).toEqual(0);
|
expect(config.persistInterval).toEqual(0);
|
||||||
|
@ -232,7 +232,7 @@ describe('appConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => appConfig()).toThrow(
|
expect(() => appConfig()).toThrow(
|
||||||
'"HD_BASE_URL" must not contain a subdirectory',
|
'HD_BASE_URL: baseUrl must not contain a subdirectory',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -252,7 +252,7 @@ describe('appConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => appConfig()).toThrow(
|
expect(() => appConfig()).toThrow(
|
||||||
'"HD_BACKEND_PORT" must be a positive number',
|
'HD_BACKEND_PORT: Number must be greater than 0',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -272,7 +272,7 @@ describe('appConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => appConfig()).toThrow(
|
expect(() => appConfig()).toThrow(
|
||||||
'"HD_BACKEND_PORT" must be less than or equal to 65535',
|
'HD_BACKEND_PORT: Number must be less than or equal to 65535',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -291,7 +291,9 @@ describe('appConfig', () => {
|
||||||
clear: true,
|
clear: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => appConfig()).toThrow('"HD_BACKEND_PORT" must be an integer');
|
expect(() => appConfig()).toThrow(
|
||||||
|
'HD_BACKEND_PORT: Expected integer, received float',
|
||||||
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -309,7 +311,9 @@ describe('appConfig', () => {
|
||||||
clear: true,
|
clear: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => appConfig()).toThrow('"HD_BACKEND_PORT" must be a number');
|
expect(() => appConfig()).toThrow(
|
||||||
|
'HD_BACKEND_PORT: Expected number, received nan',
|
||||||
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -9,94 +9,104 @@ import {
|
||||||
WrongProtocolError,
|
WrongProtocolError,
|
||||||
} from '@hedgedoc/commons';
|
} from '@hedgedoc/commons';
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
import * as Joi from 'joi';
|
import z, { RefinementCtx } from 'zod';
|
||||||
import { CustomHelpers, ErrorReport } from 'joi';
|
|
||||||
|
|
||||||
import { Loglevel } from './loglevel.enum';
|
import { Loglevel } from './loglevel.enum';
|
||||||
import { buildErrorMessage, parseOptionalNumber } from './utils';
|
import { parseOptionalBoolean, parseOptionalNumber } from './utils';
|
||||||
|
import {
|
||||||
|
buildErrorMessage,
|
||||||
|
extractDescriptionFromZodIssue,
|
||||||
|
} from './zod-error-message';
|
||||||
|
|
||||||
export interface AppConfig {
|
function validateUrl(value: string | undefined, ctx: RefinementCtx): void {
|
||||||
baseUrl: string;
|
if (!value) {
|
||||||
rendererBaseUrl: string;
|
return z.NEVER;
|
||||||
port: number;
|
}
|
||||||
loglevel: Loglevel;
|
|
||||||
showLogTimestamp: boolean;
|
|
||||||
persistInterval: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateUrl(
|
|
||||||
value: string,
|
|
||||||
helpers: CustomHelpers,
|
|
||||||
): string | ErrorReport {
|
|
||||||
try {
|
try {
|
||||||
return parseUrl(value).isPresent() ? value : helpers.error('string.uri');
|
if (!parseUrl(value).isPresent()) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.invalid_string,
|
||||||
|
message: "Can't parse as URL",
|
||||||
|
fatal: true,
|
||||||
|
validation: 'url',
|
||||||
|
});
|
||||||
|
return z.NEVER;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof NoSubdirectoryAllowedError) {
|
if (error instanceof NoSubdirectoryAllowedError) {
|
||||||
return helpers.error('url.noSubDirectoryAllowed');
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.invalid_string,
|
||||||
|
message: ctx.path[0] + ' must not contain a subdirectory',
|
||||||
|
fatal: true,
|
||||||
|
validation: 'url',
|
||||||
|
});
|
||||||
} else if (error instanceof WrongProtocolError) {
|
} else if (error instanceof WrongProtocolError) {
|
||||||
return helpers.error('url.wrongProtocol');
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.invalid_string,
|
||||||
|
message: ctx.path[0] + ' protocol must be HTTP or HTTPS',
|
||||||
|
fatal: true,
|
||||||
|
validation: 'url',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = Joi.object({
|
const schema = z
|
||||||
baseUrl: Joi.string().custom(validateUrl).label('HD_BASE_URL'),
|
.object({
|
||||||
rendererBaseUrl: Joi.string()
|
baseUrl: z.string().superRefine(validateUrl).describe('HD_BASE_URL'),
|
||||||
.custom(validateUrl)
|
rendererBaseUrl: z
|
||||||
.default(Joi.ref('baseUrl'))
|
.string()
|
||||||
.optional()
|
.superRefine(validateUrl)
|
||||||
.label('HD_RENDERER_BASE_URL'),
|
.default('')
|
||||||
port: Joi.number()
|
.describe('HD_RENDERER_BASE_URL'),
|
||||||
.positive()
|
backendPort: z
|
||||||
.integer()
|
.number()
|
||||||
.default(3000)
|
.positive()
|
||||||
.max(65535)
|
.int()
|
||||||
.optional()
|
.max(65535)
|
||||||
.label('HD_BACKEND_PORT'),
|
.default(3000)
|
||||||
loglevel: Joi.string()
|
.describe('HD_BACKEND_PORT'),
|
||||||
.valid(...Object.values(Loglevel))
|
loglevel: z
|
||||||
.default(Loglevel.WARN)
|
.enum(Object.values(Loglevel) as [Loglevel, ...Loglevel[]])
|
||||||
.optional()
|
.default(Loglevel.WARN)
|
||||||
.label('HD_LOGLEVEL'),
|
.describe('HD_LOGLEVEL'),
|
||||||
showLogTimestamp: Joi.boolean()
|
showLogTimestamp: z
|
||||||
.default(true)
|
.boolean()
|
||||||
.optional()
|
.default(true)
|
||||||
.label('HD_SHOW_LOG_TIMESTAMP'),
|
.describe('HD_SHOW_LOG_TIMESTAMP'),
|
||||||
persistInterval: Joi.number()
|
persistInterval: z.coerce
|
||||||
.integer()
|
.number()
|
||||||
.min(0)
|
.int()
|
||||||
.default(10)
|
.min(0)
|
||||||
.optional()
|
.default(10)
|
||||||
.label('HD_PERSIST_INTERVAL'),
|
.describe('HD_PERSIST_INTERVAL'),
|
||||||
}).messages({
|
})
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
.transform((data) => {
|
||||||
'url.noSubDirectoryAllowed': '{{#label}} must not contain a subdirectory',
|
// Handle the default reference for rendererBaseUrl
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
if (data.rendererBaseUrl === '') {
|
||||||
'url.wrongProtocol': '{{#label}} protocol must be HTTP or HTTPS',
|
data.rendererBaseUrl = data.baseUrl;
|
||||||
});
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AppConfig = z.infer<typeof schema>;
|
||||||
|
|
||||||
export default registerAs('appConfig', () => {
|
export default registerAs('appConfig', () => {
|
||||||
const appConfig = schema.validate(
|
const appConfig = schema.safeParse({
|
||||||
{
|
baseUrl: process.env.HD_BASE_URL,
|
||||||
baseUrl: process.env.HD_BASE_URL,
|
rendererBaseUrl: process.env.HD_RENDERER_BASE_URL,
|
||||||
rendererBaseUrl: process.env.HD_RENDERER_BASE_URL,
|
backendPort: parseOptionalNumber(process.env.HD_BACKEND_PORT),
|
||||||
port: parseOptionalNumber(process.env.HD_BACKEND_PORT),
|
loglevel: process.env.HD_LOGLEVEL,
|
||||||
loglevel: process.env.HD_LOGLEVEL,
|
showLogTimestamp: parseOptionalBoolean(process.env.HD_SHOW_LOG_TIMESTAMP),
|
||||||
showLogTimestamp: process.env.HD_SHOW_LOG_TIMESTAMP,
|
persistInterval: process.env.HD_PERSIST_INTERVAL,
|
||||||
persistInterval: process.env.HD_PERSIST_INTERVAL,
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
abortEarly: false,
|
|
||||||
presence: 'required',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (appConfig.error) {
|
if (appConfig.error) {
|
||||||
const errorMessages = appConfig.error.details.map(
|
const errorMessages = appConfig.error.errors.map((issue) =>
|
||||||
(detail) => detail.message,
|
extractDescriptionFromZodIssue(issue, 'HD'),
|
||||||
);
|
);
|
||||||
throw new Error(buildErrorMessage(errorMessages));
|
throw new Error(buildErrorMessage(errorMessages));
|
||||||
}
|
}
|
||||||
return appConfig.value as AppConfig;
|
return appConfig.data;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -129,7 +129,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow(
|
expect(() => authConfig()).toThrow(
|
||||||
'"HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH" must be less than or equal to 4',
|
'HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH: Number must be less than or equal to 4',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -147,7 +147,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow(
|
expect(() => authConfig()).toThrow(
|
||||||
'"HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH" must be greater than or equal to 0',
|
'HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH: Number must be greater than or equal to 0',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -200,6 +200,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -232,6 +233,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -263,6 +265,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -294,6 +297,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -325,6 +329,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -356,6 +361,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -387,6 +393,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -418,6 +425,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -449,6 +457,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.ldap).toBeDefined();
|
||||||
expect(config.ldap).toHaveLength(1);
|
expect(config.ldap).toHaveLength(1);
|
||||||
const firstLdap = config.ldap[0];
|
const firstLdap = config.ldap[0];
|
||||||
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
expect(firstLdap.identifier).toEqual(ldapNames[0]);
|
||||||
|
@ -481,7 +490,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow(
|
expect(() => authConfig()).toThrow(
|
||||||
'"HD_AUTH_LDAP_FUTURAMA_URL" is required',
|
'HD_AUTH_LDAP_FUTURAMA_URL: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -499,7 +508,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow(
|
expect(() => authConfig()).toThrow(
|
||||||
'"HD_AUTH_LDAP_FUTURAMA_SEARCH_BASE" is required',
|
'HD_AUTH_LDAP_FUTURAMA_SEARCH_BASE: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -517,7 +526,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow(
|
expect(() => authConfig()).toThrow(
|
||||||
'"HD_AUTH_LDAP_FUTURAMA_TLS_CERT_PATHS[0]" must not be a sparse array item',
|
'HD_AUTH_LDAP_FUTURAMA_TLS_CA_CERTS[0]: File not found',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -582,11 +591,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -595,7 +605,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.scope).toEqual(scope);
|
expect(firstOidc.scope).toEqual(scope);
|
||||||
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -616,11 +626,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toBeUndefined();
|
expect(firstOidc.theme).toBeUndefined();
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -629,7 +640,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.scope).toEqual(scope);
|
expect(firstOidc.scope).toEqual(scope);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -650,11 +661,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toBeUndefined();
|
expect(firstOidc.authorizeUrl).toBeUndefined();
|
||||||
|
@ -663,7 +675,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.scope).toEqual(scope);
|
expect(firstOidc.scope).toEqual(scope);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -684,11 +696,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -697,7 +710,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.scope).toEqual(scope);
|
expect(firstOidc.scope).toEqual(scope);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -718,11 +731,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -731,7 +745,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.scope).toEqual(scope);
|
expect(firstOidc.scope).toEqual(scope);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -752,11 +766,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -765,7 +780,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.endSessionUrl).toBeUndefined();
|
expect(firstOidc.endSessionUrl).toBeUndefined();
|
||||||
expect(firstOidc.scope).toEqual(scope);
|
expect(firstOidc.scope).toEqual(scope);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -786,11 +801,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -799,7 +815,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.scope).toEqual(defaultScope);
|
expect(firstOidc.scope).toEqual(defaultScope);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -820,11 +836,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -833,7 +850,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.userIdField).toEqual(defaultUserIdField);
|
expect(firstOidc.userIdField).toEqual(defaultUserIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -854,11 +871,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -867,7 +885,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(defaultDisplayNameField);
|
expect(firstOidc.displayNameField).toEqual(defaultDisplayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -888,11 +906,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -901,7 +920,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(
|
expect(firstOidc.profilePictureField).toEqual(
|
||||||
defaultProfilePictureField,
|
defaultProfilePictureField,
|
||||||
|
@ -924,11 +943,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -937,7 +957,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(defaultEmailField);
|
expect(firstOidc.emailField).toEqual(defaultEmailField);
|
||||||
|
@ -958,11 +978,12 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = authConfig();
|
const config = authConfig();
|
||||||
|
expect(config.oidc).toBeDefined();
|
||||||
expect(config.oidc).toHaveLength(1);
|
expect(config.oidc).toHaveLength(1);
|
||||||
const firstOidc = config.oidc[0];
|
const firstOidc = config.oidc[0];
|
||||||
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
expect(firstOidc.identifier).toEqual(oidcNames[0]);
|
||||||
expect(firstOidc.issuer).toEqual(issuer);
|
expect(firstOidc.issuer).toEqual(issuer);
|
||||||
expect(firstOidc.clientID).toEqual(clientId);
|
expect(firstOidc.clientId).toEqual(clientId);
|
||||||
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
expect(firstOidc.clientSecret).toEqual(clientSecret);
|
||||||
expect(firstOidc.theme).toEqual(theme);
|
expect(firstOidc.theme).toEqual(theme);
|
||||||
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
expect(firstOidc.authorizeUrl).toEqual(authorizeUrl);
|
||||||
|
@ -971,7 +992,7 @@ describe('authConfig', () => {
|
||||||
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
expect(firstOidc.userinfoUrl).toEqual(userinfoUrl);
|
||||||
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
expect(firstOidc.endSessionUrl).toEqual(endSessionUrl);
|
||||||
expect(firstOidc.userIdField).toEqual(userIdField);
|
expect(firstOidc.userIdField).toEqual(userIdField);
|
||||||
expect(firstOidc.userNameField).toEqual(userNameField);
|
expect(firstOidc.usernameField).toEqual(userNameField);
|
||||||
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
expect(firstOidc.displayNameField).toEqual(displayNameField);
|
||||||
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
expect(firstOidc.profilePictureField).toEqual(profilePictureField);
|
||||||
expect(firstOidc.emailField).toEqual(emailField);
|
expect(firstOidc.emailField).toEqual(emailField);
|
||||||
|
@ -994,7 +1015,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow(
|
expect(() => authConfig()).toThrow(
|
||||||
'"HD_AUTH_OIDC_GITLAB_ISSUER" is required',
|
'HD_AUTH_OIDC_GITLAB_ISSUER: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -1012,7 +1033,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow(
|
expect(() => authConfig()).toThrow(
|
||||||
'"HD_AUTH_OIDC_GITLAB_CLIENT_ID" is required',
|
'HD_AUTH_OIDC_GITLAB_CLIENT_ID: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -1030,7 +1051,7 @@ describe('authConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow(
|
expect(() => authConfig()).toThrow(
|
||||||
'"HD_AUTH_OIDC_GITLAB_CLIENT_SECRET" is required',
|
'HD_AUTH_OIDC_GITLAB_CLIENT_SECRET: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -1047,7 +1068,9 @@ describe('authConfig', () => {
|
||||||
clear: true,
|
clear: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => authConfig()).toThrow('"HD_AUTH_OIDC_GITLAB_THEME"');
|
expect(() => authConfig()).toThrow(
|
||||||
|
"HD_AUTH_OIDC_GITLAB_THEME: Invalid enum value. Expected 'google' | 'github' | 'gitlab' | 'facebook' | 'discord' | 'mastodon' | 'azure', received 'something else'",
|
||||||
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,174 +1,220 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
import * as fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as Joi from 'joi';
|
import z from 'zod';
|
||||||
|
|
||||||
import { Theme } from './theme.enum';
|
import { Theme } from './theme.enum';
|
||||||
import {
|
import {
|
||||||
buildErrorMessage,
|
|
||||||
ensureNoDuplicatesExist,
|
ensureNoDuplicatesExist,
|
||||||
parseOptionalBoolean,
|
parseOptionalBoolean,
|
||||||
parseOptionalNumber,
|
parseOptionalNumber,
|
||||||
replaceAuthErrorsWithEnvironmentVariables,
|
|
||||||
toArrayConfig,
|
toArrayConfig,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import {
|
||||||
|
buildErrorMessage,
|
||||||
|
extractDescriptionFromZodIssue,
|
||||||
|
} from './zod-error-message';
|
||||||
|
|
||||||
export interface InternalIdentifier {
|
const ldapSchema = z
|
||||||
identifier: string;
|
.object({
|
||||||
providerName: string;
|
identifier: z.string().describe('HD_AUTH_LDAP_SERVERS'),
|
||||||
}
|
providerName: z
|
||||||
|
.string()
|
||||||
export interface LDAPConfig extends InternalIdentifier {
|
.default('LDAP')
|
||||||
url: string;
|
.describe('HD_AUTH_LDAP_*_PROVIDER_NAME'),
|
||||||
bindDn?: string;
|
url: z.string().describe('HD_AUTH_LDAP_*_URL'),
|
||||||
bindCredentials?: string;
|
bindDn: z.string().optional().describe('HD_AUTH_LDAP_*_BIND_DN'),
|
||||||
searchBase: string;
|
bindCredentials: z
|
||||||
searchFilter: string;
|
.string()
|
||||||
searchAttributes: string[];
|
.optional()
|
||||||
userIdField: string;
|
.describe('HD_AUTH_LDAP_*_BIND_CREDENTIALS'),
|
||||||
displayNameField: string;
|
searchBase: z.string().describe('HD_AUTH_LDAP_*_SEARCH_BASE'),
|
||||||
emailField: string;
|
searchFilter: z
|
||||||
profilePictureField: string;
|
.string()
|
||||||
tlsCaCerts?: string[];
|
.default('(uid={{username}})')
|
||||||
}
|
.describe('HD_AUTH_LDAP_*_SEARCH_FILTER'),
|
||||||
|
searchAttributes: z
|
||||||
export interface OidcConfig extends InternalIdentifier {
|
.array(z.string())
|
||||||
issuer: string;
|
.optional()
|
||||||
clientID: string;
|
.describe('HD_AUTH_LDAP_*_SEARCH_ATTRIBUTES'),
|
||||||
clientSecret: string;
|
userIdField: z
|
||||||
theme?: string;
|
.string()
|
||||||
authorizeUrl?: string;
|
.default('uid')
|
||||||
tokenUrl?: string;
|
.describe('HD_AUTH_LDAP_*_USER_ID_FIELD'),
|
||||||
userinfoUrl?: string;
|
displayNameField: z
|
||||||
endSessionUrl?: string;
|
.string()
|
||||||
scope: string;
|
.default('displayName')
|
||||||
userNameField: string;
|
.describe('HD_AUTH_LDAP_*_DISPLAY_NAME_FIELD'),
|
||||||
userIdField: string;
|
emailField: z
|
||||||
displayNameField: string;
|
.string()
|
||||||
profilePictureField: string;
|
.default('mail')
|
||||||
emailField: string;
|
.describe('HD_AUTH_LDAP_*_EMAIL_FIELD'),
|
||||||
enableRegistration?: boolean;
|
profilePictureField: z
|
||||||
}
|
.string()
|
||||||
|
.default('jpegPhoto')
|
||||||
export interface AuthConfig {
|
.describe('HD_AUTH_LDAP_*_PROFILE_PICTURE_FIELD'),
|
||||||
common: {
|
tlsCaCerts: z
|
||||||
allowProfileEdits: boolean;
|
.array(
|
||||||
allowChooseUsername: boolean;
|
z.string({
|
||||||
syncSource?: string;
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
};
|
required_error: 'File not found',
|
||||||
session: {
|
}),
|
||||||
secret: string;
|
)
|
||||||
lifetime: number;
|
.optional()
|
||||||
};
|
.describe('HD_AUTH_LDAP_*_TLS_CA_CERTS'),
|
||||||
local: {
|
tlsRejectUnauthorized: z
|
||||||
enableLogin: boolean;
|
.boolean()
|
||||||
enableRegister: boolean;
|
|
||||||
minimalPasswordStrength: number;
|
|
||||||
};
|
|
||||||
// ToDo: tlsOptions exist in config.json.example. See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
|
|
||||||
ldap: LDAPConfig[];
|
|
||||||
oidc: OidcConfig[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const authSchema = Joi.object({
|
|
||||||
common: {
|
|
||||||
allowProfileEdits: Joi.boolean()
|
|
||||||
.default(true)
|
.default(true)
|
||||||
|
.describe('HD_AUTH_LDAP_*_TLS_REJECT_UNAUTHORIZED'),
|
||||||
|
tlsSniName: z.string().optional().describe('HD_AUTH_LDAP_*_TLS_SNI_NAME'),
|
||||||
|
tlsAllowPartialTrustChain: z
|
||||||
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.label('HD_AUTH_ALLOW_PROFILE_EDITS'),
|
.describe('HD_AUTH_LDAP_*_TLS_ALLOW_PARTIAL_TRUST_CHAIN'),
|
||||||
allowChooseUsername: Joi.boolean()
|
tlsMinVersion: z
|
||||||
.default(true)
|
.enum(['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'])
|
||||||
.optional()
|
.optional()
|
||||||
.label('HD_AUTH_ALLOW_CHOOSE_USERNAME'),
|
.describe('HD_AUTH_LDAP_*_TLS_MIN_VERSION'),
|
||||||
syncSource: Joi.string().optional().label('HD_AUTH_SYNC_SOURCE'),
|
tlsMaxVersion: z
|
||||||
},
|
.enum(['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'])
|
||||||
session: {
|
|
||||||
secret: Joi.string().label('HD_SESSION_SECRET'),
|
|
||||||
lifetime: Joi.number()
|
|
||||||
.default(1209600) // 14 * 24 * 60 * 60s = 14 days
|
|
||||||
.optional()
|
.optional()
|
||||||
.label('HD_SESSION_LIFETIME'),
|
.describe('HD_AUTH_LDAP_*_TLS_MAX_VERSION'),
|
||||||
},
|
})
|
||||||
local: {
|
.superRefine((config, ctx) => {
|
||||||
enableLogin: Joi.boolean()
|
const tlsMin = config.tlsMinVersion?.replace('TLSv', '');
|
||||||
.default(false)
|
const tlsMax = config.tlsMaxVersion?.replace('TLSv', '');
|
||||||
.optional()
|
if (tlsMin && tlsMax && tlsMin > tlsMax) {
|
||||||
.label('HD_AUTH_LOCAL_ENABLE_LOGIN'),
|
ctx.addIssue({
|
||||||
enableRegister: Joi.boolean()
|
code: z.ZodIssueCode.custom,
|
||||||
.default(false)
|
message:
|
||||||
.optional()
|
'TLS min version must be less than or equal to TLS max version',
|
||||||
.label('HD_AUTH_LOCAL_ENABLE_REGISTER'),
|
fatal: true,
|
||||||
minimalPasswordStrength: Joi.number()
|
});
|
||||||
.default(2)
|
}
|
||||||
.min(0)
|
if ((tlsMin && tlsMin < '1.2') || (tlsMax && tlsMax < '1.2')) {
|
||||||
.max(4)
|
ctx.addIssue({
|
||||||
.optional()
|
code: z.ZodIssueCode.custom,
|
||||||
.label('HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH'),
|
message:
|
||||||
},
|
'For security reasons, consider using TLS version 1.2 or higher',
|
||||||
ldap: Joi.array()
|
fatal: false,
|
||||||
.items(
|
});
|
||||||
Joi.object({
|
}
|
||||||
identifier: Joi.string(),
|
});
|
||||||
providerName: Joi.string().default('LDAP').optional(),
|
|
||||||
url: Joi.string(),
|
const oidcSchema = z.object({
|
||||||
bindDn: Joi.string().optional(),
|
identifier: z.string().describe('HD_AUTH_OIDC_SERVERS'),
|
||||||
bindCredentials: Joi.string().optional(),
|
providerName: z
|
||||||
searchBase: Joi.string(),
|
.string()
|
||||||
searchFilter: Joi.string().default('(uid={{username}})').optional(),
|
.default('OpenID Connect')
|
||||||
searchAttributes: Joi.array().items(Joi.string()).optional(),
|
.describe('HD_AUTH_OIDC_*_PROVIDER_NAME'),
|
||||||
userIdField: Joi.string().default('uid').optional(),
|
issuer: z.string().url().describe('HD_AUTH_OIDC_*_ISSUER'),
|
||||||
displayNameField: Joi.string().default('displayName').optional(),
|
clientId: z.string().describe('HD_AUTH_OIDC_*_CLIENT_ID'),
|
||||||
emailField: Joi.string().default('mail').optional(),
|
clientSecret: z.string().describe('HD_AUTH_OIDC_*_CLIENT_SECRET'),
|
||||||
profilePictureField: Joi.string().default('jpegPhoto').optional(),
|
theme: z.nativeEnum(Theme).optional().describe('HD_AUTH_OIDC_*_THEME'),
|
||||||
tlsCaCerts: Joi.array().items(Joi.string()).optional(),
|
authorizeUrl: z
|
||||||
}).optional(),
|
.string()
|
||||||
)
|
.url()
|
||||||
.optional(),
|
.optional()
|
||||||
oidc: Joi.array()
|
.describe('HD_AUTH_OIDC_*_AUTHORIZE_URL'),
|
||||||
.items(
|
tokenUrl: z.string().url().optional().describe('HD_AUTH_OIDC_*_TOKEN_URL'),
|
||||||
Joi.object({
|
userinfoUrl: z
|
||||||
identifier: Joi.string(),
|
.string()
|
||||||
providerName: Joi.string().default('OpenID Connect').optional(),
|
.url()
|
||||||
issuer: Joi.string(),
|
.optional()
|
||||||
clientID: Joi.string(),
|
.describe('HD_AUTH_OIDC_*_USERINFO_URL'),
|
||||||
clientSecret: Joi.string(),
|
endSessionUrl: z
|
||||||
theme: Joi.string()
|
.string()
|
||||||
.valid(...Object.values(Theme))
|
.url()
|
||||||
.optional(),
|
.optional()
|
||||||
authorizeUrl: Joi.string().optional(),
|
.describe('HD_AUTH_OIDC_*_END_SESSION_URL'),
|
||||||
tokenUrl: Joi.string().optional(),
|
scope: z
|
||||||
userinfoUrl: Joi.string().optional(),
|
.string()
|
||||||
endSessionUrl: Joi.string().optional(),
|
.default('openid profile email')
|
||||||
scope: Joi.string().default('openid profile email').optional(),
|
.describe('HD_AUTH_OIDC_*_SCOPE'),
|
||||||
userIdField: Joi.string().default('sub').optional(),
|
usernameField: z
|
||||||
userNameField: Joi.string().default('preferred_username').optional(),
|
.string()
|
||||||
displayNameField: Joi.string().default('name').optional(),
|
.default('preferred_username')
|
||||||
profilePictureField: Joi.string().default('picture').optional(),
|
.describe('HD_AUTH_OIDC_*_USERNAME_FIELD'),
|
||||||
emailField: Joi.string().default('email').optional(),
|
userIdField: z
|
||||||
enableRegistration: Joi.boolean().default(true).optional(),
|
.string()
|
||||||
}).optional(),
|
.default('sub')
|
||||||
)
|
.describe('HD_AUTH_OIDC_*_USER_ID_FIELD'),
|
||||||
.optional(),
|
displayNameField: z
|
||||||
|
.string()
|
||||||
|
.default('name')
|
||||||
|
.describe('HD_AUTH_OIDC_*_DISPLAY_NAME_FIELD'),
|
||||||
|
profilePictureField: z
|
||||||
|
.string()
|
||||||
|
.default('picture')
|
||||||
|
.describe('HD_AUTH_OIDC_*_PROFILE_PICTURE_FIELD'),
|
||||||
|
emailField: z
|
||||||
|
.string()
|
||||||
|
.default('email')
|
||||||
|
.describe('HD_AUTH_OIDC_*_EMAIL_FIELD'),
|
||||||
|
enableRegistration: z
|
||||||
|
.boolean()
|
||||||
|
.default(true)
|
||||||
|
.describe('HD_AUTH_OIDC_*_ENABLE_REGISTRATION'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
common: z.object({
|
||||||
|
allowProfileEdits: z
|
||||||
|
.boolean()
|
||||||
|
.default(true)
|
||||||
|
.describe('HD_AUTH_ALLOW_PROFILE_EDITS'),
|
||||||
|
allowChooseUsername: z
|
||||||
|
.boolean()
|
||||||
|
.default(true)
|
||||||
|
.describe('HD_AUTH_ALLOW_CHOOSE_USERNAME'),
|
||||||
|
syncSource: z.string().optional().describe('HD_AUTH_SYNC_SOURCE'),
|
||||||
|
}),
|
||||||
|
session: z.object({
|
||||||
|
secret: z.string().describe('HD_SESSION_SECRET'),
|
||||||
|
lifetime: z.number().default(1209600).describe('HD_SESSION_LIFETIME'), // 14 * 24 * 60 * 60s = 14 days
|
||||||
|
}),
|
||||||
|
local: z.object({
|
||||||
|
enableLogin: z
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.describe('HD_AUTH_LOCAL_ENABLE_LOGIN'),
|
||||||
|
enableRegister: z
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.describe('HD_AUTH_LOCAL_ENABLE_REGISTER'),
|
||||||
|
minimalPasswordStrength: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0)
|
||||||
|
.max(4)
|
||||||
|
.default(2)
|
||||||
|
.describe('HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH'),
|
||||||
|
}),
|
||||||
|
ldap: z.array(ldapSchema).describe('HD_AUTH_LDAP_*'),
|
||||||
|
oidc: z.array(oidcSchema).describe('HD_AUTH_OIDC_*'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AuthConfig = z.infer<typeof schema>;
|
||||||
|
export type LdapConfig = z.infer<typeof ldapSchema>;
|
||||||
|
export type OidcConfig = z.infer<typeof oidcSchema>;
|
||||||
|
|
||||||
export default registerAs('authConfig', () => {
|
export default registerAs('authConfig', () => {
|
||||||
const ldapNames = (
|
const ldapServers = (process.env.HD_AUTH_LDAP_SERVERS?.split(',') ?? []).map(
|
||||||
toArrayConfig(process.env.HD_AUTH_LDAP_SERVERS, ',') ?? []
|
(name) => name.toUpperCase(),
|
||||||
).map((name) => name.toUpperCase());
|
);
|
||||||
ensureNoDuplicatesExist('LDAP', ldapNames);
|
ensureNoDuplicatesExist('LDAP', ldapServers);
|
||||||
|
|
||||||
const oidcNames = (
|
const oidcServers = (process.env.HD_AUTH_OIDC_SERVERS?.split(',') ?? []).map(
|
||||||
toArrayConfig(process.env.HD_AUTH_OIDC_SERVERS, ',') ?? []
|
(name) => name.toUpperCase(),
|
||||||
).map((name) => name.toUpperCase());
|
);
|
||||||
ensureNoDuplicatesExist('OIDC', oidcNames);
|
ensureNoDuplicatesExist('OIDC', oidcServers);
|
||||||
|
|
||||||
const ldapInstances = ldapNames.map((ldapName) => {
|
const ldapConfig: Partial<LdapConfig>[] = ldapServers.map((name) => {
|
||||||
const caFiles = toArrayConfig(
|
const caFiles = toArrayConfig(
|
||||||
process.env[`HD_AUTH_LDAP_${ldapName}_TLS_CERT_PATHS`],
|
process.env[`HD_AUTH_LDAP_${name}_TLS_CERT_PATHS`],
|
||||||
',',
|
',',
|
||||||
);
|
);
|
||||||
let tlsCaCerts = undefined;
|
let tlsCaCerts = undefined;
|
||||||
|
@ -180,106 +226,97 @@ export default registerAs('authConfig', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
identifier: ldapName.toLowerCase(),
|
identifier: name.toLowerCase(),
|
||||||
providerName: process.env[`HD_AUTH_LDAP_${ldapName}_PROVIDER_NAME`],
|
providerName: process.env[`HD_AUTH_LDAP_${name}_PROVIDER_NAME`],
|
||||||
url: process.env[`HD_AUTH_LDAP_${ldapName}_URL`],
|
url: process.env[`HD_AUTH_LDAP_${name}_URL`],
|
||||||
bindDn: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_DN`],
|
bindDn: process.env[`HD_AUTH_LDAP_${name}_BIND_DN`],
|
||||||
bindCredentials: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_CREDENTIALS`],
|
bindCredentials: process.env[`HD_AUTH_LDAP_${name}_BIND_CREDENTIALS`],
|
||||||
searchBase: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_BASE`],
|
searchBase: process.env[`HD_AUTH_LDAP_${name}_SEARCH_BASE`],
|
||||||
searchFilter: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_FILTER`],
|
searchFilter: process.env[`HD_AUTH_LDAP_${name}_SEARCH_FILTER`],
|
||||||
searchAttributes: toArrayConfig(
|
searchAttributes:
|
||||||
process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_ATTRIBUTES`],
|
process.env[`HD_AUTH_LDAP_${name}_SEARCH_ATTRIBUTES`]?.split(','),
|
||||||
',',
|
userIdField: process.env[`HD_AUTH_LDAP_${name}_USER_ID_FIELD`],
|
||||||
),
|
displayNameField: process.env[`HD_AUTH_LDAP_${name}_DISPLAY_NAME_FIELD`],
|
||||||
userIdField: process.env[`HD_AUTH_LDAP_${ldapName}_USER_ID_FIELD`],
|
emailField: process.env[`HD_AUTH_LDAP_${name}_EMAIL_FIELD`],
|
||||||
displayNameField:
|
|
||||||
process.env[`HD_AUTH_LDAP_${ldapName}_DISPLAY_NAME_FIELD`],
|
|
||||||
emailField: process.env[`HD_AUTH_LDAP_${ldapName}_EMAIL_FIELD`],
|
|
||||||
profilePictureField:
|
profilePictureField:
|
||||||
process.env[`HD_AUTH_LDAP_${ldapName}_PROFILE_PICTURE_FIELD`],
|
process.env[`HD_AUTH_LDAP_${name}_PROFILE_PICTURE_FIELD`],
|
||||||
tlsCaCerts: tlsCaCerts,
|
// Technically this can be (string | undefined)[] | undefined, but an undefined array element tells us that the file is not there and the user input is invalid
|
||||||
|
tlsCaCerts: tlsCaCerts as string[] | undefined,
|
||||||
|
tlsRejectUnauthorized: parseOptionalBoolean(
|
||||||
|
process.env[`HD_AUTH_LDAP_${name}_TLS_REJECT_UNAUTHORIZED`],
|
||||||
|
),
|
||||||
|
tlsSniName: process.env[`HD_AUTH_LDAP_${name}_TLS_SNI_NAME`],
|
||||||
|
tlsAllowPartialTrustChain: parseOptionalBoolean(
|
||||||
|
process.env[`HD_AUTH_LDAP_${name}_TLS_ALLOW_PARTIAL_TRUST_CHAIN`],
|
||||||
|
),
|
||||||
|
tlsMinVersion: process.env[`HD_AUTH_LDAP_${name}_TLS_MIN_VERSION`] as
|
||||||
|
| 'TLSv1' // This typecast is required since zod validates the input later but TypeScript already expects valid input
|
||||||
|
| undefined,
|
||||||
|
tlsMaxVersion: process.env[`HD_AUTH_LDAP_${name}_TLS_MAX_VERSION`] as
|
||||||
|
| 'TLSv1'
|
||||||
|
| undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const oidcInstances = oidcNames.map((oidcName) => ({
|
const oidcConfig: Partial<OidcConfig>[] = oidcServers.map((name) => ({
|
||||||
identifier: oidcName.toLowerCase(),
|
identifier: name.toLowerCase(),
|
||||||
providerName: process.env[`HD_AUTH_OIDC_${oidcName}_PROVIDER_NAME`],
|
providerName: process.env[`HD_AUTH_OIDC_${name}_PROVIDER_NAME`],
|
||||||
issuer: process.env[`HD_AUTH_OIDC_${oidcName}_ISSUER`],
|
issuer: process.env[`HD_AUTH_OIDC_${name}_ISSUER`],
|
||||||
clientID: process.env[`HD_AUTH_OIDC_${oidcName}_CLIENT_ID`],
|
clientId: process.env[`HD_AUTH_OIDC_${name}_CLIENT_ID`],
|
||||||
clientSecret: process.env[`HD_AUTH_OIDC_${oidcName}_CLIENT_SECRET`],
|
clientSecret: process.env[`HD_AUTH_OIDC_${name}_CLIENT_SECRET`],
|
||||||
theme: process.env[`HD_AUTH_OIDC_${oidcName}_THEME`],
|
theme: process.env[`HD_AUTH_OIDC_${name}_THEME`] as Theme | undefined,
|
||||||
authorizeUrl: process.env[`HD_AUTH_OIDC_${oidcName}_AUTHORIZE_URL`],
|
authorizeUrl: process.env[`HD_AUTH_OIDC_${name}_AUTHORIZE_URL`],
|
||||||
tokenUrl: process.env[`HD_AUTH_OIDC_${oidcName}_TOKEN_URL`],
|
tokenUrl: process.env[`HD_AUTH_OIDC_${name}_TOKEN_URL`],
|
||||||
userinfoUrl: process.env[`HD_AUTH_OIDC_${oidcName}_USERINFO_URL`],
|
userinfoUrl: process.env[`HD_AUTH_OIDC_${name}_USERINFO_URL`],
|
||||||
endSessionUrl: process.env[`HD_AUTH_OIDC_${oidcName}_END_SESSION_URL`],
|
endSessionUrl: process.env[`HD_AUTH_OIDC_${name}_END_SESSION_URL`],
|
||||||
scope: process.env[`HD_AUTH_OIDC_${oidcName}_SCOPE`],
|
scope: process.env[`HD_AUTH_OIDC_${name}_SCOPE`],
|
||||||
userIdField: process.env[`HD_AUTH_OIDC_${oidcName}_USER_ID_FIELD`],
|
userIdField: process.env[`HD_AUTH_OIDC_${name}_USER_ID_FIELD`],
|
||||||
userNameField: process.env[`HD_AUTH_OIDC_${oidcName}_USER_NAME_FIELD`],
|
userNameField: process.env[`HD_AUTH_OIDC_${name}_USER_NAME_FIELD`],
|
||||||
displayNameField:
|
displayNameField: process.env[`HD_AUTH_OIDC_${name}_DISPLAY_NAME_FIELD`],
|
||||||
process.env[`HD_AUTH_OIDC_${oidcName}_DISPLAY_NAME_FIELD`],
|
|
||||||
profilePictureField:
|
profilePictureField:
|
||||||
process.env[`HD_AUTH_OIDC_${oidcName}_PROFILE_PICTURE_FIELD`],
|
process.env[`HD_AUTH_OIDC_${name}_PROFILE_PICTURE_FIELD`],
|
||||||
emailField: process.env[`HD_AUTH_OIDC_${oidcName}_EMAIL_FIELD`],
|
emailField: process.env[`HD_AUTH_OIDC_${name}_EMAIL_FIELD`],
|
||||||
enableRegistration: parseOptionalBoolean(
|
enableRegistration: parseOptionalBoolean(
|
||||||
process.env[`HD_AUTH_OIDC_${oidcName}_ENABLE_REGISTER`],
|
process.env[`HD_AUTH_OIDC_${name}_ENABLE_REGISTER`],
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let syncSource = process.env.HD_AUTH_SYNC_SOURCE;
|
const authConfig = schema.safeParse({
|
||||||
if (syncSource !== undefined) {
|
common: {
|
||||||
syncSource = syncSource.toLowerCase();
|
allowProfileEdits: parseOptionalBoolean(
|
||||||
}
|
process.env.HD_AUTH_ALLOW_PROFILE_EDITS,
|
||||||
|
),
|
||||||
|
allowChooseUsername: parseOptionalBoolean(
|
||||||
|
process.env.HD_AUTH_ALLOW_CHOOSE_USERNAME,
|
||||||
|
),
|
||||||
|
syncSource: process.env.HD_AUTH_SYNC_SOURCE?.toLowerCase(),
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
secret: process.env.HD_SESSION_SECRET,
|
||||||
|
lifetime: parseOptionalNumber(process.env.HD_SESSION_LIFETIME),
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
enableLogin: parseOptionalBoolean(process.env.HD_AUTH_LOCAL_ENABLE_LOGIN),
|
||||||
|
enableRegister: parseOptionalBoolean(
|
||||||
|
process.env.HD_AUTH_LOCAL_ENABLE_REGISTER,
|
||||||
|
),
|
||||||
|
minimalPasswordStrength: parseOptionalNumber(
|
||||||
|
process.env.HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ldap: ldapConfig,
|
||||||
|
oidc: oidcConfig,
|
||||||
|
});
|
||||||
|
|
||||||
const authConfig = authSchema.validate(
|
|
||||||
{
|
|
||||||
common: {
|
|
||||||
allowProfileEdits: process.env.HD_AUTH_ALLOW_PROFILE_EDITS,
|
|
||||||
allowChooseUsername: process.env.HD_AUTH_ALLOW_CHOOSE_USERNAME,
|
|
||||||
syncSource: syncSource,
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
secret: process.env.HD_SESSION_SECRET,
|
|
||||||
lifetime: parseOptionalNumber(process.env.HD_SESSION_LIFETIME),
|
|
||||||
},
|
|
||||||
local: {
|
|
||||||
enableLogin: parseOptionalBoolean(
|
|
||||||
process.env.HD_AUTH_LOCAL_ENABLE_LOGIN,
|
|
||||||
),
|
|
||||||
enableRegister: parseOptionalBoolean(
|
|
||||||
process.env.HD_AUTH_LOCAL_ENABLE_REGISTER,
|
|
||||||
),
|
|
||||||
minimalPasswordStrength: parseOptionalNumber(
|
|
||||||
process.env.HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
ldap: ldapInstances,
|
|
||||||
oidc: oidcInstances,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
abortEarly: false,
|
|
||||||
presence: 'required',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (authConfig.error) {
|
if (authConfig.error) {
|
||||||
const errorMessages = authConfig.error.details
|
const errorMessages = authConfig.error.errors.map((issue) =>
|
||||||
.map((detail) => detail.message)
|
extractDescriptionFromZodIssue(issue, 'HD_AUTH', {
|
||||||
.map((error) =>
|
ldap: ldapServers,
|
||||||
replaceAuthErrorsWithEnvironmentVariables(
|
oidc: oidcServers,
|
||||||
error,
|
}),
|
||||||
'ldap',
|
);
|
||||||
'HD_AUTH_LDAP_',
|
|
||||||
ldapNames,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((error) =>
|
|
||||||
replaceAuthErrorsWithEnvironmentVariables(
|
|
||||||
error,
|
|
||||||
'oidc',
|
|
||||||
'HD_AUTH_OIDC_',
|
|
||||||
oidcNames,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
throw new Error(buildErrorMessage(errorMessages));
|
throw new Error(buildErrorMessage(errorMessages));
|
||||||
}
|
}
|
||||||
return authConfig.value as AuthConfig;
|
|
||||||
|
return authConfig.data;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
import * as Joi from 'joi';
|
import * as process from 'node:process';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
import { buildErrorMessage } from './utils';
|
import { parseOptionalBoolean } from './utils';
|
||||||
|
import {
|
||||||
|
buildErrorMessage,
|
||||||
|
extractDescriptionFromZodIssue,
|
||||||
|
} from './zod-error-message';
|
||||||
|
|
||||||
export interface CspConfig {
|
const cspSchema = z.object({
|
||||||
enable: boolean;
|
enable: z.boolean().default(true).describe('HD_CSP_ENABLED'),
|
||||||
reportURI: string;
|
reportURI: z.string().optional().describe('HD_CSP_REPORT_URI'),
|
||||||
}
|
|
||||||
|
|
||||||
const cspSchema = Joi.object({
|
|
||||||
enable: Joi.boolean().default(true).optional().label('HD_CSP_ENABLE'),
|
|
||||||
reportURI: Joi.string().optional().label('HD_CSP_REPORT_URI'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type CspConfig = z.infer<typeof cspSchema>;
|
||||||
|
|
||||||
export default registerAs('cspConfig', () => {
|
export default registerAs('cspConfig', () => {
|
||||||
if (
|
if (
|
||||||
process.env.HD_CSP_ENABLE !== undefined ||
|
process.env.HD_CSP_ENABLE !== undefined ||
|
||||||
|
@ -28,21 +30,15 @@ export default registerAs('cspConfig', () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cspConfig = cspSchema.validate(
|
const cspConfig = cspSchema.safeParse({
|
||||||
{
|
enable: parseOptionalBoolean(process.env.HD_CSP_ENABLED),
|
||||||
enable: process.env.HD_CSP_ENABLE || true,
|
reportURI: process.env.HD_CSP_REPORT_URI,
|
||||||
reportURI: process.env.HD_CSP_REPORT_URI,
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
abortEarly: false,
|
|
||||||
presence: 'required',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (cspConfig.error) {
|
if (cspConfig.error) {
|
||||||
const errorMessages = cspConfig.error.details.map(
|
const errorMessages = cspConfig.error.errors.map((issue) =>
|
||||||
(detail) => detail.message,
|
extractDescriptionFromZodIssue(issue, 'HD_CSP'),
|
||||||
);
|
);
|
||||||
throw new Error(buildErrorMessage(errorMessages));
|
throw new Error(buildErrorMessage(errorMessages));
|
||||||
}
|
}
|
||||||
return cspConfig.value as CspConfig;
|
return cspConfig.data;
|
||||||
});
|
});
|
||||||
|
|
68
backend/src/config/customization.config.spec.ts
Normal file
68
backend/src/config/customization.config.spec.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import mockedEnv from 'mocked-env';
|
||||||
|
|
||||||
|
import customizationConfig from './customization.config';
|
||||||
|
|
||||||
|
describe('customizationConfig', () => {
|
||||||
|
const customName = 'test name';
|
||||||
|
const customLogo = 'https://example.com/logo.png';
|
||||||
|
const privacyUrl = 'https://privacy.example.com';
|
||||||
|
const termsOfUseUrl = 'https://termsOfUse.example.com';
|
||||||
|
const imprintUrl = 'https://imprint.example.com';
|
||||||
|
const invalidCustomLogo = 'example.com/logo.png';
|
||||||
|
const invalidPrivacyUrl = 'privacy.example.com';
|
||||||
|
const invalidTermsOfUseUrl = 'termsOfUse.example.com';
|
||||||
|
const invalidImprintUrl = 'imprint.example.com';
|
||||||
|
|
||||||
|
it('correctly parses valid config', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_CUSTOM_NAME: customName,
|
||||||
|
HD_CUSTOM_LOGO: customLogo,
|
||||||
|
HD_PRIVACY_URL: privacyUrl,
|
||||||
|
HD_TERMS_OF_USE_URL: termsOfUseUrl,
|
||||||
|
HD_IMPRINT_URL: imprintUrl,
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const config = customizationConfig();
|
||||||
|
expect(config.branding.customName).toEqual(customName);
|
||||||
|
expect(config.branding.customLogo).toEqual(customLogo);
|
||||||
|
expect(config.specialUrls.privacy).toEqual(privacyUrl);
|
||||||
|
expect(config.specialUrls.termsOfUse).toEqual(termsOfUseUrl);
|
||||||
|
expect(config.specialUrls.imprint).toEqual(imprintUrl);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if anything is wrongly configured', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_CUSTOM_NAME: customName,
|
||||||
|
HD_CUSTOM_LOGO: invalidCustomLogo,
|
||||||
|
HD_PRIVACY_URL: invalidPrivacyUrl,
|
||||||
|
HD_TERMS_OF_USE_URL: invalidTermsOfUseUrl,
|
||||||
|
HD_IMPRINT_URL: invalidImprintUrl,
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(() => customizationConfig()).toThrow(
|
||||||
|
`- HD_BRANDING_CUSTOM_LOGO: Invalid url
|
||||||
|
- HD_SPECIAL_URLS_PRIVACY: Invalid url
|
||||||
|
- HD_SPECIAL_URLS_TERMS_OF_USE: Invalid url
|
||||||
|
- HD_SPECIAL_URLS_IMPRINT: Invalid url`,
|
||||||
|
);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,77 +4,44 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
import * as Joi from 'joi';
|
import z from 'zod';
|
||||||
|
|
||||||
import { buildErrorMessage } from './utils';
|
import {
|
||||||
|
buildErrorMessage,
|
||||||
|
extractDescriptionFromZodIssue,
|
||||||
|
} from './zod-error-message';
|
||||||
|
|
||||||
export interface CustomizationConfig {
|
const schema = z.object({
|
||||||
branding: {
|
branding: z.object({
|
||||||
customName: string | null;
|
customName: z.string().or(z.null()).describe('HD_CUSTOM_NAME'),
|
||||||
customLogo: string | null;
|
customLogo: z.string().url().or(z.null()).describe('HD_CUSTOM_LOGO'),
|
||||||
};
|
|
||||||
specialUrls: {
|
|
||||||
privacy: string | null;
|
|
||||||
termsOfUse: string | null;
|
|
||||||
imprint: string | null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = Joi.object({
|
|
||||||
branding: Joi.object({
|
|
||||||
customName: Joi.string().allow(null).label('HD_CUSTOM_NAME'),
|
|
||||||
customLogo: Joi.string()
|
|
||||||
.uri({
|
|
||||||
scheme: [/https?/],
|
|
||||||
})
|
|
||||||
.allow(null)
|
|
||||||
.label('HD_CUSTOM_LOGO'),
|
|
||||||
}),
|
}),
|
||||||
specialUrls: Joi.object({
|
specialUrls: z.object({
|
||||||
privacy: Joi.string()
|
privacy: z.string().url().or(z.null()).describe('HD_PRIVACY_URL'),
|
||||||
.uri({
|
termsOfUse: z.string().url().or(z.null()).describe('HD_TERMS_OF_USE_URL'),
|
||||||
scheme: /https?/,
|
imprint: z.string().url().or(z.null()).describe('HD_IMPRINT_URL'),
|
||||||
})
|
|
||||||
.allow(null)
|
|
||||||
.label('HD_PRIVACY_URL'),
|
|
||||||
termsOfUse: Joi.string()
|
|
||||||
.uri({
|
|
||||||
scheme: /https?/,
|
|
||||||
})
|
|
||||||
.allow(null)
|
|
||||||
.label('HD_TERMS_OF_USE_URL'),
|
|
||||||
imprint: Joi.string()
|
|
||||||
.uri({
|
|
||||||
scheme: /https?/,
|
|
||||||
})
|
|
||||||
.allow(null)
|
|
||||||
.label('HD_IMPRINT_URL'),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type CustomizationConfig = z.infer<typeof schema>;
|
||||||
|
|
||||||
export default registerAs('customizationConfig', () => {
|
export default registerAs('customizationConfig', () => {
|
||||||
const customizationConfig = schema.validate(
|
const customizationConfig = schema.safeParse({
|
||||||
{
|
branding: {
|
||||||
branding: {
|
customName: process.env.HD_CUSTOM_NAME ?? null,
|
||||||
customName: process.env.HD_CUSTOM_NAME ?? null,
|
customLogo: process.env.HD_CUSTOM_LOGO ?? null,
|
||||||
customLogo: process.env.HD_CUSTOM_LOGO ?? null,
|
|
||||||
},
|
|
||||||
specialUrls: {
|
|
||||||
privacy: process.env.HD_PRIVACY_URL ?? null,
|
|
||||||
termsOfUse: process.env.HD_TERMS_OF_USE_URL ?? null,
|
|
||||||
imprint: process.env.HD_IMPRINT_URL ?? null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
specialUrls: {
|
||||||
abortEarly: false,
|
privacy: process.env.HD_PRIVACY_URL ?? null,
|
||||||
presence: 'required',
|
termsOfUse: process.env.HD_TERMS_OF_USE_URL ?? null,
|
||||||
|
imprint: process.env.HD_IMPRINT_URL ?? null,
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
if (customizationConfig.error) {
|
if (customizationConfig.error) {
|
||||||
const errorMessages = customizationConfig.error.details.map(
|
const errorMessages = customizationConfig.error.errors.map((issue) =>
|
||||||
(detail) => detail.message,
|
extractDescriptionFromZodIssue(issue, 'HD'),
|
||||||
);
|
);
|
||||||
throw new Error(buildErrorMessage(errorMessages));
|
throw new Error(buildErrorMessage(errorMessages));
|
||||||
}
|
}
|
||||||
return customizationConfig.value as CustomizationConfig;
|
return customizationConfig.data;
|
||||||
});
|
});
|
||||||
|
|
169
backend/src/config/database.config.spec.ts
Normal file
169
backend/src/config/database.config.spec.ts
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import mockedEnv from 'mocked-env';
|
||||||
|
|
||||||
|
import databaseConfig, {
|
||||||
|
MariadbDatabaseConfig,
|
||||||
|
MySQLDatabaseConfig,
|
||||||
|
PostgresDatabaseConfig,
|
||||||
|
SqliteDatabaseConfig,
|
||||||
|
} from './database.config';
|
||||||
|
|
||||||
|
describe('databaseConfig', () => {
|
||||||
|
const databaseTypeSqlite = 'sqlite';
|
||||||
|
const databaseTypeMysql = 'mysql';
|
||||||
|
const databaseTypeMariadb = 'mariadb';
|
||||||
|
const databaseTypePostgres = 'postgres';
|
||||||
|
const databaseName = 'test-db';
|
||||||
|
const databaseUser = 'test-user';
|
||||||
|
const databasePass = 'test-pass';
|
||||||
|
const databaseHost = 'test-host';
|
||||||
|
const databasePort = 1234;
|
||||||
|
const invalidDatabasePort = -1234;
|
||||||
|
const invalidDatabasePort2 = 65536;
|
||||||
|
const databaseFileSqlite = 'test.db';
|
||||||
|
|
||||||
|
describe('correctly parses valid', () => {
|
||||||
|
it('SQLite config', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_DATABASE_TYPE: databaseTypeSqlite,
|
||||||
|
HD_DATABASE_NAME: databaseFileSqlite,
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const config = databaseConfig() as SqliteDatabaseConfig;
|
||||||
|
expect(config.type).toEqual(databaseTypeSqlite);
|
||||||
|
expect(config.name).toEqual(databaseFileSqlite);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MySQL config', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_DATABASE_TYPE: databaseTypeMysql,
|
||||||
|
HD_DATABASE_NAME: databaseName,
|
||||||
|
HD_DATABASE_USERNAME: databaseUser,
|
||||||
|
HD_DATABASE_PASSWORD: databasePass,
|
||||||
|
HD_DATABASE_HOST: databaseHost,
|
||||||
|
HD_DATABASE_PORT: String(databasePort),
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const config = databaseConfig() as MySQLDatabaseConfig;
|
||||||
|
expect(config.type).toEqual(databaseTypeMysql);
|
||||||
|
expect(config.name).toEqual(databaseName);
|
||||||
|
expect(config.username).toEqual(databaseUser);
|
||||||
|
expect(config.password).toEqual(databasePass);
|
||||||
|
expect(config.host).toEqual(databaseHost);
|
||||||
|
expect(config.port).toEqual(databasePort);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MariaDB config', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_DATABASE_TYPE: databaseTypeMariadb,
|
||||||
|
HD_DATABASE_NAME: databaseName,
|
||||||
|
HD_DATABASE_USERNAME: databaseUser,
|
||||||
|
HD_DATABASE_PASSWORD: databasePass,
|
||||||
|
HD_DATABASE_HOST: databaseHost,
|
||||||
|
HD_DATABASE_PORT: String(databasePort),
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const config = databaseConfig() as MariadbDatabaseConfig;
|
||||||
|
expect(config.type).toEqual(databaseTypeMariadb);
|
||||||
|
expect(config.name).toEqual(databaseName);
|
||||||
|
expect(config.username).toEqual(databaseUser);
|
||||||
|
expect(config.password).toEqual(databasePass);
|
||||||
|
expect(config.host).toEqual(databaseHost);
|
||||||
|
expect(config.port).toEqual(databasePort);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Postgres config', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_DATABASE_TYPE: databaseTypePostgres,
|
||||||
|
HD_DATABASE_NAME: databaseName,
|
||||||
|
HD_DATABASE_USERNAME: databaseUser,
|
||||||
|
HD_DATABASE_PASSWORD: databasePass,
|
||||||
|
HD_DATABASE_HOST: databaseHost,
|
||||||
|
HD_DATABASE_PORT: String(databasePort),
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const config = databaseConfig() as PostgresDatabaseConfig;
|
||||||
|
expect(config.type).toEqual(databaseTypePostgres);
|
||||||
|
expect(config.name).toEqual(databaseName);
|
||||||
|
expect(config.username).toEqual(databaseUser);
|
||||||
|
expect(config.password).toEqual(databasePass);
|
||||||
|
expect(config.host).toEqual(databaseHost);
|
||||||
|
expect(config.port).toEqual(databasePort);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if the port is negative', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_DATABASE_TYPE: databaseTypePostgres,
|
||||||
|
HD_DATABASE_NAME: databaseName,
|
||||||
|
HD_DATABASE_USERNAME: databaseUser,
|
||||||
|
HD_DATABASE_PASSWORD: databasePass,
|
||||||
|
HD_DATABASE_HOST: databaseHost,
|
||||||
|
HD_DATABASE_PORT: String(invalidDatabasePort),
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(() => databaseConfig()).toThrow(
|
||||||
|
'HD_DATABASE_PORT: Number must be greater than 0',
|
||||||
|
);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
it('throws an error if the port is too big', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_DATABASE_TYPE: databaseTypePostgres,
|
||||||
|
HD_DATABASE_NAME: databaseName,
|
||||||
|
HD_DATABASE_USERNAME: databaseUser,
|
||||||
|
HD_DATABASE_PASSWORD: databasePass,
|
||||||
|
HD_DATABASE_HOST: databaseHost,
|
||||||
|
HD_DATABASE_PORT: String(invalidDatabasePort2),
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(() => databaseConfig()).toThrow(
|
||||||
|
'HD_DATABASE_PORT: Number must be less than or equal to 65535',
|
||||||
|
);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,73 +1,92 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
import * as Joi from 'joi';
|
import z from 'zod';
|
||||||
|
|
||||||
import { DatabaseType } from './database-type.enum';
|
import { DatabaseType } from './database-type.enum';
|
||||||
import { buildErrorMessage, parseOptionalNumber } from './utils';
|
import { parseOptionalNumber } from './utils';
|
||||||
|
import {
|
||||||
|
buildErrorMessage,
|
||||||
|
extractDescriptionFromZodIssue,
|
||||||
|
} from './zod-error-message';
|
||||||
|
|
||||||
export interface DatabaseConfig {
|
const sqliteDbSchema = z.object({
|
||||||
username: string;
|
type: z.literal(DatabaseType.SQLITE).describe('HD_DATABASE_TYPE'),
|
||||||
password: string;
|
name: z.string().describe('HD_DATABASE_NAME'),
|
||||||
database: string;
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
type: DatabaseType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const databaseSchema = Joi.object({
|
|
||||||
type: Joi.string()
|
|
||||||
.valid(...Object.values(DatabaseType))
|
|
||||||
.label('HD_DATABASE_TYPE'),
|
|
||||||
|
|
||||||
// This is the database name, except for SQLite,
|
|
||||||
// where it is the path to the database file.
|
|
||||||
database: Joi.string().label('HD_DATABASE_NAME'),
|
|
||||||
username: Joi.when('type', {
|
|
||||||
is: Joi.invalid(DatabaseType.SQLITE),
|
|
||||||
then: Joi.string(),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}).label('HD_DATABASE_USER'),
|
|
||||||
password: Joi.when('type', {
|
|
||||||
is: Joi.invalid(DatabaseType.SQLITE),
|
|
||||||
then: Joi.string(),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}).label('HD_DATABASE_PASS'),
|
|
||||||
host: Joi.when('type', {
|
|
||||||
is: Joi.invalid(DatabaseType.SQLITE),
|
|
||||||
then: Joi.string(),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}).label('HD_DATABASE_HOST'),
|
|
||||||
port: Joi.when('type', {
|
|
||||||
is: Joi.invalid(DatabaseType.SQLITE),
|
|
||||||
then: Joi.number(),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}).label('HD_DATABASE_PORT'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const postgresDbSchema = z.object({
|
||||||
|
type: z.literal(DatabaseType.POSTGRES).describe('HD_DATABASE_TYPE'),
|
||||||
|
name: z.string().describe('HD_DATABASE_NAME'),
|
||||||
|
username: z.string().describe('HD_DATABASE_USERNAME'),
|
||||||
|
password: z.string().describe('HD_DATABASE_PASSWORD'),
|
||||||
|
host: z.string().describe('HD_DATABASE_HOST'),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.max(65535)
|
||||||
|
.default(5432)
|
||||||
|
.describe('HD_DATABASE_PORT'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mariaDbSchema = z.object({
|
||||||
|
type: z.literal(DatabaseType.MARIADB).describe('HD_DATABASE_TYPE'),
|
||||||
|
name: z.string().describe('HD_DATABASE_NAME'),
|
||||||
|
username: z.string().describe('HD_DATABASE_USERNAME'),
|
||||||
|
password: z.string().describe('HD_DATABASE_PASSWORD'),
|
||||||
|
host: z.string().describe('HD_DATABASE_HOST'),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.max(65535)
|
||||||
|
.default(3306)
|
||||||
|
.describe('HD_DATABASE_PORT'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mysqlDbSchema = z.object({
|
||||||
|
type: z.literal(DatabaseType.MYSQL).describe('HD_DATABASE_TYPE'),
|
||||||
|
name: z.string().describe('HD_DATABASE_NAME'),
|
||||||
|
username: z.string().describe('HD_DATABASE_USERNAME'),
|
||||||
|
password: z.string().describe('HD_DATABASE_PASSWORD'),
|
||||||
|
host: z.string().describe('HD_DATABASE_HOST'),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.max(65535)
|
||||||
|
.default(3306)
|
||||||
|
.describe('HD_DATABASE_PORT'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbSchema = z.discriminatedUnion('type', [
|
||||||
|
sqliteDbSchema,
|
||||||
|
mariaDbSchema,
|
||||||
|
mysqlDbSchema,
|
||||||
|
postgresDbSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type SqliteDatabaseConfig = z.infer<typeof sqliteDbSchema>;
|
||||||
|
export type PostgresDatabaseConfig = z.infer<typeof postgresDbSchema>;
|
||||||
|
export type MariadbDatabaseConfig = z.infer<typeof mariaDbSchema>;
|
||||||
|
export type MySQLDatabaseConfig = z.infer<typeof mysqlDbSchema>;
|
||||||
|
export type DatabaseConfig = z.infer<typeof dbSchema>;
|
||||||
|
|
||||||
export default registerAs('databaseConfig', () => {
|
export default registerAs('databaseConfig', () => {
|
||||||
const databaseConfig = databaseSchema.validate(
|
const databaseConfig = dbSchema.safeParse({
|
||||||
{
|
type: process.env.HD_DATABASE_TYPE,
|
||||||
type: process.env.HD_DATABASE_TYPE,
|
username: process.env.HD_DATABASE_USERNAME,
|
||||||
username: process.env.HD_DATABASE_USER,
|
password: process.env.HD_DATABASE_PASSWORD,
|
||||||
password: process.env.HD_DATABASE_PASS,
|
name: process.env.HD_DATABASE_NAME,
|
||||||
database: process.env.HD_DATABASE_NAME,
|
host: process.env.HD_DATABASE_HOST,
|
||||||
host: process.env.HD_DATABASE_HOST,
|
port: parseOptionalNumber(process.env.HD_DATABASE_PORT),
|
||||||
port: parseOptionalNumber(process.env.HD_DATABASE_PORT),
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
abortEarly: false,
|
|
||||||
presence: 'required',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (databaseConfig.error) {
|
if (databaseConfig.error) {
|
||||||
const errorMessages = databaseConfig.error.details.map(
|
const errorMessages = databaseConfig.error.errors.map((issue) =>
|
||||||
(detail) => detail.message,
|
extractDescriptionFromZodIssue(issue, 'HD_DATABASE'),
|
||||||
);
|
);
|
||||||
throw new Error(buildErrorMessage(errorMessages));
|
throw new Error(buildErrorMessage(errorMessages));
|
||||||
}
|
}
|
||||||
return databaseConfig.value as DatabaseConfig;
|
return databaseConfig.data;
|
||||||
});
|
});
|
||||||
|
|
64
backend/src/config/external-services.config.spec.ts
Normal file
64
backend/src/config/external-services.config.spec.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import mockedEnv from 'mocked-env';
|
||||||
|
|
||||||
|
import externalServicesConfig from './external-services.config';
|
||||||
|
|
||||||
|
describe('externalServices', () => {
|
||||||
|
const plantUmlServer = 'https://plantuml.example.com';
|
||||||
|
const imageProxy = 'https://proxy.example.com';
|
||||||
|
|
||||||
|
it('correctly parses valid config', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_PLANTUML_SERVER: plantUmlServer,
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const config = externalServicesConfig();
|
||||||
|
expect(config.plantumlServer).toEqual(plantUmlServer);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if PlantUML server is configured with an invalid URL', () => {
|
||||||
|
const invalid = 'wrong!';
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_PLANTUML_SERVER: invalid,
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(() => externalServicesConfig()).toThrow(
|
||||||
|
'HD_PLANTUML_SERVER: Invalid url',
|
||||||
|
);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if image proxy is configured', () => {
|
||||||
|
const restore = mockedEnv(
|
||||||
|
{
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
HD_IMAGE_PROXY: imageProxy,
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(() => externalServicesConfig()).toThrow(
|
||||||
|
"HD_IMAGE_PROXY is currently not yet supported. Please don't configure it",
|
||||||
|
);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,51 +4,35 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
import * as Joi from 'joi';
|
import z from 'zod';
|
||||||
|
|
||||||
import { buildErrorMessage } from './utils';
|
import {
|
||||||
|
buildErrorMessage,
|
||||||
|
extractDescriptionFromZodIssue,
|
||||||
|
} from './zod-error-message';
|
||||||
|
|
||||||
export interface ExternalServicesConfig {
|
const schema = z.object({
|
||||||
plantUmlServer: string | null;
|
plantumlServer: z.string().url().or(z.null()).describe('HD_PLANTUML_SERVER'),
|
||||||
imageProxy: string;
|
imageProxy: z.string().url().or(z.null()).describe('HD_IMAGE_PROXY'),
|
||||||
}
|
|
||||||
|
|
||||||
const schema = Joi.object({
|
|
||||||
plantUmlServer: Joi.string()
|
|
||||||
.uri({
|
|
||||||
scheme: /https?/,
|
|
||||||
})
|
|
||||||
.allow(null)
|
|
||||||
.label('HD_PLANTUML_SERVER'),
|
|
||||||
imageProxy: Joi.string()
|
|
||||||
.uri({
|
|
||||||
scheme: /https?/,
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.label('HD_IMAGE_PROXY'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type ExternalServicesConfig = z.infer<typeof schema>;
|
||||||
|
|
||||||
export default registerAs('externalServicesConfig', () => {
|
export default registerAs('externalServicesConfig', () => {
|
||||||
if (process.env.HD_IMAGE_PROXY !== undefined) {
|
if (process.env.HD_IMAGE_PROXY !== undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"HD_IMAGE_PROXY is currently not yet supported. Please don't configure it",
|
"HD_IMAGE_PROXY is currently not yet supported. Please don't configure it",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const externalConfig = schema.validate(
|
const externalConfig = schema.safeParse({
|
||||||
{
|
plantumlServer: process.env.HD_PLANTUML_SERVER ?? null,
|
||||||
plantUmlServer: process.env.HD_PLANTUML_SERVER ?? null,
|
imageProxy: process.env.HD_IMAGE_PROXY ?? null,
|
||||||
imageProxy: process.env.HD_IMAGE_PROXY,
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
abortEarly: false,
|
|
||||||
presence: 'required',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (externalConfig.error) {
|
if (externalConfig.error) {
|
||||||
const errorMessages = externalConfig.error.details.map(
|
const errorMessages = externalConfig.error.errors.map((issue) =>
|
||||||
(detail) => detail.message,
|
extractDescriptionFromZodIssue(issue, 'HD'),
|
||||||
);
|
);
|
||||||
throw new Error(buildErrorMessage(errorMessages));
|
throw new Error(buildErrorMessage(errorMessages));
|
||||||
}
|
}
|
||||||
return externalConfig.value as ExternalServicesConfig;
|
return externalConfig.data;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import mockedEnv from 'mocked-env';
|
import mockedEnv from 'mocked-env';
|
||||||
|
|
||||||
import { BackendType } from '../media/backends/backend-type.enum';
|
import { BackendType } from '../media/backends/backend-type.enum';
|
||||||
import mediaConfig from './media.config';
|
import mediaConfig, {
|
||||||
|
AzureMediaConfig,
|
||||||
|
FilesystemMediaConfig,
|
||||||
|
ImgurMediaConfig,
|
||||||
|
S3MediaConfig,
|
||||||
|
WebdavMediaConfig,
|
||||||
|
} from './media.config';
|
||||||
|
|
||||||
describe('mediaConfig', () => {
|
describe('mediaConfig', () => {
|
||||||
// Filesystem
|
// Filesystem
|
||||||
|
@ -41,7 +47,7 @@ describe('mediaConfig', () => {
|
||||||
clear: true,
|
clear: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = mediaConfig();
|
const config = mediaConfig() as { backend: FilesystemMediaConfig };
|
||||||
expect(config.backend.use).toEqual(BackendType.FILESYSTEM);
|
expect(config.backend.use).toEqual(BackendType.FILESYSTEM);
|
||||||
expect(config.backend.filesystem.uploadPath).toEqual(uploadPath);
|
expect(config.backend.filesystem.uploadPath).toEqual(uploadPath);
|
||||||
restore();
|
restore();
|
||||||
|
@ -64,12 +70,12 @@ describe('mediaConfig', () => {
|
||||||
clear: true,
|
clear: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = mediaConfig();
|
const config = mediaConfig() as { backend: S3MediaConfig };
|
||||||
expect(config.backend.use).toEqual(BackendType.S3);
|
expect(config.backend.use).toEqual(BackendType.S3);
|
||||||
expect(config.backend.s3.accessKeyId).toEqual(accessKeyId);
|
expect(config.backend.s3.accessKeyId).toEqual(accessKeyId);
|
||||||
expect(config.backend.s3.secretAccessKey).toEqual(secretAccessKey);
|
expect(config.backend.s3.secretAccessKey).toEqual(secretAccessKey);
|
||||||
expect(config.backend.s3.bucket).toEqual(bucket);
|
expect(config.backend.s3.bucket).toEqual(bucket);
|
||||||
expect(config.backend.s3.endPoint).toEqual(endPoint);
|
expect(config.backend.s3.endpoint).toEqual(endPoint);
|
||||||
expect(config.backend.s3.region).toEqual(region);
|
expect(config.backend.s3.region).toEqual(region);
|
||||||
expect(config.backend.s3.pathStyle).toEqual(pathStyle);
|
expect(config.backend.s3.pathStyle).toEqual(pathStyle);
|
||||||
restore();
|
restore();
|
||||||
|
@ -88,7 +94,7 @@ describe('mediaConfig', () => {
|
||||||
clear: true,
|
clear: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = mediaConfig();
|
const config = mediaConfig() as { backend: AzureMediaConfig };
|
||||||
expect(config.backend.use).toEqual(BackendType.AZURE);
|
expect(config.backend.use).toEqual(BackendType.AZURE);
|
||||||
expect(config.backend.azure.connectionString).toEqual(
|
expect(config.backend.azure.connectionString).toEqual(
|
||||||
azureConnectionString,
|
azureConnectionString,
|
||||||
|
@ -109,9 +115,9 @@ describe('mediaConfig', () => {
|
||||||
clear: true,
|
clear: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = mediaConfig();
|
const config = mediaConfig() as { backend: ImgurMediaConfig };
|
||||||
expect(config.backend.use).toEqual(BackendType.IMGUR);
|
expect(config.backend.use).toEqual(BackendType.IMGUR);
|
||||||
expect(config.backend.imgur.clientID).toEqual(clientID);
|
expect(config.backend.imgur.clientId).toEqual(clientID);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -129,7 +135,7 @@ describe('mediaConfig', () => {
|
||||||
clear: true,
|
clear: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const config = mediaConfig();
|
const config = mediaConfig() as { backend: WebdavMediaConfig };
|
||||||
expect(config.backend.use).toEqual(BackendType.WEBDAV);
|
expect(config.backend.use).toEqual(BackendType.WEBDAV);
|
||||||
expect(config.backend.webdav.connectionString).toEqual(
|
expect(config.backend.webdav.connectionString).toEqual(
|
||||||
webdavConnectionString,
|
webdavConnectionString,
|
||||||
|
@ -154,7 +160,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH" is required',
|
'HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -176,7 +182,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_S3_ACCESS_KEY" is required',
|
'HD_MEDIA_BACKEND_S3_ACCESS_KEY_ID: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -195,7 +201,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_S3_SECRET_KEY" is required',
|
'HD_MEDIA_BACKEND_S3_SECRET_ACCESS_KEY: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -214,7 +220,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_S3_BUCKET" is required',
|
'HD_MEDIA_BACKEND_S3_BUCKET: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -233,7 +239,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_S3_ENDPOINT" is required',
|
'HD_MEDIA_BACKEND_S3_ENDPOINT: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -253,27 +259,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_S3_ENDPOINT" must be a valid uri with a scheme matching the ^https? pattern',
|
'HD_MEDIA_BACKEND_S3_ENDPOINT: Invalid url',
|
||||||
);
|
|
||||||
restore();
|
|
||||||
});
|
|
||||||
it('when HD_MEDIA_BACKEND_S3_ENDPOINT is an URI with a non-http(s) protocol', async () => {
|
|
||||||
const restore = mockedEnv(
|
|
||||||
{
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
|
||||||
HD_MEDIA_BACKEND: BackendType.S3,
|
|
||||||
HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId,
|
|
||||||
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
|
|
||||||
HD_MEDIA_BACKEND_S3_BUCKET: bucket,
|
|
||||||
HD_MEDIA_BACKEND_S3_ENDPOINT: 'ftps://example.org',
|
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
|
||||||
},
|
|
||||||
{
|
|
||||||
clear: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(() => mediaConfig()).toThrow(
|
|
||||||
'"HD_MEDIA_BACKEND_S3_ENDPOINT" must be a valid uri with a scheme matching the ^https? pattern',
|
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -293,7 +279,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING" is required',
|
'HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -310,7 +296,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_AZURE_CONTAINER" is required',
|
'HD_MEDIA_BACKEND_AZURE_CONTAINER: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -329,7 +315,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_IMGUR_CLIENT_ID" is required',
|
'HD_MEDIA_BACKEND_IMGUR_CLIENT_ID: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -350,7 +336,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING" is required',
|
'HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -369,7 +355,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING" must be a valid uri',
|
'HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: Invalid url',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -387,7 +373,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL" is required',
|
'HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: Required',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -406,7 +392,7 @@ describe('mediaConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => mediaConfig()).toThrow(
|
expect(() => mediaConfig()).toThrow(
|
||||||
'"HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL" must be a valid uri',
|
'HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: Invalid url',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,152 +1,125 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
import * as Joi from 'joi';
|
import z from 'zod';
|
||||||
|
|
||||||
import { BackendType } from '../media/backends/backend-type.enum';
|
import { BackendType } from '../media/backends/backend-type.enum';
|
||||||
import { buildErrorMessage, parseOptionalBoolean } from './utils';
|
import { parseOptionalBoolean } from './utils';
|
||||||
|
import {
|
||||||
|
buildErrorMessage,
|
||||||
|
extractDescriptionFromZodIssue,
|
||||||
|
} from './zod-error-message';
|
||||||
|
|
||||||
export interface MediaConfig {
|
const azureSchema = z.object({
|
||||||
backend: MediaBackendConfig;
|
use: z.literal(BackendType.AZURE),
|
||||||
}
|
azure: z.object({
|
||||||
|
connectionString: z
|
||||||
export interface MediaBackendConfig {
|
.string()
|
||||||
use: BackendType;
|
.describe('HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING'),
|
||||||
filesystem: {
|
container: z.string().describe('HD_MEDIA_BACKEND_AZURE_CONTAINER'),
|
||||||
uploadPath: string;
|
}),
|
||||||
};
|
|
||||||
s3: {
|
|
||||||
accessKeyId: string;
|
|
||||||
secretAccessKey: string;
|
|
||||||
bucket: string;
|
|
||||||
endPoint: string;
|
|
||||||
region: string;
|
|
||||||
pathStyle: boolean;
|
|
||||||
};
|
|
||||||
azure: {
|
|
||||||
connectionString: string;
|
|
||||||
container: string;
|
|
||||||
};
|
|
||||||
imgur: {
|
|
||||||
clientID: string;
|
|
||||||
};
|
|
||||||
webdav: {
|
|
||||||
connectionString: string;
|
|
||||||
uploadDir: string;
|
|
||||||
publicUrl: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaSchema = Joi.object({
|
|
||||||
backend: {
|
|
||||||
use: Joi.string()
|
|
||||||
.valid(...Object.values(BackendType))
|
|
||||||
.label('HD_MEDIA_BACKEND'),
|
|
||||||
filesystem: {
|
|
||||||
uploadPath: Joi.when('...use', {
|
|
||||||
is: Joi.valid(BackendType.FILESYSTEM),
|
|
||||||
then: Joi.string(),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}).label('HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH'),
|
|
||||||
},
|
|
||||||
s3: Joi.when('use', {
|
|
||||||
is: Joi.valid(BackendType.S3),
|
|
||||||
then: Joi.object({
|
|
||||||
accessKeyId: Joi.string().label('HD_MEDIA_BACKEND_S3_ACCESS_KEY'),
|
|
||||||
secretAccessKey: Joi.string().label('HD_MEDIA_BACKEND_S3_SECRET_KEY'),
|
|
||||||
bucket: Joi.string().label('HD_MEDIA_BACKEND_S3_BUCKET'),
|
|
||||||
endPoint: Joi.string()
|
|
||||||
.uri({ scheme: /^https?/ })
|
|
||||||
.label('HD_MEDIA_BACKEND_S3_ENDPOINT'),
|
|
||||||
region: Joi.string().optional().label('HD_MEDIA_BACKEND_S3_REGION'),
|
|
||||||
pathStyle: Joi.boolean()
|
|
||||||
.default(false)
|
|
||||||
.label('HD_MEDIA_BACKEND_S3_PATH_STYLE'),
|
|
||||||
}),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}),
|
|
||||||
azure: Joi.when('use', {
|
|
||||||
is: Joi.valid(BackendType.AZURE),
|
|
||||||
then: Joi.object({
|
|
||||||
connectionString: Joi.string().label(
|
|
||||||
'HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING',
|
|
||||||
),
|
|
||||||
container: Joi.string().label('HD_MEDIA_BACKEND_AZURE_CONTAINER'),
|
|
||||||
}),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}),
|
|
||||||
imgur: Joi.when('use', {
|
|
||||||
is: Joi.valid(BackendType.IMGUR),
|
|
||||||
then: Joi.object({
|
|
||||||
clientID: Joi.string().label('HD_MEDIA_BACKEND_IMGUR_CLIENT_ID'),
|
|
||||||
}),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}),
|
|
||||||
webdav: Joi.when('use', {
|
|
||||||
is: Joi.valid(BackendType.WEBDAV),
|
|
||||||
then: Joi.object({
|
|
||||||
connectionString: Joi.string()
|
|
||||||
.uri()
|
|
||||||
.label('HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING'),
|
|
||||||
uploadDir: Joi.string()
|
|
||||||
.optional()
|
|
||||||
.label('HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR'),
|
|
||||||
publicUrl: Joi.string()
|
|
||||||
.uri()
|
|
||||||
.label('HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL'),
|
|
||||||
}),
|
|
||||||
otherwise: Joi.optional(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filesystemSchema = z.object({
|
||||||
|
use: z.literal(BackendType.FILESYSTEM),
|
||||||
|
filesystem: z.object({
|
||||||
|
uploadPath: z.string().describe('HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const imgurSchema = z.object({
|
||||||
|
use: z.literal(BackendType.IMGUR),
|
||||||
|
imgur: z.object({
|
||||||
|
clientId: z.string().describe('HD_MEDIA_BACKEND_IMGUR_CLIENT_ID'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const s3Schema = z.object({
|
||||||
|
use: z.literal(BackendType.S3),
|
||||||
|
s3: z.object({
|
||||||
|
accessKeyId: z.string().describe('HD_MEDIA_BACKEND_S3_ACCESS_KEY'),
|
||||||
|
secretAccessKey: z.string().describe('HD_MEDIA_BACKEND_S3_SECRET_KEY'),
|
||||||
|
bucket: z.string().describe('HD_MEDIA_BACKEND_S3_BUCKET'),
|
||||||
|
endpoint: z.string().url().describe('HD_MEDIA_BACKEND_S3_ENDPOINT'),
|
||||||
|
region: z.string().optional().describe('HD_MEDIA_BACKEND_S3_REGION'),
|
||||||
|
pathStyle: z
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.describe('HD_MEDIA_BACKEND_S3_PATH_STYLE'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const webdavSchema = z.object({
|
||||||
|
use: z.literal(BackendType.WEBDAV),
|
||||||
|
webdav: z.object({
|
||||||
|
connectionString: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.describe('HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING'),
|
||||||
|
uploadDir: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR'),
|
||||||
|
publicUrl: z.string().url().describe('HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
backend: z.discriminatedUnion('use', [
|
||||||
|
azureSchema,
|
||||||
|
filesystemSchema,
|
||||||
|
imgurSchema,
|
||||||
|
s3Schema,
|
||||||
|
webdavSchema,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type MediaConfig = z.infer<typeof schema>;
|
||||||
|
export type AzureMediaConfig = z.infer<typeof azureSchema>;
|
||||||
|
export type FilesystemMediaConfig = z.infer<typeof filesystemSchema>;
|
||||||
|
export type ImgurMediaConfig = z.infer<typeof imgurSchema>;
|
||||||
|
export type S3MediaConfig = z.infer<typeof s3Schema>;
|
||||||
|
export type WebdavMediaConfig = z.infer<typeof webdavSchema>;
|
||||||
|
|
||||||
export default registerAs('mediaConfig', () => {
|
export default registerAs('mediaConfig', () => {
|
||||||
const mediaConfig = mediaSchema.validate(
|
const mediaConfig = schema.safeParse({
|
||||||
{
|
backend: {
|
||||||
backend: {
|
use: process.env.HD_MEDIA_BACKEND,
|
||||||
use: process.env.HD_MEDIA_BACKEND,
|
filesystem: {
|
||||||
filesystem: {
|
uploadPath: process.env.HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH,
|
||||||
uploadPath: process.env.HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH,
|
},
|
||||||
},
|
s3: {
|
||||||
s3: {
|
accessKeyId: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY,
|
||||||
accessKeyId: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY,
|
secretAccessKey: process.env.HD_MEDIA_BACKEND_S3_SECRET_KEY,
|
||||||
secretAccessKey: process.env.HD_MEDIA_BACKEND_S3_SECRET_KEY,
|
bucket: process.env.HD_MEDIA_BACKEND_S3_BUCKET,
|
||||||
bucket: process.env.HD_MEDIA_BACKEND_S3_BUCKET,
|
endpoint: process.env.HD_MEDIA_BACKEND_S3_ENDPOINT,
|
||||||
endPoint: process.env.HD_MEDIA_BACKEND_S3_ENDPOINT,
|
region: process.env.HD_MEDIA_BACKEND_S3_REGION,
|
||||||
region: process.env.HD_MEDIA_BACKEND_S3_REGION,
|
pathStyle: parseOptionalBoolean(
|
||||||
pathStyle: parseOptionalBoolean(
|
process.env.HD_MEDIA_BACKEND_S3_PATH_STYLE,
|
||||||
process.env.HD_MEDIA_BACKEND_S3_PATH_STYLE,
|
),
|
||||||
),
|
},
|
||||||
},
|
azure: {
|
||||||
azure: {
|
connectionString: process.env.HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING,
|
||||||
connectionString:
|
container: process.env.HD_MEDIA_BACKEND_AZURE_CONTAINER,
|
||||||
process.env.HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING,
|
},
|
||||||
container: process.env.HD_MEDIA_BACKEND_AZURE_CONTAINER,
|
imgur: {
|
||||||
},
|
clientId: process.env.HD_MEDIA_BACKEND_IMGUR_CLIENT_ID,
|
||||||
imgur: {
|
},
|
||||||
clientID: process.env.HD_MEDIA_BACKEND_IMGUR_CLIENT_ID,
|
webdav: {
|
||||||
},
|
connectionString: process.env.HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING,
|
||||||
webdav: {
|
uploadDir: process.env.HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR,
|
||||||
connectionString:
|
publicUrl: process.env.HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL,
|
||||||
process.env.HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING,
|
|
||||||
uploadDir: process.env.HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR,
|
|
||||||
publicUrl: process.env.HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
abortEarly: false,
|
|
||||||
presence: 'required',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (mediaConfig.error) {
|
if (mediaConfig.error) {
|
||||||
const errorMessages = mediaConfig.error.details.map(
|
const errorMessages = mediaConfig.error.errors.map((issue) =>
|
||||||
(detail) => detail.message,
|
extractDescriptionFromZodIssue(issue, 'HD_MEDIA'),
|
||||||
);
|
);
|
||||||
throw new Error(buildErrorMessage(errorMessages));
|
throw new Error(buildErrorMessage(errorMessages));
|
||||||
}
|
}
|
||||||
return mediaConfig.value as MediaConfig;
|
return mediaConfig.data;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -13,7 +13,7 @@ export function createDefaultMockAppConfig(): AppConfig {
|
||||||
return {
|
return {
|
||||||
baseUrl: 'md.example.com',
|
baseUrl: 'md.example.com',
|
||||||
rendererBaseUrl: 'md-renderer.example.com',
|
rendererBaseUrl: 'md-renderer.example.com',
|
||||||
port: 3000,
|
backendPort: 3000,
|
||||||
loglevel: Loglevel.ERROR,
|
loglevel: Loglevel.ERROR,
|
||||||
showLogTimestamp: true,
|
showLogTimestamp: true,
|
||||||
persistInterval: 10,
|
persistInterval: 10,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -13,7 +13,7 @@ export function createDefaultMockDatabaseConfig(): DatabaseConfig {
|
||||||
return {
|
return {
|
||||||
type: (process.env.HEDGEDOC_TEST_DB_TYPE ||
|
type: (process.env.HEDGEDOC_TEST_DB_TYPE ||
|
||||||
DatabaseType.SQLITE) as DatabaseType,
|
DatabaseType.SQLITE) as DatabaseType,
|
||||||
database: 'hedgedoc',
|
name: 'hedgedoc',
|
||||||
password: 'hedgedoc',
|
password: 'hedgedoc',
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 0,
|
port: 0,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -10,7 +10,7 @@ import { ExternalServicesConfig } from '../external-services.config';
|
||||||
|
|
||||||
export function createDefaultMockExternalServicesConfig(): ExternalServicesConfig {
|
export function createDefaultMockExternalServicesConfig(): ExternalServicesConfig {
|
||||||
return {
|
return {
|
||||||
plantUmlServer: 'https://plantuml.example.com',
|
plantumlServer: 'https://plantuml.example.com',
|
||||||
imageProxy: 'https://imageProxy.example.com',
|
imageProxy: 'https://imageProxy.example.com',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -17,26 +17,6 @@ export function createDefaultMockMediaConfig(): MediaConfig {
|
||||||
uploadPath:
|
uploadPath:
|
||||||
'test_uploads' + Math.floor(Math.random() * 100000).toString(),
|
'test_uploads' + Math.floor(Math.random() * 100000).toString(),
|
||||||
},
|
},
|
||||||
s3: {
|
|
||||||
accessKeyId: '',
|
|
||||||
secretAccessKey: '',
|
|
||||||
bucket: '',
|
|
||||||
endPoint: '',
|
|
||||||
pathStyle: false,
|
|
||||||
region: '',
|
|
||||||
},
|
|
||||||
azure: {
|
|
||||||
connectionString: '',
|
|
||||||
container: '',
|
|
||||||
},
|
|
||||||
imgur: {
|
|
||||||
clientID: '',
|
|
||||||
},
|
|
||||||
webdav: {
|
|
||||||
connectionString: '',
|
|
||||||
uploadDir: '',
|
|
||||||
publicUrl: '',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
HD_REVISION_RETENTION_DAYS: retentionDays.toString(),
|
HD_REVISION_RETENTION_DAYS: retentionDays.toString(),
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
@ -58,8 +58,8 @@ describe('noteConfig', () => {
|
||||||
{
|
{
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -86,8 +86,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteId,
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteId,
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -115,8 +115,8 @@ describe('noteConfig', () => {
|
||||||
{
|
{
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -145,7 +145,7 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -174,7 +174,7 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -203,7 +203,7 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -231,8 +231,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -263,8 +263,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: invalidforbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: invalidforbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -273,7 +273,7 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
'"forbiddenNoteIds[0]" is not allowed to be empty',
|
'HD_FORBIDDEN_NOTE_IDS[0]: String must contain at least 1 character(s)\n - HD_FORBIDDEN_NOTE_IDS[1]: String must contain at least 1 character(s)',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -284,8 +284,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: negativeMaxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: negativeMaxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -294,7 +294,7 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
'"HD_MAX_DOCUMENT_LENGTH" must be a positive number',
|
'HD_MAX_DOCUMENT_LENGTH: Number must be greater than 0',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -305,8 +305,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: floatMaxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: floatMaxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -315,7 +315,7 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
'"HD_MAX_DOCUMENT_LENGTH" must be an integer',
|
'HD_MAX_DOCUMENT_LENGTH: Expected integer, received float',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -326,8 +326,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: invalidMaxDocumentLength,
|
HD_MAX_DOCUMENT_LENGTH: invalidMaxDocumentLength,
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -336,19 +336,19 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
'"HD_MAX_DOCUMENT_LENGTH" must be a number',
|
'HD_MAX_DOCUMENT_LENGTH: Expected number, received nan',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when given a non-valid HD_PERMISSION_DEFAULT_EVERYONE', async () => {
|
it('when given a non-valid HD_PERMISSIONS_DEFAULT_EVERYONE', async () => {
|
||||||
const restore = mockedEnv(
|
const restore = mockedEnv(
|
||||||
{
|
{
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: wrongDefaultPermission,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: wrongDefaultPermission,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -357,19 +357,19 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
'"HD_PERMISSION_DEFAULT_EVERYONE" must be one of [none, read, write]',
|
"HD_PERMISSIONS_DEFAULT_EVERYONE: Invalid enum value. Expected 'none' | 'read' | 'write', received 'wrong'",
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when given a non-valid HD_PERMISSION_DEFAULT_LOGGED_IN', async () => {
|
it('when given a non-valid HD_PERMISSIONS_DEFAULT_LOGGED_IN', async () => {
|
||||||
const restore = mockedEnv(
|
const restore = mockedEnv(
|
||||||
{
|
{
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: wrongDefaultPermission,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: wrongDefaultPermission,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -378,7 +378,7 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
'"HD_PERMISSION_DEFAULT_LOGGED_IN" must be one of [none, read, write]',
|
"HD_PERMISSIONS_DEFAULT_LOGGED_IN: Invalid enum value. Expected 'none' | 'read' | 'write', received 'wrong'",
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -399,7 +399,7 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
'"HD_GUEST_ACCESS" must be one of [deny, read, write, create]',
|
"HD_GUEST_ACCESS: Invalid enum value. Expected 'deny' | 'read' | 'write' | 'create', received 'wrong'",
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -410,8 +410,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: 'deny',
|
HD_GUEST_ACCESS: 'deny',
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -420,19 +420,19 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
`'HD_GUEST_ACCESS' is set to 'deny', but 'HD_PERMISSION_DEFAULT_EVERYONE' is also configured. Please remove 'HD_PERMISSION_DEFAULT_EVERYONE'.`,
|
`'HD_GUEST_ACCESS' is set to 'deny', but 'HD_PERMISSIONS_DEFAULT_EVERYONE' is also configured. Please remove 'HD_PERMISSIONS_DEFAULT_EVERYONE'.`,
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to write, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to read', async () => {
|
it('when HD_PERMISSIONS_DEFAULT_EVERYONE is set to write, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to read', async () => {
|
||||||
const restore = mockedEnv(
|
const restore = mockedEnv(
|
||||||
{
|
{
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.WRITE,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.WRITE,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -441,19 +441,19 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessLevel.WRITE}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessLevel.READ}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
|
`'HD_PERMISSIONS_DEFAULT_EVERYONE' is set to '${DefaultAccessLevel.WRITE}', but 'HD_PERMISSIONS_DEFAULT_LOGGED_IN' is set to '${DefaultAccessLevel.READ}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to write, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to none', async () => {
|
it('when HD_PERMISSIONS_DEFAULT_EVERYONE is set to write, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to none', async () => {
|
||||||
const restore = mockedEnv(
|
const restore = mockedEnv(
|
||||||
{
|
{
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.WRITE,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.WRITE,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.NONE,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.NONE,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -462,19 +462,19 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessLevel.WRITE}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessLevel.NONE}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
|
`'HD_PERMISSIONS_DEFAULT_EVERYONE' is set to '${DefaultAccessLevel.WRITE}', but 'HD_PERMISSIONS_DEFAULT_LOGGED_IN' is set to '${DefaultAccessLevel.NONE}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to read, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to none', async () => {
|
it('when HD_PERMISSIONS_DEFAULT_EVERYONE is set to read, but HD_PERMISSIONS_DEFAULT_LOGGED_IN is set to none', async () => {
|
||||||
const restore = mockedEnv(
|
const restore = mockedEnv(
|
||||||
{
|
{
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.NONE,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.NONE,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
},
|
},
|
||||||
|
@ -483,7 +483,7 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessLevel.READ}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessLevel.NONE}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
|
`'HD_PERMISSIONS_DEFAULT_EVERYONE' is set to '${DefaultAccessLevel.READ}', but 'HD_PERMISSIONS_DEFAULT_LOGGED_IN' is set to '${DefaultAccessLevel.NONE}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
@ -494,8 +494,8 @@ describe('noteConfig', () => {
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
|
||||||
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
|
||||||
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_EVERYONE: DefaultAccessLevel.READ,
|
||||||
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
HD_PERMISSIONS_DEFAULT_LOGGED_IN: DefaultAccessLevel.READ,
|
||||||
HD_GUEST_ACCESS: guestAccess,
|
HD_GUEST_ACCESS: guestAccess,
|
||||||
HD_REVISION_RETENTION_DAYS: (-1).toString(),
|
HD_REVISION_RETENTION_DAYS: (-1).toString(),
|
||||||
/* eslint-enable @typescript-eslint/naming-convention */
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
@ -505,7 +505,7 @@ describe('noteConfig', () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(() => noteConfig()).toThrow(
|
expect(() => noteConfig()).toThrow(
|
||||||
'"HD_REVISION_RETENTION_DAYS" must be greater than or equal to 0',
|
'HD_REVISION_RETENTION_DAYS: Number must be greater than or equal to 0',
|
||||||
);
|
);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,72 +5,67 @@
|
||||||
*/
|
*/
|
||||||
import { GuestAccess } from '@hedgedoc/commons';
|
import { GuestAccess } from '@hedgedoc/commons';
|
||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
import * as Joi from 'joi';
|
import z from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DefaultAccessLevel,
|
DefaultAccessLevel,
|
||||||
getDefaultAccessLevelOrdinal,
|
getDefaultAccessLevelOrdinal,
|
||||||
} from './default-access-level.enum';
|
} from './default-access-level.enum';
|
||||||
import { buildErrorMessage, parseOptionalNumber, toArrayConfig } from './utils';
|
import { parseOptionalNumber, toArrayConfig } from './utils';
|
||||||
|
import {
|
||||||
|
buildErrorMessage,
|
||||||
|
extractDescriptionFromZodIssue,
|
||||||
|
} from './zod-error-message';
|
||||||
|
|
||||||
export interface NoteConfig {
|
const schema = z.object({
|
||||||
forbiddenNoteIds: string[];
|
forbiddenNoteIds: z
|
||||||
maxDocumentLength: number;
|
.array(z.string().min(1))
|
||||||
guestAccess: GuestAccess;
|
|
||||||
permissions: {
|
|
||||||
default: {
|
|
||||||
everyone: DefaultAccessLevel;
|
|
||||||
loggedIn: DefaultAccessLevel;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
revisionRetentionDays: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = Joi.object<NoteConfig>({
|
|
||||||
forbiddenNoteIds: Joi.array()
|
|
||||||
.items(Joi.string())
|
|
||||||
.optional()
|
.optional()
|
||||||
.default([])
|
.default([])
|
||||||
.label('HD_FORBIDDEN_NOTE_IDS'),
|
.describe('HD_FORBIDDEN_NOTE_IDS'),
|
||||||
maxDocumentLength: Joi.number()
|
maxDocumentLength: z
|
||||||
.default(100000)
|
.number()
|
||||||
|
.int()
|
||||||
.positive()
|
.positive()
|
||||||
.integer()
|
|
||||||
.optional()
|
.optional()
|
||||||
.label('HD_MAX_DOCUMENT_LENGTH'),
|
.default(100000)
|
||||||
guestAccess: Joi.string()
|
.describe('HD_MAX_DOCUMENT_LENGTH'),
|
||||||
.valid(...Object.values(GuestAccess))
|
guestAccess: z
|
||||||
|
.nativeEnum(GuestAccess)
|
||||||
.optional()
|
.optional()
|
||||||
.default(GuestAccess.WRITE)
|
.default(GuestAccess.WRITE)
|
||||||
.label('HD_GUEST_ACCESS'),
|
.describe('HD_GUEST_ACCESS'),
|
||||||
permissions: {
|
permissions: z.object({
|
||||||
default: {
|
default: z.object({
|
||||||
everyone: Joi.string()
|
everyone: z
|
||||||
.valid(...Object.values(DefaultAccessLevel))
|
.nativeEnum(DefaultAccessLevel)
|
||||||
.optional()
|
.optional()
|
||||||
.default(DefaultAccessLevel.READ)
|
.default(DefaultAccessLevel.READ)
|
||||||
.label('HD_PERMISSION_DEFAULT_EVERYONE'),
|
.describe('HD_PERMISSIONS_DEFAULT_EVERYONE'),
|
||||||
loggedIn: Joi.string()
|
loggedIn: z
|
||||||
.valid(...Object.values(DefaultAccessLevel))
|
.nativeEnum(DefaultAccessLevel)
|
||||||
.optional()
|
.optional()
|
||||||
.default(DefaultAccessLevel.WRITE)
|
.default(DefaultAccessLevel.WRITE)
|
||||||
.label('HD_PERMISSION_DEFAULT_LOGGED_IN'),
|
.describe('HD_PERMISSIONS_DEFAULT_LOGGED_IN'),
|
||||||
},
|
}),
|
||||||
},
|
}),
|
||||||
revisionRetentionDays: Joi.number()
|
revisionRetentionDays: z
|
||||||
.integer()
|
.number()
|
||||||
.default(0)
|
.int()
|
||||||
.min(0)
|
.nonnegative()
|
||||||
.optional()
|
.optional()
|
||||||
.label('HD_REVISION_RETENTION_DAYS'),
|
.default(0)
|
||||||
|
.describe('HD_REVISION_RETENTION_DAYS'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type NoteConfig = z.infer<typeof schema>;
|
||||||
|
|
||||||
function checkEveryoneConfigIsConsistent(config: NoteConfig): void {
|
function checkEveryoneConfigIsConsistent(config: NoteConfig): void {
|
||||||
const everyoneDefaultSet =
|
const everyoneDefaultSet =
|
||||||
process.env.HD_PERMISSION_DEFAULT_EVERYONE !== undefined;
|
process.env.HD_PERMISSIONS_DEFAULT_EVERYONE !== undefined;
|
||||||
if (config.guestAccess === GuestAccess.DENY && everyoneDefaultSet) {
|
if (config.guestAccess === GuestAccess.DENY && everyoneDefaultSet) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`'HD_GUEST_ACCESS' is set to '${config.guestAccess}', but 'HD_PERMISSION_DEFAULT_EVERYONE' is also configured. Please remove 'HD_PERMISSION_DEFAULT_EVERYONE'.`,
|
`'HD_GUEST_ACCESS' is set to '${config.guestAccess}', but 'HD_PERMISSIONS_DEFAULT_EVERYONE' is also configured. Please remove 'HD_PERMISSIONS_DEFAULT_EVERYONE'.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,41 +80,33 @@ function checkLoggedInUsersHaveHigherDefaultPermissionsThanGuests(
|
||||||
getDefaultAccessLevelOrdinal(loggedIn)
|
getDefaultAccessLevelOrdinal(loggedIn)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${everyone}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${loggedIn}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
|
`'HD_PERMISSIONS_DEFAULT_EVERYONE' is set to '${everyone}', but 'HD_PERMISSIONS_DEFAULT_LOGGED_IN' is set to '${loggedIn}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default registerAs('noteConfig', () => {
|
export default registerAs('noteConfig', () => {
|
||||||
const noteConfig = schema.validate(
|
const noteConfig = schema.safeParse({
|
||||||
{
|
forbiddenNoteIds: toArrayConfig(process.env.HD_FORBIDDEN_NOTE_IDS, ','),
|
||||||
forbiddenNoteIds: toArrayConfig(process.env.HD_FORBIDDEN_NOTE_IDS, ','),
|
maxDocumentLength: parseOptionalNumber(process.env.HD_MAX_DOCUMENT_LENGTH),
|
||||||
maxDocumentLength: parseOptionalNumber(
|
guestAccess: process.env.HD_GUEST_ACCESS,
|
||||||
process.env.HD_MAX_DOCUMENT_LENGTH,
|
permissions: {
|
||||||
),
|
default: {
|
||||||
guestAccess: process.env.HD_GUEST_ACCESS,
|
everyone: process.env.HD_PERMISSIONS_DEFAULT_EVERYONE,
|
||||||
permissions: {
|
loggedIn: process.env.HD_PERMISSIONS_DEFAULT_LOGGED_IN,
|
||||||
default: {
|
|
||||||
everyone: process.env.HD_PERMISSION_DEFAULT_EVERYONE,
|
|
||||||
loggedIn: process.env.HD_PERMISSION_DEFAULT_LOGGED_IN,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
revisionRetentionDays: parseOptionalNumber(
|
|
||||||
process.env.HD_REVISION_RETENTION_DAYS,
|
|
||||||
),
|
|
||||||
} as NoteConfig,
|
|
||||||
{
|
|
||||||
abortEarly: false,
|
|
||||||
presence: 'required',
|
|
||||||
},
|
},
|
||||||
);
|
revisionRetentionDays: parseOptionalNumber(
|
||||||
|
process.env.HD_REVISION_RETENTION_DAYS,
|
||||||
|
),
|
||||||
|
});
|
||||||
if (noteConfig.error) {
|
if (noteConfig.error) {
|
||||||
const errorMessages = noteConfig.error.details.map(
|
const errorMessages = noteConfig.error.errors.map((issue) =>
|
||||||
(detail) => detail.message,
|
extractDescriptionFromZodIssue(issue, 'HD'),
|
||||||
);
|
);
|
||||||
throw new Error(buildErrorMessage(errorMessages));
|
throw new Error(buildErrorMessage(errorMessages));
|
||||||
}
|
}
|
||||||
const config = noteConfig.value;
|
const config = noteConfig.data;
|
||||||
checkEveryoneConfigIsConsistent(config);
|
checkEveryoneConfigIsConsistent(config);
|
||||||
checkLoggedInUsersHaveHigherDefaultPermissionsThanGuests(config);
|
checkLoggedInUsersHaveHigherDefaultPermissionsThanGuests(config);
|
||||||
return config;
|
return config;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -10,7 +10,6 @@ import {
|
||||||
needToLog,
|
needToLog,
|
||||||
parseOptionalBoolean,
|
parseOptionalBoolean,
|
||||||
parseOptionalNumber,
|
parseOptionalNumber,
|
||||||
replaceAuthErrorsWithEnvironmentVariables,
|
|
||||||
toArrayConfig,
|
toArrayConfig,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
|
@ -39,13 +38,13 @@ describe('config utils', () => {
|
||||||
});
|
});
|
||||||
it('throws error if there is a duplicate', () => {
|
it('throws error if there is a duplicate', () => {
|
||||||
expect(() => ensureNoDuplicatesExist('Test', ['A', 'A'])).toThrow(
|
expect(() => ensureNoDuplicatesExist('Test', ['A', 'A'])).toThrow(
|
||||||
"Your Test names 'A,A' contain duplicates 'A'",
|
"Your Test names 'A,A' contain duplicates: 'A'",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('throws error if there are multiple duplicates', () => {
|
it('throws error if there are multiple duplicates', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
ensureNoDuplicatesExist('Test', ['A', 'A', 'B', 'B']),
|
ensureNoDuplicatesExist('Test', ['A', 'A', 'B', 'B']),
|
||||||
).toThrow("Your Test names 'A,A,B,B' contain duplicates 'A,B'");
|
).toThrow("Your Test names 'A,A,B,B' contain duplicates: 'A,B'");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('toArrayConfig', () => {
|
describe('toArrayConfig', () => {
|
||||||
|
@ -67,28 +66,6 @@ describe('config utils', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('replaceAuthErrorsWithEnvironmentVariables', () => {
|
|
||||||
it('"ldap[0].url', () => {
|
|
||||||
expect(
|
|
||||||
replaceAuthErrorsWithEnvironmentVariables(
|
|
||||||
'"ldap[0].url',
|
|
||||||
'ldap',
|
|
||||||
'HD_AUTH_LDAP_',
|
|
||||||
['test'],
|
|
||||||
),
|
|
||||||
).toEqual('"HD_AUTH_LDAP_test_URL');
|
|
||||||
});
|
|
||||||
it('"ldap[0].url is not changed by gitlab call', () => {
|
|
||||||
expect(
|
|
||||||
replaceAuthErrorsWithEnvironmentVariables(
|
|
||||||
'"ldap[0].url',
|
|
||||||
'gitlab',
|
|
||||||
'HD_AUTH_GITLAB_',
|
|
||||||
['test'],
|
|
||||||
),
|
|
||||||
).toEqual('"ldap[0].url');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('needToLog', () => {
|
describe('needToLog', () => {
|
||||||
it('currentLevel ERROR', () => {
|
it('currentLevel ERROR', () => {
|
||||||
const currentLevel = Loglevel.ERROR;
|
const currentLevel = Loglevel.ERROR;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -22,7 +22,7 @@ export function ensureNoDuplicatesExist(
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Your ${authName} names '${names.join(
|
`Your ${authName} names '${names.join(
|
||||||
',',
|
',',
|
||||||
)}' contain duplicates '${duplicates.join(',')}'`,
|
)}' contain duplicates: '${duplicates.join(',')}'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,60 +39,6 @@ export function toArrayConfig(
|
||||||
return configValue.split(separator).map((arrayItem) => arrayItem.trim());
|
return configValue.split(separator).map((arrayItem) => arrayItem.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildErrorMessage(errorMessages: string[]): string {
|
|
||||||
let totalErrorMessage = 'There were some errors with your configuration:';
|
|
||||||
for (const message of errorMessages) {
|
|
||||||
totalErrorMessage += '\n - ';
|
|
||||||
totalErrorMessage += message;
|
|
||||||
}
|
|
||||||
totalErrorMessage +=
|
|
||||||
'\nFor further information, have a look at our configuration docs at https://docs.hedgedoc.org/configuration';
|
|
||||||
return totalErrorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaceAuthErrorsWithEnvironmentVariables(
|
|
||||||
message: string,
|
|
||||||
name: string,
|
|
||||||
replacement: string,
|
|
||||||
arrayOfNames: string[],
|
|
||||||
): string {
|
|
||||||
// this builds a regex like /"gitlab\[(\d+)]\./ to extract the position in the arrayOfNames
|
|
||||||
const regex = new RegExp('"' + name + '\\[(\\d+)]\\.', 'g');
|
|
||||||
let newMessage = message.replace(
|
|
||||||
regex,
|
|
||||||
(_, index: number) => `"${replacement}${arrayOfNames[index]}.`,
|
|
||||||
);
|
|
||||||
if (newMessage != message) {
|
|
||||||
newMessage = newMessage.replace('.providerName', '_PROVIDER_NAME');
|
|
||||||
newMessage = newMessage.replace('.baseURL', '_BASE_URL');
|
|
||||||
newMessage = newMessage.replace('.clientID', '_CLIENT_ID');
|
|
||||||
newMessage = newMessage.replace('.url', '_URL');
|
|
||||||
newMessage = newMessage.replace('.clientSecret', '_CLIENT_SECRET');
|
|
||||||
newMessage = newMessage.replace('.bindDn', '_BIND_DN');
|
|
||||||
newMessage = newMessage.replace('.bindCredentials', '_BIND_CREDENTIALS');
|
|
||||||
newMessage = newMessage.replace('.searchBase', '_SEARCH_BASE');
|
|
||||||
newMessage = newMessage.replace('.searchFilter', '_SEARCH_FILTER');
|
|
||||||
newMessage = newMessage.replace('.searchAttributes', '_SEARCH_ATTRIBUTES');
|
|
||||||
newMessage = newMessage.replace('.userIdField', '_USER_ID_FIELD');
|
|
||||||
newMessage = newMessage.replace('.userNameField', '_USER_NAME_FIELD');
|
|
||||||
newMessage = newMessage.replace('.displayNameField', '_DISPLAY_NAME_FIELD');
|
|
||||||
newMessage = newMessage.replace('.emailField', '_EMAIL_FIELD');
|
|
||||||
newMessage = newMessage.replace(
|
|
||||||
'.profilePictureField',
|
|
||||||
'_PROFILE_PICTURE_FIELD',
|
|
||||||
);
|
|
||||||
newMessage = newMessage.replace('.authorizeUrl', '_AUTHORIZE_URL');
|
|
||||||
newMessage = newMessage.replace('.tokenUrl', '_TOKEN_URL');
|
|
||||||
newMessage = newMessage.replace('.userinfoUrl', '_USERINFO_URL');
|
|
||||||
newMessage = newMessage.replace('.endSessionUrl', '_END_SESSION_URL');
|
|
||||||
newMessage = newMessage.replace('.scope', '_SCOPE');
|
|
||||||
newMessage = newMessage.replace('.tlsCaCerts', '_TLS_CERT_PATHS');
|
|
||||||
newMessage = newMessage.replace('.issuer', '_ISSUER');
|
|
||||||
newMessage = newMessage.replace('.theme', '_THEME');
|
|
||||||
}
|
|
||||||
return newMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function needToLog(
|
export function needToLog(
|
||||||
currentLoglevel: Loglevel,
|
currentLoglevel: Loglevel,
|
||||||
requestedLoglevel: Loglevel,
|
requestedLoglevel: Loglevel,
|
||||||
|
|
53
backend/src/config/zod-error-message.spec.ts
Normal file
53
backend/src/config/zod-error-message.spec.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { extractDescriptionFromZodIssue } from './zod-error-message';
|
||||||
|
|
||||||
|
const PREFIX = 'HD_TEST';
|
||||||
|
|
||||||
|
describe('zod error message', () => {
|
||||||
|
describe('extractDescriptionFromZodSchema', () => {
|
||||||
|
it('correctly builds an error message on a simple object', () => {
|
||||||
|
const schema = z.object({
|
||||||
|
port: z.number().describe('port').positive(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = schema.safeParse({
|
||||||
|
port: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.error).toBeDefined();
|
||||||
|
|
||||||
|
const errorMessages = results.error!.errors.map((issue) =>
|
||||||
|
extractDescriptionFromZodIssue(issue, PREFIX),
|
||||||
|
);
|
||||||
|
expect(errorMessages).toHaveLength(1);
|
||||||
|
expect(errorMessages[0]).toEqual(
|
||||||
|
`${PREFIX}_PORT: Number must be greater than 0`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('correctly builds an error message on an array object', () => {
|
||||||
|
const schema = z.object({
|
||||||
|
array: z.array(z.number().positive()).describe('array'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = schema.safeParse({
|
||||||
|
array: [1, -1],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.error).toBeDefined();
|
||||||
|
|
||||||
|
const errorMessages = results.error!.errors.map((issue) =>
|
||||||
|
extractDescriptionFromZodIssue(issue, PREFIX),
|
||||||
|
);
|
||||||
|
expect(errorMessages).toHaveLength(1);
|
||||||
|
expect(errorMessages[0]).toEqual(
|
||||||
|
`${PREFIX}_ARRAY[1]: Number must be greater than 0`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
43
backend/src/config/zod-error-message.ts
Normal file
43
backend/src/config/zod-error-message.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { ZodIssue } from 'zod';
|
||||||
|
|
||||||
|
function camelToSnakeCase(str: string): string {
|
||||||
|
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractDescriptionFromZodIssue(
|
||||||
|
issue: ZodIssue,
|
||||||
|
prefix: string,
|
||||||
|
allArrays?: Record<string, string[]>,
|
||||||
|
): string {
|
||||||
|
let identifier: string = prefix;
|
||||||
|
for (let index = 0; index < issue.path.length; index++) {
|
||||||
|
const pathSegment = issue.path[index];
|
||||||
|
if (typeof pathSegment === 'string') {
|
||||||
|
identifier += '_' + camelToSnakeCase(pathSegment).toUpperCase();
|
||||||
|
} else if (index >= 1) {
|
||||||
|
const previousPathSegment = issue.path[index - 1] as string;
|
||||||
|
if (allArrays && allArrays[previousPathSegment]) {
|
||||||
|
identifier += '_' + allArrays[previousPathSegment][pathSegment];
|
||||||
|
} else {
|
||||||
|
identifier += `[${pathSegment}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${identifier}: ${issue.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildErrorMessage(errorMessages: string[]): string {
|
||||||
|
let totalErrorMessage = 'There were some errors with your configuration:';
|
||||||
|
for (const message of errorMessages) {
|
||||||
|
totalErrorMessage += '\n - ';
|
||||||
|
totalErrorMessage += message;
|
||||||
|
}
|
||||||
|
totalErrorMessage +=
|
||||||
|
'\nFor further information, have a look at our configuration docs at https://docs.hedgedoc.org/configuration\n';
|
||||||
|
return totalErrorMessage;
|
||||||
|
}
|
|
@ -67,11 +67,11 @@ describe('FrontendConfigService', () => {
|
||||||
identifier: 'oidcTestIdentifier',
|
identifier: 'oidcTestIdentifier',
|
||||||
providerName: 'oidcTestProviderName',
|
providerName: 'oidcTestProviderName',
|
||||||
issuer: 'oidcTestIssuer',
|
issuer: 'oidcTestIssuer',
|
||||||
clientID: 'oidcTestId',
|
clientId: 'oidcTestId',
|
||||||
clientSecret: 'oidcTestSecret',
|
clientSecret: 'oidcTestSecret',
|
||||||
scope: 'openid profile email',
|
scope: 'openid profile email',
|
||||||
userIdField: '',
|
userIdField: '',
|
||||||
userNameField: '',
|
usernameField: '',
|
||||||
displayNameField: '',
|
displayNameField: '',
|
||||||
profilePictureField: '',
|
profilePictureField: '',
|
||||||
emailField: '',
|
emailField: '',
|
||||||
|
@ -82,7 +82,7 @@ describe('FrontendConfigService', () => {
|
||||||
const appConfig: AppConfig = {
|
const appConfig: AppConfig = {
|
||||||
baseUrl: domain,
|
baseUrl: domain,
|
||||||
rendererBaseUrl: 'https://renderer.example.org',
|
rendererBaseUrl: 'https://renderer.example.org',
|
||||||
port: 3000,
|
backendPort: 3000,
|
||||||
loglevel: Loglevel.ERROR,
|
loglevel: Loglevel.ERROR,
|
||||||
showLogTimestamp: false,
|
showLogTimestamp: false,
|
||||||
persistInterval: 10,
|
persistInterval: 10,
|
||||||
|
@ -182,7 +182,7 @@ describe('FrontendConfigService', () => {
|
||||||
const appConfig: AppConfig = {
|
const appConfig: AppConfig = {
|
||||||
baseUrl: domain,
|
baseUrl: domain,
|
||||||
rendererBaseUrl: 'https://renderer.example.org',
|
rendererBaseUrl: 'https://renderer.example.org',
|
||||||
port: 3000,
|
backendPort: 3000,
|
||||||
loglevel: Loglevel.ERROR,
|
loglevel: Loglevel.ERROR,
|
||||||
showLogTimestamp: false,
|
showLogTimestamp: false,
|
||||||
persistInterval: 10,
|
persistInterval: 10,
|
||||||
|
@ -207,7 +207,7 @@ describe('FrontendConfigService', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const externalServicesConfig: ExternalServicesConfig = {
|
const externalServicesConfig: ExternalServicesConfig = {
|
||||||
plantUmlServer: plantUmlServer,
|
plantumlServer: plantUmlServer,
|
||||||
imageProxy: imageProxy,
|
imageProxy: imageProxy,
|
||||||
};
|
};
|
||||||
const noteConfig: NoteConfig = {
|
const noteConfig: NoteConfig = {
|
||||||
|
|
|
@ -52,8 +52,8 @@ export class FrontendConfigService {
|
||||||
authProviders: this.getAuthProviders(),
|
authProviders: this.getAuthProviders(),
|
||||||
branding: this.getBranding(),
|
branding: this.getBranding(),
|
||||||
maxDocumentLength: this.noteConfig.maxDocumentLength,
|
maxDocumentLength: this.noteConfig.maxDocumentLength,
|
||||||
plantUmlServer: this.externalServicesConfig.plantUmlServer
|
plantUmlServer: this.externalServicesConfig.plantumlServer
|
||||||
? new URL(this.externalServicesConfig.plantUmlServer).toString()
|
? new URL(this.externalServicesConfig.plantumlServer).toString()
|
||||||
: null,
|
: null,
|
||||||
specialUrls: this.getSpecialUrls(),
|
specialUrls: this.getSpecialUrls(),
|
||||||
useImageProxy: !!this.externalServicesConfig.imageProxy,
|
useImageProxy: !!this.externalServicesConfig.imageProxy,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -44,8 +44,8 @@ async function bootstrap(): Promise<void> {
|
||||||
await setupApp(app, appConfig, authConfig, mediaConfig, logger);
|
await setupApp(app, appConfig, authConfig, mediaConfig, logger);
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
await app.listen(appConfig.port);
|
await app.listen(appConfig.backendPort);
|
||||||
logger.warn(`Listening on port ${appConfig.port}`, 'AppBootstrap');
|
logger.warn(`Listening on port ${appConfig.backendPort}`, 'AppBootstrap');
|
||||||
}
|
}
|
||||||
|
|
||||||
void bootstrap();
|
void bootstrap();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -14,7 +14,10 @@ import {
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { FileTypeResult } from 'file-type';
|
import { FileTypeResult } from 'file-type';
|
||||||
|
|
||||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
import mediaConfiguration, {
|
||||||
|
AzureMediaConfig,
|
||||||
|
MediaConfig,
|
||||||
|
} from '../../config/media.config';
|
||||||
import { MediaBackendError } from '../../errors/errors';
|
import { MediaBackendError } from '../../errors/errors';
|
||||||
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';
|
||||||
|
@ -22,7 +25,7 @@ import { BackendType } from './backend-type.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AzureBackend implements MediaBackend {
|
export class AzureBackend implements MediaBackend {
|
||||||
private config: MediaConfig['backend']['azure'];
|
private config: AzureMediaConfig['azure'];
|
||||||
private client: ContainerClient;
|
private client: ContainerClient;
|
||||||
private readonly credential: StorageSharedKeyCredential;
|
private readonly credential: StorageSharedKeyCredential;
|
||||||
|
|
||||||
|
@ -32,7 +35,7 @@ export class AzureBackend implements MediaBackend {
|
||||||
private mediaConfig: MediaConfig,
|
private mediaConfig: MediaConfig,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(AzureBackend.name);
|
this.logger.setContext(AzureBackend.name);
|
||||||
this.config = this.mediaConfig.backend.azure;
|
this.config = (this.mediaConfig.backend as AzureMediaConfig).azure;
|
||||||
if (this.mediaConfig.backend.use === BackendType.AZURE) {
|
if (this.mediaConfig.backend.use === BackendType.AZURE) {
|
||||||
// only create the client if the backend is configured to azure
|
// only create the client if the backend is configured to azure
|
||||||
const blobServiceClient = BlobServiceClient.fromConnectionString(
|
const blobServiceClient = BlobServiceClient.fromConnectionString(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -8,7 +8,10 @@ import { FileTypeResult } from 'file-type';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
import mediaConfiguration, {
|
||||||
|
FilesystemMediaConfig,
|
||||||
|
MediaConfig,
|
||||||
|
} from '../../config/media.config';
|
||||||
import { MediaBackendError } from '../../errors/errors';
|
import { MediaBackendError } from '../../errors/errors';
|
||||||
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';
|
||||||
|
@ -23,7 +26,9 @@ export class FilesystemBackend implements MediaBackend {
|
||||||
private mediaConfig: MediaConfig,
|
private mediaConfig: MediaConfig,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(FilesystemBackend.name);
|
this.logger.setContext(FilesystemBackend.name);
|
||||||
this.uploadDirectory = this.mediaConfig.backend.filesystem.uploadPath;
|
this.uploadDirectory = (
|
||||||
|
this.mediaConfig.backend as FilesystemMediaConfig
|
||||||
|
).filesystem.uploadPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveFile(
|
async saveFile(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -7,7 +7,10 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import fetch, { Response } from 'node-fetch';
|
import fetch, { Response } from 'node-fetch';
|
||||||
import { URLSearchParams } from 'url';
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
import mediaConfiguration, {
|
||||||
|
ImgurMediaConfig,
|
||||||
|
MediaConfig,
|
||||||
|
} from '../../config/media.config';
|
||||||
import { MediaBackendError } from '../../errors/errors';
|
import { MediaBackendError } from '../../errors/errors';
|
||||||
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';
|
||||||
|
@ -26,7 +29,7 @@ interface ImgurBackendData {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImgurBackend implements MediaBackend {
|
export class ImgurBackend implements MediaBackend {
|
||||||
private config: MediaConfig['backend']['imgur'];
|
private config: ImgurMediaConfig['imgur'];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: ConsoleLoggerService,
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@ -34,7 +37,7 @@ export class ImgurBackend implements MediaBackend {
|
||||||
private mediaConfig: MediaConfig,
|
private mediaConfig: MediaConfig,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(ImgurBackend.name);
|
this.logger.setContext(ImgurBackend.name);
|
||||||
this.config = this.mediaConfig.backend.imgur;
|
this.config = (this.mediaConfig.backend as ImgurMediaConfig).imgur;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveFile(uuid: string, buffer: Buffer): Promise<string> {
|
async saveFile(uuid: string, buffer: Buffer): Promise<string> {
|
||||||
|
@ -46,7 +49,7 @@ export class ImgurBackend implements MediaBackend {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
headers: { Authorization: `Client-ID ${this.config.clientID}` },
|
headers: { Authorization: `Client-ID ${this.config.clientId}` },
|
||||||
})
|
})
|
||||||
.then((res) => ImgurBackend.checkStatus(res))
|
.then((res) => ImgurBackend.checkStatus(res))
|
||||||
.then((res) => res.json())) as UploadResult;
|
.then((res) => res.json())) as UploadResult;
|
||||||
|
@ -80,7 +83,7 @@ export class ImgurBackend implements MediaBackend {
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
headers: { Authorization: `Client-ID ${this.config.clientID}` },
|
headers: { Authorization: `Client-ID ${this.config.clientId}` },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ImgurBackend.checkStatus(result);
|
ImgurBackend.checkStatus(result);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -48,7 +48,7 @@ describe('s3 backend', () => {
|
||||||
accessKeyId: mockedS3AccessKeyId,
|
accessKeyId: mockedS3AccessKeyId,
|
||||||
secretAccessKey: mockedS3SecretAccessKey,
|
secretAccessKey: mockedS3SecretAccessKey,
|
||||||
bucket: mockedS3Bucket,
|
bucket: mockedS3Bucket,
|
||||||
endPoint: endPoint,
|
endpoint: endPoint,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -8,7 +8,10 @@ import { FileTypeResult } from 'file-type';
|
||||||
import { Client } from 'minio';
|
import { Client } from 'minio';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
|
||||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
import mediaConfiguration, {
|
||||||
|
MediaConfig,
|
||||||
|
S3MediaConfig,
|
||||||
|
} from '../../config/media.config';
|
||||||
import { MediaBackendError } from '../../errors/errors';
|
import { MediaBackendError } from '../../errors/errors';
|
||||||
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';
|
||||||
|
@ -16,7 +19,7 @@ import { BackendType } from './backend-type.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class S3Backend implements MediaBackend {
|
export class S3Backend implements MediaBackend {
|
||||||
private config: MediaConfig['backend']['s3'];
|
private config: S3MediaConfig['s3'];
|
||||||
private client: Client;
|
private client: Client;
|
||||||
|
|
||||||
private static determinePort(url: URL): number | undefined {
|
private static determinePort(url: URL): number | undefined {
|
||||||
|
@ -34,7 +37,7 @@ export class S3Backend implements MediaBackend {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.config = this.mediaConfig.backend.s3;
|
this.config = this.mediaConfig.backend.s3;
|
||||||
const url = new URL(this.config.endPoint);
|
const url = new URL(this.config.endpoint);
|
||||||
const isSecure = url.protocol === 'https:';
|
const isSecure = url.protocol === 'https:';
|
||||||
this.client = new Client({
|
this.client = new Client({
|
||||||
endPoint: url.hostname,
|
endPoint: url.hostname,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -8,7 +8,10 @@ import { FileTypeResult } from 'file-type';
|
||||||
import fetch, { Response } from 'node-fetch';
|
import fetch, { Response } from 'node-fetch';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
|
||||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
import mediaConfiguration, {
|
||||||
|
MediaConfig,
|
||||||
|
WebdavMediaConfig,
|
||||||
|
} from '../../config/media.config';
|
||||||
import { MediaBackendError } from '../../errors/errors';
|
import { MediaBackendError } from '../../errors/errors';
|
||||||
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';
|
||||||
|
@ -16,7 +19,7 @@ import { BackendType } from './backend-type.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebdavBackend implements MediaBackend {
|
export class WebdavBackend implements MediaBackend {
|
||||||
private config: MediaConfig['backend']['webdav'];
|
private config: WebdavMediaConfig['webdav'];
|
||||||
private authHeader: string;
|
private authHeader: string;
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,12 @@ We officially support and test these databases:
|
||||||
We don't necessarily support MySQL.
|
We don't necessarily support MySQL.
|
||||||
|
|
||||||
<!-- markdownlint-disable proper-names -->
|
<!-- markdownlint-disable proper-names -->
|
||||||
| environment variable | default | example | description |
|
| environment variable | default | example | description |
|
||||||
|-----------------------|---------|---------------------|----------------------------------------------------------------------------------------|
|
|------------------------|---------|---------------------|----------------------------------------------------------------------------------------|
|
||||||
| `HD_DATABASE_TYPE` | - | `postgres` | The database type you want to use. This can be `postgres`, `mariadb` or `sqlite`. |
|
| `HD_DATABASE_TYPE` | - | `postgres` | The database type you want to use. This can be `postgres`, `mariadb` or `sqlite`. |
|
||||||
| `HD_DATABASE_NAME` | - | `hedgedoc` | The name of the database to use. When using SQLite, this is the path to the database file. |
|
| `HD_DATABASE_NAME` | - | `hedgedoc` | The name of the database to use. When using SQLite, this is the path to the database file. |
|
||||||
| `HD_DATABASE_HOST` | - | `db.example.com` | The host, where the database runs. *Only if you're **not** using `sqlite`.* |
|
| `HD_DATABASE_HOST` | - | `db.example.com` | The host, where the database runs. *Only if you're **not** using `sqlite`.* |
|
||||||
| `HD_DATABASE_PORT` | - | `5432` | The port, where the database runs. *Only if you're **not** using `sqlite`.* |
|
| `HD_DATABASE_PORT` | - | `5432` | The port, where the database runs. *Only if you're **not** using `sqlite`.* |
|
||||||
| `HD_DATABASE_USER` | - | `hedgedoc` | The user that logs in the database. *Only if you're **not** using `sqlite`.* |
|
| `HD_DATABASE_USERNAME` | - | `hedgedoc` | The user that logs in the database. *Only if you're **not** using `sqlite`.* |
|
||||||
| `HD_DATABASE_PASS` | - | `password` | The password to log into the database. *Only if you're **not** using `sqlite`.* |
|
| `HD_DATABASE_PASSWORD` | - | `password` | The password to log into the database. *Only if you're **not** using `sqlite`.* |
|
||||||
<!-- markdownlint-enable proper-names -->
|
<!-- markdownlint-enable proper-names -->
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# Notes
|
# Notes
|
||||||
|
|
||||||
| environment variable | default | example | description |
|
| environment variable | default | example | description |
|
||||||
|-----------------------------------|---------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------------------|---------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed, alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. |
|
| `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed, alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. |
|
||||||
| `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. |
|
| `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. |
|
||||||
| `HD_GUEST_ACCESS` | `write` | `deny`, `read`, `write`, `create` | Defines the maximum access level for guest users to the instance. If guest access is set lower than the "everyone" permission of a note then the note permission will be overridden. |
|
| `HD_GUEST_ACCESS` | `write` | `deny`, `read`, `write`, `create` | Defines the maximum access level for guest users to the instance. If guest access is set lower than the "everyone" permission of a note then the note permission will be overridden. |
|
||||||
| `HD_PERMISSION_DEFAULT_LOGGED_IN` | `write` | `none`, `read`, `write` | The default permission for the "logged-in" group that is set on new notes. |
|
| `HD_PERMISSIONS_DEFAULT_LOGGED_IN` | `write` | `none`, `read`, `write` | The default permission for the "logged-in" group that is set on new notes. |
|
||||||
| `HD_PERMISSION_DEFAULT_EVERYONE` | `read` | `none`, `read`, `write` | The default permission for the "everyone" group (logged-in & guest users), that is set on new notes created by logged-in users. Notes created by guests always set this to "write". |
|
| `HD_PERMISSIONS_DEFAULT_EVERYONE` | `read` | `none`, `read`, `write` | The default permission for the "everyone" group (logged-in & guest users), that is set on new notes created by logged-in users. Notes created by guests always set this to "write". |
|
||||||
| `HD_PERSIST_INTERVAL` | 10 | `0`, `5`, `10`, `20` | The time interval in **minutes** for the periodic note revision creation during realtime editing. `0` deactivates the periodic note revision creation. |
|
| `HD_PERSIST_INTERVAL` | 10 | `0`, `5`, `10`, `20` | The time interval in **minutes** for the periodic note revision creation during realtime editing. `0` deactivates the periodic note revision creation. |
|
||||||
| `HD_REVISION_RETENTION_DAYS` | 0 | | The number of days a revision should be kept. If the config option is not set or set to 0, all revisions will be kept forever. |
|
| `HD_REVISION_RETENTION_DAYS` | 0 | | The number of days a revision should be kept. If the config option is not set or set to 0, all revisions will be kept forever. |
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue