fix(repository): Move backend code into subdirectory

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-10-02 20:10:32 +02:00 committed by David Mehren
parent 86584e705f
commit bf30cbcf48
272 changed files with 87 additions and 67 deletions

View file

@ -0,0 +1,182 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
IsArray,
IsBoolean,
IsNumber,
IsOptional,
IsString,
IsUrl,
ValidateNested,
} from 'class-validator';
import { URL } from 'url';
import { GuestAccess } from '../config/guest_access.enum';
import { ServerVersion } from '../monitoring/server-status.dto';
import { BaseDto } from '../utils/base.dto.';
export enum AuthProviderType {
LOCAL = 'local',
LDAP = 'ldap',
SAML = 'saml',
OAUTH2 = 'oauth2',
GITLAB = 'gitlab',
FACEBOOK = 'facebook',
GITHUB = 'github',
TWITTER = 'twitter',
DROPBOX = 'dropbox',
GOOGLE = 'google',
}
export type AuthProviderTypeWithCustomName =
| AuthProviderType.LDAP
| AuthProviderType.OAUTH2
| AuthProviderType.SAML
| AuthProviderType.GITLAB;
export type AuthProviderTypeWithoutCustomName =
| AuthProviderType.LOCAL
| AuthProviderType.FACEBOOK
| AuthProviderType.GITHUB
| AuthProviderType.TWITTER
| AuthProviderType.DROPBOX
| AuthProviderType.GOOGLE;
export class AuthProviderWithoutCustomNameDto extends BaseDto {
/**
* The type of the auth provider.
*/
@IsString()
type: AuthProviderTypeWithoutCustomName;
}
export class AuthProviderWithCustomNameDto extends BaseDto {
/**
* The type of the auth provider.
*/
@IsString()
type: AuthProviderTypeWithCustomName;
/**
* The identifier with which the auth provider can be called
* @example gitlab-fsorg
*/
@IsString()
identifier: string;
/**
* The name given to the auth provider
* @example GitLab fachschaften.org
*/
@IsString()
providerName: string;
}
export type AuthProviderDto =
| AuthProviderWithCustomNameDto
| AuthProviderWithoutCustomNameDto;
export class BrandingDto extends BaseDto {
/**
* The name to be displayed next to the HedgeDoc logo
* @example ACME Corp
*/
@IsString()
@IsOptional()
name?: string;
/**
* The logo to be displayed next to the HedgeDoc logo
* @example https://md.example.com/logo.png
*/
@IsUrl()
@IsOptional()
logo?: URL;
}
export class SpecialUrlsDto extends BaseDto {
/**
* A link to the privacy notice
* @example https://md.example.com/n/privacy
*/
@IsUrl()
@IsOptional()
privacy?: URL;
/**
* A link to the terms of use
* @example https://md.example.com/n/termsOfUse
*/
@IsUrl()
@IsOptional()
termsOfUse?: URL;
/**
* A link to the imprint
* @example https://md.example.com/n/imprint
*/
@IsUrl()
@IsOptional()
imprint?: URL;
}
export class FrontendConfigDto extends BaseDto {
/**
* Maximum access level for guest users
*/
@IsString()
guestAccess: GuestAccess;
/**
* Are users allowed to register on this instance?
*/
@IsBoolean()
allowRegister: boolean;
/**
* Which auth providers are enabled and how are they configured?
*/
@IsArray()
@ValidateNested({ each: true })
authProviders: AuthProviderDto[];
/**
* Individual branding information
*/
@ValidateNested()
branding: BrandingDto;
/**
* Is an image proxy enabled?
*/
@IsBoolean()
useImageProxy: boolean;
/**
* Links to some special pages
*/
@ValidateNested()
specialUrls: SpecialUrlsDto;
/**
* The version of HedgeDoc
*/
@ValidateNested()
version: ServerVersion;
/**
* The plantUML server that should be used to render.
*/
@IsUrl()
@IsOptional()
plantUmlServer?: URL;
/**
* The maximal length of each document
*/
@IsNumber()
maxDocumentLength: number;
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { LoggerModule } from '../logger/logger.module';
import { FrontendConfigService } from './frontend-config.service';
@Module({
imports: [LoggerModule, ConfigModule],
providers: [FrontendConfigService],
exports: [FrontendConfigService],
})
export class FrontendConfigModule {}

View file

@ -0,0 +1,423 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigModule, registerAs } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { URL } from 'url';
import { AppConfig } from '../config/app.config';
import { AuthConfig } from '../config/auth.config';
import { CustomizationConfig } from '../config/customization.config';
import { DefaultAccessPermission } from '../config/default-access-permission.enum';
import { ExternalServicesConfig } from '../config/external-services.config';
import { GitlabScope, GitlabVersion } from '../config/gitlab.enum';
import { GuestAccess } from '../config/guest_access.enum';
import { Loglevel } from '../config/loglevel.enum';
import { NoteConfig } from '../config/note.config';
import { LoggerModule } from '../logger/logger.module';
import { getServerVersionFromPackageJson } from '../utils/serverVersion';
import { AuthProviderType } from './frontend-config.dto';
import { FrontendConfigService } from './frontend-config.service';
/* eslint-disable
jest/no-conditional-expect
*/
describe('FrontendConfigService', () => {
const domain = 'http://md.example.com';
const emptyAuthConfig: AuthConfig = {
session: {
secret: 'my-secret',
lifetime: 1209600000,
},
local: {
enableLogin: false,
enableRegister: false,
minimalPasswordStrength: 2,
},
facebook: {
clientID: undefined,
clientSecret: undefined,
},
twitter: {
consumerKey: undefined,
consumerSecret: undefined,
},
github: {
clientID: undefined,
clientSecret: undefined,
},
dropbox: {
clientID: undefined,
clientSecret: undefined,
appKey: undefined,
},
google: {
clientID: undefined,
clientSecret: undefined,
apiKey: undefined,
},
gitlab: [],
ldap: [],
saml: [],
oauth2: [],
};
describe('getAuthProviders', () => {
const facebook: AuthConfig['facebook'] = {
clientID: 'facebookTestId',
clientSecret: 'facebookTestSecret',
};
const twitter: AuthConfig['twitter'] = {
consumerKey: 'twitterTestId',
consumerSecret: 'twitterTestSecret',
};
const github: AuthConfig['github'] = {
clientID: 'githubTestId',
clientSecret: 'githubTestSecret',
};
const dropbox: AuthConfig['dropbox'] = {
clientID: 'dropboxTestId',
clientSecret: 'dropboxTestSecret',
appKey: 'dropboxTestKey',
};
const google: AuthConfig['google'] = {
clientID: 'googleTestId',
clientSecret: 'googleTestSecret',
apiKey: 'googleTestKey',
};
const gitlab: AuthConfig['gitlab'] = [
{
identifier: 'gitlabTestIdentifier',
providerName: 'gitlabTestName',
baseURL: 'gitlabTestUrl',
clientID: 'gitlabTestId',
clientSecret: 'gitlabTestSecret',
scope: GitlabScope.API,
version: GitlabVersion.V4,
},
];
const ldap: AuthConfig['ldap'] = [
{
identifier: 'ldapTestIdentifier',
providerName: 'ldapTestName',
url: 'ldapTestUrl',
bindDn: 'ldapTestBindDn',
bindCredentials: 'ldapTestBindCredentials',
searchBase: 'ldapTestSearchBase',
searchFilter: 'ldapTestSearchFilter',
searchAttributes: ['ldapTestSearchAttribute'],
userIdField: 'ldapTestUserId',
displayNameField: 'ldapTestDisplayName',
profilePictureField: 'ldapTestProfilePicture',
tlsCaCerts: ['ldapTestTlsCa'],
},
];
const saml: AuthConfig['saml'] = [
{
identifier: 'samlTestIdentifier',
providerName: 'samlTestName',
idpSsoUrl: 'samlTestUrl',
idpCert: 'samlTestCert',
clientCert: 'samlTestClientCert',
issuer: 'samlTestIssuer',
identifierFormat: 'samlTestUrl',
disableRequestedAuthnContext: 'samlTestUrl',
groupAttribute: 'samlTestUrl',
requiredGroups: ['samlTestUrl'],
externalGroups: ['samlTestUrl'],
attribute: {
id: 'samlTestUrl',
username: 'samlTestUrl',
email: 'samlTestUrl',
},
},
];
const oauth2: AuthConfig['oauth2'] = [
{
identifier: 'oauth2Testidentifier',
providerName: 'oauth2TestName',
baseURL: 'oauth2TestUrl',
userProfileURL: 'oauth2TestProfileUrl',
userProfileIdAttr: 'oauth2TestProfileId',
userProfileUsernameAttr: 'oauth2TestProfileUsername',
userProfileDisplayNameAttr: 'oauth2TestProfileDisplay',
userProfileEmailAttr: 'oauth2TestProfileEmail',
tokenURL: 'oauth2TestTokenUrl',
authorizationURL: 'oauth2TestAuthUrl',
clientID: 'oauth2TestId',
clientSecret: 'oauth2TestSecret',
scope: 'oauth2TestScope',
rolesClaim: 'oauth2TestRoles',
accessRole: 'oauth2TestAccess',
},
];
for (const authConfigConfigured of [
facebook,
twitter,
github,
dropbox,
google,
gitlab,
ldap,
saml,
oauth2,
]) {
it(`works with ${JSON.stringify(authConfigConfigured)}`, async () => {
const appConfig: AppConfig = {
domain: domain,
rendererBaseUrl: 'https://renderer.example.org',
port: 3000,
loglevel: Loglevel.ERROR,
persistInterval: 10,
};
const authConfig: AuthConfig = {
...emptyAuthConfig,
...authConfigConfigured,
};
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
registerAs('appConfig', () => appConfig),
registerAs('authConfig', () => authConfig),
registerAs('customizationConfig', () => {
return { branding: {}, specialUrls: {} };
}),
registerAs('externalServicesConfig', () => {
return {};
}),
registerAs('noteConfig', () => {
return {
forbiddenNoteIds: [],
maxDocumentLength: 200,
guestAccess: GuestAccess.CREATE,
permissions: {
default: {
everyone: DefaultAccessPermission.READ,
loggedIn: DefaultAccessPermission.WRITE,
},
},
} as NoteConfig;
}),
],
}),
LoggerModule,
],
providers: [FrontendConfigService],
}).compile();
const service = module.get(FrontendConfigService);
const config = await service.getFrontendConfig();
if (authConfig.dropbox.clientID) {
expect(config.authProviders).toContainEqual({
type: AuthProviderType.DROPBOX,
});
}
if (authConfig.facebook.clientID) {
expect(config.authProviders).toContainEqual({
type: AuthProviderType.FACEBOOK,
});
}
if (authConfig.google.clientID) {
expect(config.authProviders).toContainEqual({
type: AuthProviderType.GOOGLE,
});
}
if (authConfig.github.clientID) {
expect(config.authProviders).toContainEqual({
type: AuthProviderType.GITHUB,
});
}
if (authConfig.local.enableLogin) {
expect(config.authProviders).toContainEqual({
type: AuthProviderType.LOCAL,
});
}
if (authConfig.twitter.consumerKey) {
expect(config.authProviders).toContainEqual({
type: AuthProviderType.TWITTER,
});
}
expect(
config.authProviders.filter(
(provider) => provider.type === AuthProviderType.GITLAB,
).length,
).toEqual(authConfig.gitlab.length);
expect(
config.authProviders.filter(
(provider) => provider.type === AuthProviderType.LDAP,
).length,
).toEqual(authConfig.ldap.length);
expect(
config.authProviders.filter(
(provider) => provider.type === AuthProviderType.SAML,
).length,
).toEqual(authConfig.saml.length);
expect(
config.authProviders.filter(
(provider) => provider.type === AuthProviderType.OAUTH2,
).length,
).toEqual(authConfig.oauth2.length);
if (authConfig.gitlab.length > 0) {
expect(
config.authProviders.find(
(provider) => provider.type === AuthProviderType.GITLAB,
),
).toEqual({
type: AuthProviderType.GITLAB,
providerName: authConfig.gitlab[0].providerName,
identifier: authConfig.gitlab[0].identifier,
});
}
if (authConfig.ldap.length > 0) {
expect(
config.authProviders.find(
(provider) => provider.type === AuthProviderType.LDAP,
),
).toEqual({
type: AuthProviderType.LDAP,
providerName: authConfig.ldap[0].providerName,
identifier: authConfig.ldap[0].identifier,
});
}
if (authConfig.saml.length > 0) {
expect(
config.authProviders.find(
(provider) => provider.type === AuthProviderType.SAML,
),
).toEqual({
type: AuthProviderType.SAML,
providerName: authConfig.saml[0].providerName,
identifier: authConfig.saml[0].identifier,
});
}
if (authConfig.oauth2.length > 0) {
expect(
config.authProviders.find(
(provider) => provider.type === AuthProviderType.OAUTH2,
),
).toEqual({
type: AuthProviderType.OAUTH2,
providerName: authConfig.oauth2[0].providerName,
identifier: authConfig.oauth2[0].identifier,
});
}
});
}
});
const maxDocumentLength = 100000;
const enableRegister = true;
const imageProxy = 'https://imageProxy.example.com';
const customName = 'Test Branding Name';
let index = 1;
for (const customLogo of [undefined, 'https://example.com/logo.png']) {
for (const privacyLink of [undefined, 'https://example.com/privacy']) {
for (const termsOfUseLink of [undefined, 'https://example.com/terms']) {
for (const imprintLink of [undefined, 'https://example.com/imprint']) {
for (const plantUmlServer of [
undefined,
'https://plantuml.example.com',
]) {
it(`combination #${index} works`, async () => {
const appConfig: AppConfig = {
domain: domain,
rendererBaseUrl: 'https://renderer.example.org',
port: 3000,
loglevel: Loglevel.ERROR,
persistInterval: 10,
};
const authConfig: AuthConfig = {
...emptyAuthConfig,
local: {
enableLogin: true,
enableRegister,
minimalPasswordStrength: 3,
},
};
const customizationConfig: CustomizationConfig = {
branding: {
customName: customName,
customLogo: customLogo,
},
specialUrls: {
privacy: privacyLink,
termsOfUse: termsOfUseLink,
imprint: imprintLink,
},
};
const externalServicesConfig: ExternalServicesConfig = {
plantUmlServer: plantUmlServer,
imageProxy: imageProxy,
};
const noteConfig: NoteConfig = {
forbiddenNoteIds: [],
maxDocumentLength: maxDocumentLength,
guestAccess: GuestAccess.CREATE,
permissions: {
default: {
everyone: DefaultAccessPermission.READ,
loggedIn: DefaultAccessPermission.WRITE,
},
},
};
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
registerAs('appConfig', () => appConfig),
registerAs('authConfig', () => authConfig),
registerAs(
'customizationConfig',
() => customizationConfig,
),
registerAs(
'externalServicesConfig',
() => externalServicesConfig,
),
registerAs('noteConfig', () => noteConfig),
],
}),
LoggerModule,
],
providers: [FrontendConfigService],
}).compile();
const service = module.get(FrontendConfigService);
const config = await service.getFrontendConfig();
expect(config.allowRegister).toEqual(enableRegister);
expect(config.guestAccess).toEqual(noteConfig.guestAccess);
expect(config.branding.name).toEqual(customName);
expect(config.branding.logo).toEqual(
customLogo ? new URL(customLogo) : undefined,
);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.plantUmlServer).toEqual(
plantUmlServer ? new URL(plantUmlServer) : undefined,
);
expect(config.specialUrls.imprint).toEqual(
imprintLink ? new URL(imprintLink) : undefined,
);
expect(config.specialUrls.privacy).toEqual(
privacyLink ? new URL(privacyLink) : undefined,
);
expect(config.specialUrls.termsOfUse).toEqual(
termsOfUseLink ? new URL(termsOfUseLink) : undefined,
);
expect(config.useImageProxy).toEqual(!!imageProxy);
expect(config.version).toEqual(
await getServerVersionFromPackageJson(),
);
});
index += 1;
}
}
}
}
}
});

View file

@ -0,0 +1,147 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { URL } from 'url';
import appConfiguration, { AppConfig } from '../config/app.config';
import authConfiguration, { AuthConfig } from '../config/auth.config';
import customizationConfiguration, {
CustomizationConfig,
} from '../config/customization.config';
import externalServicesConfiguration, {
ExternalServicesConfig,
} from '../config/external-services.config';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { getServerVersionFromPackageJson } from '../utils/serverVersion';
import {
AuthProviderDto,
AuthProviderType,
BrandingDto,
FrontendConfigDto,
SpecialUrlsDto,
} from './frontend-config.dto';
@Injectable()
export class FrontendConfigService {
constructor(
private readonly logger: ConsoleLoggerService,
@Inject(appConfiguration.KEY)
private appConfig: AppConfig,
@Inject(noteConfiguration.KEY)
private noteConfig: NoteConfig,
@Inject(authConfiguration.KEY)
private authConfig: AuthConfig,
@Inject(customizationConfiguration.KEY)
private customizationConfig: CustomizationConfig,
@Inject(externalServicesConfiguration.KEY)
private externalServicesConfig: ExternalServicesConfig,
) {
this.logger.setContext(FrontendConfigService.name);
}
async getFrontendConfig(): Promise<FrontendConfigDto> {
return {
guestAccess: this.noteConfig.guestAccess,
allowRegister: this.authConfig.local.enableRegister,
authProviders: this.getAuthProviders(),
branding: this.getBranding(),
maxDocumentLength: this.noteConfig.maxDocumentLength,
plantUmlServer: this.externalServicesConfig.plantUmlServer
? new URL(this.externalServicesConfig.plantUmlServer)
: undefined,
specialUrls: this.getSpecialUrls(),
useImageProxy: !!this.externalServicesConfig.imageProxy,
version: await getServerVersionFromPackageJson(),
};
}
private getAuthProviders(): AuthProviderDto[] {
const providers: AuthProviderDto[] = [];
if (this.authConfig.local.enableLogin) {
providers.push({
type: AuthProviderType.LOCAL,
});
}
if (this.authConfig.dropbox.clientID) {
providers.push({
type: AuthProviderType.DROPBOX,
});
}
if (this.authConfig.facebook.clientID) {
providers.push({
type: AuthProviderType.FACEBOOK,
});
}
if (this.authConfig.github.clientID) {
providers.push({
type: AuthProviderType.GITHUB,
});
}
if (this.authConfig.google.clientID) {
providers.push({
type: AuthProviderType.GOOGLE,
});
}
if (this.authConfig.twitter.consumerKey) {
providers.push({
type: AuthProviderType.TWITTER,
});
}
this.authConfig.gitlab.forEach((gitLabEntry) => {
providers.push({
type: AuthProviderType.GITLAB,
providerName: gitLabEntry.providerName,
identifier: gitLabEntry.identifier,
});
});
this.authConfig.ldap.forEach((ldapEntry) => {
providers.push({
type: AuthProviderType.LDAP,
providerName: ldapEntry.providerName,
identifier: ldapEntry.identifier,
});
});
this.authConfig.oauth2.forEach((oauth2Entry) => {
providers.push({
type: AuthProviderType.OAUTH2,
providerName: oauth2Entry.providerName,
identifier: oauth2Entry.identifier,
});
});
this.authConfig.saml.forEach((samlEntry) => {
providers.push({
type: AuthProviderType.SAML,
providerName: samlEntry.providerName,
identifier: samlEntry.identifier,
});
});
return providers;
}
private getBranding(): BrandingDto {
return {
logo: this.customizationConfig.branding.customLogo
? new URL(this.customizationConfig.branding.customLogo)
: undefined,
name: this.customizationConfig.branding.customName,
};
}
private getSpecialUrls(): SpecialUrlsDto {
return {
imprint: this.customizationConfig.specialUrls.imprint
? new URL(this.customizationConfig.specialUrls.imprint)
: undefined,
privacy: this.customizationConfig.specialUrls.privacy
? new URL(this.customizationConfig.specialUrls.privacy)
: undefined,
termsOfUse: this.customizationConfig.specialUrls.termsOfUse
? new URL(this.customizationConfig.specialUrls.termsOfUse)
: undefined,
};
}
}