mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-10-19 22:00:15 -04:00
Merge pull request #549 from hedgedoc/feat/config
This commit is contained in:
commit
58fc65d65c
24 changed files with 912 additions and 33 deletions
|
@ -25,6 +25,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "7.6.5",
|
"@nestjs/common": "7.6.5",
|
||||||
|
"@nestjs/config": "^0.6.1",
|
||||||
"@nestjs/core": "7.6.5",
|
"@nestjs/core": "7.6.5",
|
||||||
"@nestjs/platform-express": "7.6.5",
|
"@nestjs/platform-express": "7.6.5",
|
||||||
"@nestjs/swagger": "4.7.10",
|
"@nestjs/swagger": "4.7.10",
|
||||||
|
@ -34,6 +35,7 @@
|
||||||
"cli-color": "2.0.0",
|
"cli-color": "2.0.0",
|
||||||
"connect-typeorm": "1.1.4",
|
"connect-typeorm": "1.1.4",
|
||||||
"file-type": "16.2.0",
|
"file-type": "16.2.0",
|
||||||
|
"joi": "^17.3.0",
|
||||||
"raw-body": "2.4.1",
|
"raw-body": "2.4.1",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import appConfigMock from '../../../config/app.config.mock';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
import { MediaUpload } from '../../../media/media-upload.entity';
|
import { MediaUpload } from '../../../media/media-upload.entity';
|
||||||
import { MediaModule } from '../../../media/media.module';
|
import { MediaModule } from '../../../media/media.module';
|
||||||
|
@ -26,7 +28,15 @@ describe('Media Controller', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [MediaController],
|
controllers: [MediaController],
|
||||||
imports: [LoggerModule, MediaModule, NotesModule],
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfigMock],
|
||||||
|
}),
|
||||||
|
LoggerModule,
|
||||||
|
MediaModule,
|
||||||
|
NotesModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.overrideProvider(getRepositoryToken(AuthorColor))
|
.overrideProvider(getRepositoryToken(AuthorColor))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { PublicApiModule } from './api/public/public-api.module';
|
import { PublicApiModule } from './api/public/public-api.module';
|
||||||
import { AuthorsModule } from './authors/authors.module';
|
import { AuthorsModule } from './authors/authors.module';
|
||||||
|
@ -17,6 +18,7 @@ import { NotesModule } from './notes/notes.module';
|
||||||
import { PermissionsModule } from './permissions/permissions.module';
|
import { PermissionsModule } from './permissions/permissions.module';
|
||||||
import { RevisionsModule } from './revisions/revisions.module';
|
import { RevisionsModule } from './revisions/revisions.module';
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
|
import appConfig from './config/app.config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -26,6 +28,10 @@ import { UsersModule } from './users/users.module';
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
}),
|
}),
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
load: [appConfig],
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
NotesModule,
|
NotesModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
RevisionsModule,
|
RevisionsModule,
|
||||||
|
|
19
src/config/app.config.mock.ts
Normal file
19
src/config/app.config.mock.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('appConfig', () => ({
|
||||||
|
port: 3000,
|
||||||
|
media: {
|
||||||
|
backend: {
|
||||||
|
use: 'filesystem',
|
||||||
|
filesystem: {
|
||||||
|
uploadPath: 'uploads',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
125
src/config/app.config.ts
Normal file
125
src/config/app.config.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { Loglevel } from './loglevel.enum';
|
||||||
|
import { appConfigHsts, HstsConfig, hstsSchema } from './hsts-config';
|
||||||
|
import { appConfigCsp, CspConfig, cspSchema } from './csp-config';
|
||||||
|
import { appConfigMedia, MediaConfig, mediaSchema } from './media-config';
|
||||||
|
import {
|
||||||
|
appConfigDatabase,
|
||||||
|
DatabaseConfig,
|
||||||
|
databaseSchema,
|
||||||
|
} from './database-config';
|
||||||
|
import { appConfigAuth, AuthConfig, authSchema } from './auth-config';
|
||||||
|
|
||||||
|
// import { LinkifyHeaderStyle } from './linkify-header-style';
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
domain: string;
|
||||||
|
port: number;
|
||||||
|
loglevel: Loglevel;
|
||||||
|
/*linkifyHeaderStyle: LinkifyHeaderStyle;
|
||||||
|
sourceURL: string;
|
||||||
|
urlPath: string;
|
||||||
|
host: string;
|
||||||
|
path: string;
|
||||||
|
urlAddPort: boolean;
|
||||||
|
cookiePolicy: string;
|
||||||
|
protocolUseSSL: boolean;
|
||||||
|
allowOrigin: string[];
|
||||||
|
useCDN: boolean;
|
||||||
|
enableAnonymous: boolean;
|
||||||
|
enableAnonymousEdits: boolean;
|
||||||
|
enableFreeURL: boolean;
|
||||||
|
forbiddenNoteIDs: string[];
|
||||||
|
defaultPermission: string;
|
||||||
|
sessionSecret: string;
|
||||||
|
sessionLife: number;
|
||||||
|
tooBusyLag: number;
|
||||||
|
enableGravatar: boolean;*/
|
||||||
|
hsts: HstsConfig;
|
||||||
|
csp: CspConfig;
|
||||||
|
media: MediaConfig;
|
||||||
|
database: DatabaseConfig;
|
||||||
|
auth: AuthConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = Joi.object({
|
||||||
|
domain: Joi.string(),
|
||||||
|
port: Joi.number().default(3000).optional(),
|
||||||
|
loglevel: Joi.string()
|
||||||
|
.valid(...Object.values(Loglevel))
|
||||||
|
.default(Loglevel.WARN)
|
||||||
|
.optional(),
|
||||||
|
/*linkifyHeaderStyle: Joi.string().valid(...Object.values(LinkifyHeaderStyle)).default(LinkifyHeaderStyle.GFM).optional(),
|
||||||
|
sourceURL: Joi.string(),
|
||||||
|
urlPath: Joi.string(),
|
||||||
|
host: Joi.string().default('::').optional(),
|
||||||
|
path: Joi.string(),
|
||||||
|
urlAddPort: Joi.boolean().default(false).optional(),
|
||||||
|
cookiePolicy: Joi.string(),
|
||||||
|
protocolUseSSL: Joi.boolean().default(true).optional(),
|
||||||
|
allowOrigin: Joi.array().items(Joi.string()),
|
||||||
|
useCDN: Joi.boolean().default(false).optional(),
|
||||||
|
enableAnonymous: Joi.boolean().default(true).optional(),
|
||||||
|
enableAnonymousEdits: Joi.boolean().default(false).optional(),
|
||||||
|
enableFreeURL: Joi.boolean().default(false).optional(),
|
||||||
|
forbiddenNoteIDs: Joi.array().items(Joi.string()),
|
||||||
|
defaultPermission: Joi.string(),
|
||||||
|
sessionSecret: Joi.string(),
|
||||||
|
sessionLife: Joi.number().default(14 * 24 * 60 * 60 * 1000).optional(),
|
||||||
|
tooBusyLag: Joi.number().default(70).optional(),
|
||||||
|
enableGravatar: Joi.boolean().default(true).optional(),*/
|
||||||
|
hsts: hstsSchema,
|
||||||
|
csp: cspSchema,
|
||||||
|
media: mediaSchema,
|
||||||
|
database: databaseSchema,
|
||||||
|
auth: authSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default registerAs('appConfig', async () => {
|
||||||
|
const appConfig = schema.validate(
|
||||||
|
{
|
||||||
|
domain: process.env.HD_DOMAIN,
|
||||||
|
port: parseInt(process.env.PORT) || undefined,
|
||||||
|
loglevel: process.env.HD_LOGLEVEL, //|| Loglevel.WARN,
|
||||||
|
/*linkifyHeaderStyle: process.env.HD_LINKIFY_HEADER_STYLE,
|
||||||
|
sourceURL: process.env.HD_SOURCE_URL,
|
||||||
|
urlPath: process.env.HD_URL_PATH,
|
||||||
|
host: process.env.HD_HOST || '::',
|
||||||
|
path: process.env.HD_PATH,
|
||||||
|
urlAddPort: process.env.HD_URL_ADDPORT,
|
||||||
|
cookiePolicy: process.env.HD_COOKIE_POLICY,
|
||||||
|
protocolUseSSL: process.env.HD_PROTOCOL_USESSL || true,
|
||||||
|
allowOrigin: toArrayConfig(process.env.HD_ALLOW_ORIGIN),
|
||||||
|
useCDN: process.env.HD_USECDN,
|
||||||
|
enableAnonymous: process.env.HD_ENABLE_ANONYMOUS || true,
|
||||||
|
enableAnonymousEdits: process.env.HD_ENABLE_ANONYMOUS_EDITS,
|
||||||
|
enableFreeURL: process.env.HD_ENABLE_FREEURL,
|
||||||
|
forbiddenNoteIDs: toArrayConfig(process.env.HD_FORBIDDEN_NOTE_IDS),
|
||||||
|
defaultPermission: process.env.HD_DEFAULT_PERMISSION,
|
||||||
|
sessionSecret: process.env.HD_SESSION_SECRET,
|
||||||
|
sessionLife: parseInt(process.env.HD_SESSION_LIFE) || 14 * 24 * 60 * 60 * 1000,
|
||||||
|
tooBusyLag: parseInt(process.env.HD_TOOBUSY_LAG) || 70,
|
||||||
|
enableGravatar: process.env.HD_ENABLE_GRAVATAR || true,*/
|
||||||
|
hsts: appConfigHsts,
|
||||||
|
csp: appConfigCsp,
|
||||||
|
media: appConfigMedia,
|
||||||
|
database: appConfigDatabase,
|
||||||
|
auth: appConfigAuth,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
abortEarly: false,
|
||||||
|
presence: 'required',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (appConfig.error) {
|
||||||
|
throw new Error(appConfig.error.toString());
|
||||||
|
}
|
||||||
|
return appConfig.value;
|
||||||
|
});
|
331
src/config/auth-config.ts
Normal file
331
src/config/auth-config.ts
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { GitlabScope, GitlabVersion } from './gitlab.enum';
|
||||||
|
import { toArrayConfig } from './utils';
|
||||||
|
|
||||||
|
export interface AuthConfig {
|
||||||
|
email: {
|
||||||
|
enableLogin: boolean;
|
||||||
|
enableRegister: boolean;
|
||||||
|
};
|
||||||
|
facebook: {
|
||||||
|
clientID: string;
|
||||||
|
clientSecret: string;
|
||||||
|
};
|
||||||
|
twitter: {
|
||||||
|
consumerKey: string;
|
||||||
|
consumerSecret: string;
|
||||||
|
};
|
||||||
|
github: {
|
||||||
|
clientID: string;
|
||||||
|
clientSecret: string;
|
||||||
|
};
|
||||||
|
dropbox: {
|
||||||
|
clientID: string;
|
||||||
|
clientSecret: string;
|
||||||
|
appKey: string;
|
||||||
|
};
|
||||||
|
google: {
|
||||||
|
clientID: string;
|
||||||
|
clientSecret: string;
|
||||||
|
apiKey: string;
|
||||||
|
};
|
||||||
|
gitlab: [
|
||||||
|
{
|
||||||
|
providerName: string;
|
||||||
|
baseURL: string;
|
||||||
|
clientID: string;
|
||||||
|
clientSecret: string;
|
||||||
|
scope: GitlabScope;
|
||||||
|
version: GitlabVersion;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// ToDo: tlsOptions exist in config.json.example. See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
|
||||||
|
ldap: [
|
||||||
|
{
|
||||||
|
providerName: string;
|
||||||
|
url: string;
|
||||||
|
bindDn: string;
|
||||||
|
bindCredentials: string;
|
||||||
|
searchBase: string;
|
||||||
|
searchFilter: string;
|
||||||
|
searchAttributes: string[];
|
||||||
|
usernameField: string;
|
||||||
|
useridField: string;
|
||||||
|
tlsCa: string[];
|
||||||
|
},
|
||||||
|
];
|
||||||
|
saml: [
|
||||||
|
{
|
||||||
|
providerName: string;
|
||||||
|
idpSsoUrl: string;
|
||||||
|
idpCert: string;
|
||||||
|
clientCert: string;
|
||||||
|
issuer: string;
|
||||||
|
identifierFormat: string;
|
||||||
|
disableRequestedAuthnContext: string;
|
||||||
|
groupAttribute: string;
|
||||||
|
requiredGroups: string[];
|
||||||
|
externalGroups: string;
|
||||||
|
attribute: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
];
|
||||||
|
oauth2: [
|
||||||
|
{
|
||||||
|
providerName: string;
|
||||||
|
baseURL: string;
|
||||||
|
userProfileURL: string;
|
||||||
|
userProfileIdAttr: string;
|
||||||
|
userProfileUsernameAttr: string;
|
||||||
|
userProfileDisplayNameAttr: string;
|
||||||
|
userProfileEmailAttr: string;
|
||||||
|
tokenURL: string;
|
||||||
|
authorizationURL: string;
|
||||||
|
clientID: string;
|
||||||
|
clientSecret: string;
|
||||||
|
scope: string;
|
||||||
|
rolesClaim: string;
|
||||||
|
accessRole: string;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authSchema = Joi.object({
|
||||||
|
email: {
|
||||||
|
enableLogin: Joi.boolean().default(false).optional(),
|
||||||
|
enableRegister: Joi.boolean().default(false).optional(),
|
||||||
|
},
|
||||||
|
facebook: {
|
||||||
|
clientID: Joi.string().optional(),
|
||||||
|
clientSecret: Joi.string().optional(),
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
consumerKey: Joi.string().optional(),
|
||||||
|
consumerSecret: Joi.string().optional(),
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
clientID: Joi.string().optional(),
|
||||||
|
clientSecret: Joi.string().optional(),
|
||||||
|
},
|
||||||
|
dropbox: {
|
||||||
|
clientID: Joi.string().optional(),
|
||||||
|
clientSecret: Joi.string().optional(),
|
||||||
|
appKey: Joi.string().optional(),
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
clientID: Joi.string().optional(),
|
||||||
|
clientSecret: Joi.string().optional(),
|
||||||
|
apiKey: Joi.string().optional(),
|
||||||
|
},
|
||||||
|
gitlab: Joi.array()
|
||||||
|
.items(
|
||||||
|
Joi.object({
|
||||||
|
providerName: Joi.string().default('Gitlab').optional(),
|
||||||
|
baseURL: Joi.string().optional(),
|
||||||
|
clientID: Joi.string().optional(),
|
||||||
|
clientSecret: Joi.string().optional(),
|
||||||
|
scope: Joi.string()
|
||||||
|
.valid(...Object.values(GitlabScope))
|
||||||
|
.default(GitlabScope.READ_USER)
|
||||||
|
.optional(),
|
||||||
|
version: Joi.string()
|
||||||
|
.valid(...Object.values(GitlabVersion))
|
||||||
|
.default(GitlabVersion.V4)
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
// ToDo: should searchfilter have a default?
|
||||||
|
ldap: Joi.array()
|
||||||
|
.items(
|
||||||
|
Joi.object({
|
||||||
|
providerName: Joi.string().default('LDAP').optional(),
|
||||||
|
url: Joi.string().optional(),
|
||||||
|
bindDn: Joi.string().optional(),
|
||||||
|
bindCredentials: Joi.string().optional(),
|
||||||
|
searchBase: Joi.string().optional(),
|
||||||
|
searchFilter: Joi.string().default('(uid={{username}})').optional(),
|
||||||
|
searchAttributes: Joi.array().items(Joi.string()),
|
||||||
|
usernameField: Joi.string().default('userid').optional(),
|
||||||
|
useridField: Joi.string().optional(),
|
||||||
|
tlsCa: Joi.array().items(Joi.string()),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
saml: Joi.array()
|
||||||
|
.items(
|
||||||
|
Joi.object({
|
||||||
|
providerName: Joi.string().default('SAML').optional(),
|
||||||
|
idpSsoUrl: Joi.string().optional(),
|
||||||
|
idpCert: Joi.string().optional(),
|
||||||
|
clientCert: Joi.string().optional(),
|
||||||
|
// ToDo: (default: config.serverURL) will be build on-the-fly in the config/index.js from domain, urlAddPort and urlPath.
|
||||||
|
issuer: Joi.string().optional(), //.default().optional(),
|
||||||
|
identifierFormat: Joi.string()
|
||||||
|
.default('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')
|
||||||
|
.optional(),
|
||||||
|
disableRequestedAuthnContext: Joi.boolean().default(false).optional(),
|
||||||
|
groupAttribute: Joi.string().optional(),
|
||||||
|
requiredGroups: Joi.array().items(Joi.string()),
|
||||||
|
externalGroups: Joi.array().items(Joi.string()),
|
||||||
|
attribute: {
|
||||||
|
id: Joi.string().default('NameId').optional(),
|
||||||
|
username: Joi.string().default('NameId').optional(),
|
||||||
|
email: Joi.string().default('NameId').optional(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
oauth2: Joi.array()
|
||||||
|
.items(
|
||||||
|
Joi.object({
|
||||||
|
providerName: Joi.string().default('OAuth2').optional(),
|
||||||
|
baseURL: Joi.string().optional(),
|
||||||
|
userProfileURL: Joi.string().optional(),
|
||||||
|
userProfileIdAttr: Joi.string().optional(),
|
||||||
|
userProfileUsernameAttr: Joi.string().optional(),
|
||||||
|
userProfileDisplayNameAttr: Joi.string().optional(),
|
||||||
|
userProfileEmailAttr: Joi.string().optional(),
|
||||||
|
tokenURL: Joi.string().optional(),
|
||||||
|
authorizationURL: Joi.string().optional(),
|
||||||
|
clientID: Joi.string().optional(),
|
||||||
|
clientSecret: Joi.string().optional(),
|
||||||
|
scope: Joi.string().optional(),
|
||||||
|
rolesClaim: Joi.string().optional(),
|
||||||
|
accessRole: Joi.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ToDo: Validate these with Joi to prevent duplicate entries?
|
||||||
|
|
||||||
|
const gitlabNames = toArrayConfig(process.env.HD_AUTH_GITLABS, ',');
|
||||||
|
const ldapNames = toArrayConfig(process.env.HD_AUTH_LDAPS, ',');
|
||||||
|
const samlNames = toArrayConfig(process.env.HD_AUTH_SAMLS, ',');
|
||||||
|
const oauth2Names = toArrayConfig(process.env.HD_AUTH_OAUTH2S, ',');
|
||||||
|
|
||||||
|
const gitlabs = gitlabNames.map((gitlabName) => {
|
||||||
|
return {
|
||||||
|
providerName: process.env[`HD_AUTH_GITLAB_${gitlabName}_PROVIDER_NAME`],
|
||||||
|
baseURL: process.env[`HD_AUTH_GITLAB_${gitlabName}_BASE_URL`],
|
||||||
|
clientID: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_ID`],
|
||||||
|
clientSecret: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_SECRET`],
|
||||||
|
scope: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_SCOPE`],
|
||||||
|
version: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_VERSION`],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const ldaps = ldapNames.map((ldapName) => {
|
||||||
|
return {
|
||||||
|
providerName: process.env[`HD_AUTH_LDAP_${ldapName}_PROVIDER_NAME`],
|
||||||
|
url: process.env[`HD_AUTH_LDAP_${ldapName}_URL`],
|
||||||
|
bindDn: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_DN`],
|
||||||
|
bindCredentials: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_CREDENTIALS`],
|
||||||
|
searchBase: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_BASE`],
|
||||||
|
searchFilter: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_FILTER`],
|
||||||
|
searchAttributes: toArrayConfig(
|
||||||
|
process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_ATTRIBUTES`],
|
||||||
|
',',
|
||||||
|
),
|
||||||
|
usernameField: process.env[`HD_AUTH_LDAP_${ldapName}_USERNAME_FIELD`],
|
||||||
|
useridField: process.env[`HD_AUTH_LDAP_${ldapName}_USERID_FIELD`],
|
||||||
|
tlsCa: toArrayConfig(process.env[`HD_AUTH_LDAP_${ldapName}_TLS_CA`], ','),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const samls = samlNames.map((samlName) => {
|
||||||
|
return {
|
||||||
|
providerName: process.env[`HD_AUTH_SAML_${samlName}_PROVIDER_NAME`],
|
||||||
|
idpSsoUrl: process.env[`HD_AUTH_SAML_${samlName}_IDPSSOURL`],
|
||||||
|
idpCert: process.env[`HD_AUTH_SAML_${samlName}_IDPCERT`],
|
||||||
|
clientCert: process.env[`HD_AUTH_SAML_${samlName}_CLIENTCERT`],
|
||||||
|
issuer: process.env[`HD_AUTH_SAML_${samlName}_ISSUER`],
|
||||||
|
identifierFormat: process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIERFORMAT`],
|
||||||
|
disableRequestedAuthnContext:
|
||||||
|
process.env[`HD_AUTH_SAML_${samlName}_DISABLEREQUESTEDAUTHNCONTEXT`],
|
||||||
|
groupAttribute: process.env[`HD_AUTH_SAML_${samlName}_GROUPATTRIBUTE`],
|
||||||
|
requiredGroups: toArrayConfig(
|
||||||
|
process.env[`HD_AUTH_SAML_${samlName}_REQUIREDGROUPS`],
|
||||||
|
'|',
|
||||||
|
),
|
||||||
|
externalGroups: toArrayConfig(
|
||||||
|
process.env[`HD_AUTH_SAML_${samlName}_EXTERNALGROUPS`],
|
||||||
|
'|',
|
||||||
|
),
|
||||||
|
attribute: {
|
||||||
|
id: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_ID`],
|
||||||
|
username: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
|
||||||
|
email: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const oauth2s = oauth2Names.map((oauth2Name) => {
|
||||||
|
return {
|
||||||
|
providerName: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_PROVIDER_NAME`],
|
||||||
|
baseURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_BASEURL`],
|
||||||
|
userProfileURL:
|
||||||
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_URL`],
|
||||||
|
userProfileIdAttr:
|
||||||
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_ID_ATTR`],
|
||||||
|
userProfileUsernameAttr:
|
||||||
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_USERNAME_ATTR`],
|
||||||
|
userProfileDisplayNameAttr:
|
||||||
|
process.env[
|
||||||
|
`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_DISPLAY_NAME_ATTR`
|
||||||
|
],
|
||||||
|
userProfileEmailAttr:
|
||||||
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_EMAIL_ATTR`],
|
||||||
|
tokenURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_TOKEN_URL`],
|
||||||
|
authorizationURL:
|
||||||
|
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_AUTHORIZATION_URL`],
|
||||||
|
clientID: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_ID`],
|
||||||
|
clientSecret: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_SECRET`],
|
||||||
|
scope: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_SCOPE`],
|
||||||
|
rolesClaim: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ROLES_CLAIM`],
|
||||||
|
accessRole: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ACCESS_ROLE`],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appConfigAuth = {
|
||||||
|
email: {
|
||||||
|
enableLogin: process.env.HD_AUTH_EMAIL_ENABLE_LOGIN,
|
||||||
|
enableRegister: process.env.HD_AUTH_EMAIL_ENABLE_REGISTER,
|
||||||
|
},
|
||||||
|
facebook: {
|
||||||
|
clientID: process.env.HD_AUTH_FACEBOOK_CLIENT_ID,
|
||||||
|
clientSecret: process.env.HD_AUTH_FACEBOOK_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
consumerKey: process.env.HD_AUTH_TWITTER_CONSUMER_KEY,
|
||||||
|
consumerSecret: process.env.HD_AUTH_TWITTER_CONSUMER_SECRET,
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
clientID: process.env.HD_AUTH_GITHUB_CLIENT_ID,
|
||||||
|
clientSecret: process.env.HD_AUTH_GITHUB_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
dropbox: {
|
||||||
|
clientID: process.env.HD_AUTH_DROPBOX_CLIENT_ID,
|
||||||
|
clientSecret: process.env.HD_AUTH_DROPBOX_CLIENT_SECRET,
|
||||||
|
appKey: process.env.HD_AUTH_DROPBOX_APP_KEY,
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
clientID: process.env.HD_AUTH_GOOGLE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.HD_AUTH_GOOGLE_CLIENT_SECRET,
|
||||||
|
apiKey: process.env.HD_AUTH_GOOGLE_APP_KEY,
|
||||||
|
},
|
||||||
|
gitlab: gitlabs,
|
||||||
|
ldap: ldaps,
|
||||||
|
saml: samls,
|
||||||
|
oauth2: oauth2s,
|
||||||
|
};
|
24
src/config/csp-config.ts
Normal file
24
src/config/csp-config.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
export interface CspConfig {
|
||||||
|
enable: boolean;
|
||||||
|
maxAgeSeconds: number;
|
||||||
|
includeSubdomains: boolean;
|
||||||
|
preload: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cspSchema = Joi.object({
|
||||||
|
enable: Joi.boolean().default(true).optional(),
|
||||||
|
reportURI: Joi.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appConfigCsp = {
|
||||||
|
enable: process.env.HD_CSP_ENABLE || true,
|
||||||
|
reportURI: process.env.HD_CSP_REPORTURI,
|
||||||
|
};
|
42
src/config/database-config.ts
Normal file
42
src/config/database-config.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { DatabaseDialect } from './database-dialect.enum';
|
||||||
|
|
||||||
|
export interface DatabaseConfig {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
database: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
storage: string;
|
||||||
|
dialect: DatabaseDialect;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const databaseSchema = Joi.object({
|
||||||
|
username: Joi.string(),
|
||||||
|
password: Joi.string(),
|
||||||
|
database: Joi.string(),
|
||||||
|
host: Joi.string(),
|
||||||
|
port: Joi.number(),
|
||||||
|
storage: Joi.when('...dialect', {
|
||||||
|
is: Joi.valid(DatabaseDialect.SQLITE),
|
||||||
|
then: Joi.string(),
|
||||||
|
otherwise: Joi.optional(),
|
||||||
|
}),
|
||||||
|
dialect: Joi.string().valid(...Object.values(DatabaseDialect)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appConfigDatabase = {
|
||||||
|
username: process.env.HD_DATABASE_USER,
|
||||||
|
password: process.env.HD_DATABASE_PASS,
|
||||||
|
database: process.env.HD_DATABASE_NAME,
|
||||||
|
host: process.env.HD_DATABASE_HOST,
|
||||||
|
port: parseInt(process.env.HD_DATABASE_PORT) || undefined,
|
||||||
|
storage: process.env.HD_DATABASE_STORAGE,
|
||||||
|
dialect: process.env.HD_DATABASE_DIALECT,
|
||||||
|
};
|
12
src/config/database-dialect.enum.ts
Normal file
12
src/config/database-dialect.enum.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum DatabaseDialect {
|
||||||
|
POSTGRES = 'postgres',
|
||||||
|
MYSQL = 'mysql',
|
||||||
|
MARIADB = 'mariadb',
|
||||||
|
SQLITE = 'sqlite',
|
||||||
|
}
|
16
src/config/gitlab.enum.ts
Normal file
16
src/config/gitlab.enum.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum GitlabScope {
|
||||||
|
READ_USER = 'read_user',
|
||||||
|
API = 'api',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDo: Evaluate if V3 is really necessary anymore (it's deprecated since 2017)
|
||||||
|
export enum GitlabVersion {
|
||||||
|
V3 = 'v3',
|
||||||
|
V4 = 'v4',
|
||||||
|
}
|
30
src/config/hsts-config.ts
Normal file
30
src/config/hsts-config.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
export interface HstsConfig {
|
||||||
|
enable: boolean;
|
||||||
|
maxAgeSeconds: number;
|
||||||
|
includeSubdomains: boolean;
|
||||||
|
preload: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hstsSchema = Joi.object({
|
||||||
|
enable: Joi.boolean().default(true).optional(),
|
||||||
|
maxAgeSeconds: Joi.number()
|
||||||
|
.default(60 * 60 * 24 * 365)
|
||||||
|
.optional(),
|
||||||
|
includeSubdomains: Joi.boolean().default(true).optional(),
|
||||||
|
preload: Joi.boolean().default(true).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appConfigHsts = {
|
||||||
|
enable: process.env.HD_HSTS_ENABLE,
|
||||||
|
maxAgeSeconds: parseInt(process.env.HD_HSTS_MAX_AGE) || undefined,
|
||||||
|
includeSubdomains: process.env.HD_HSTS_INCLUDE_SUBDOMAINS,
|
||||||
|
preload: process.env.HD_HSTS_PRELOAD,
|
||||||
|
};
|
11
src/config/linkify-header-style.enum.ts
Normal file
11
src/config/linkify-header-style.enum.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum LinkifyHeaderStyle {
|
||||||
|
KEEP_CASE = 'keep-case',
|
||||||
|
LOWER_CASE = 'lower-case',
|
||||||
|
GFM = 'gfm',
|
||||||
|
}
|
13
src/config/loglevel.enum.ts
Normal file
13
src/config/loglevel.enum.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum Loglevel {
|
||||||
|
TRACE = 'trace',
|
||||||
|
DEBUG = 'debug',
|
||||||
|
INFO = 'info',
|
||||||
|
WARN = 'warn',
|
||||||
|
ERROR = 'error',
|
||||||
|
}
|
93
src/config/media-config.ts
Normal file
93
src/config/media-config.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { BackendType } from '../media/backends/backend-type.enum';
|
||||||
|
|
||||||
|
export interface MediaConfig {
|
||||||
|
backend: {
|
||||||
|
use: BackendType;
|
||||||
|
filesystem: {
|
||||||
|
uploadPath: string;
|
||||||
|
};
|
||||||
|
s3: {
|
||||||
|
accessKeyId: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
region: string;
|
||||||
|
bucket: string;
|
||||||
|
endPoint: string;
|
||||||
|
};
|
||||||
|
azure: {
|
||||||
|
connectionString: string;
|
||||||
|
container: string;
|
||||||
|
};
|
||||||
|
imgur: {
|
||||||
|
clientID: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mediaSchema = Joi.object({
|
||||||
|
backend: {
|
||||||
|
use: Joi.string().valid(...Object.values(BackendType)),
|
||||||
|
filesystem: {
|
||||||
|
uploadPath: Joi.when('...use', {
|
||||||
|
is: Joi.valid(BackendType.FILESYSTEM),
|
||||||
|
then: Joi.string(),
|
||||||
|
otherwise: Joi.optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
s3: Joi.when('...use', {
|
||||||
|
is: Joi.valid(BackendType.S3),
|
||||||
|
then: Joi.object({
|
||||||
|
accessKey: Joi.string(),
|
||||||
|
secretKey: Joi.string(),
|
||||||
|
endPoint: Joi.string(),
|
||||||
|
secure: Joi.boolean(),
|
||||||
|
port: Joi.number(),
|
||||||
|
}),
|
||||||
|
otherwise: Joi.optional(),
|
||||||
|
}),
|
||||||
|
azure: Joi.when('...use', {
|
||||||
|
is: Joi.valid(BackendType.AZURE),
|
||||||
|
then: Joi.object({
|
||||||
|
connectionString: Joi.string(),
|
||||||
|
container: Joi.string(),
|
||||||
|
}),
|
||||||
|
otherwise: Joi.optional(),
|
||||||
|
}),
|
||||||
|
imgur: Joi.when('...use', {
|
||||||
|
is: Joi.valid(BackendType.IMGUR),
|
||||||
|
then: Joi.object({
|
||||||
|
clientID: Joi.string(),
|
||||||
|
}),
|
||||||
|
otherwise: Joi.optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appConfigMedia = {
|
||||||
|
backend: {
|
||||||
|
use: process.env.HD_MEDIA_BACKEND,
|
||||||
|
filesystem: {
|
||||||
|
uploadPath: process.env.HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH,
|
||||||
|
},
|
||||||
|
s3: {
|
||||||
|
accessKey: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY,
|
||||||
|
secretKey: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY,
|
||||||
|
endPoint: process.env.HD_MEDIA_BACKEND_S3_ENDPOINT,
|
||||||
|
secure: process.env.HD_MEDIA_BACKEND_S3_SECURE,
|
||||||
|
port: parseInt(process.env.HD_MEDIA_BACKEND_S3_PORT) || undefined,
|
||||||
|
},
|
||||||
|
azure: {
|
||||||
|
connectionString: process.env.HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING,
|
||||||
|
container: process.env.HD_MEDIA_BACKEND_AZURE_CONTAINER,
|
||||||
|
},
|
||||||
|
imgur: {
|
||||||
|
clientID: process.env.HD_MEDIA_BACKEND_IMGUR_CLIENTID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
17
src/config/utils.ts
Normal file
17
src/config/utils.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const toArrayConfig = (configValue: string, separator = ',') => {
|
||||||
|
if (!configValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configValue.includes(separator)) {
|
||||||
|
return [configValue.trim()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return configValue.split(separator).map((arrayItem) => arrayItem.trim());
|
||||||
|
};
|
13
src/main.ts
13
src/main.ts
|
@ -5,10 +5,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { AppConfig } from './config/app.config';
|
||||||
import { NestConsoleLoggerService } from './logger/nest-console-logger.service';
|
import { NestConsoleLoggerService } from './logger/nest-console-logger.service';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
@ -16,6 +18,8 @@ async function bootstrap() {
|
||||||
const logger = await app.resolve(NestConsoleLoggerService);
|
const logger = await app.resolve(NestConsoleLoggerService);
|
||||||
logger.log('Switching logger', 'AppBootstrap');
|
logger.log('Switching logger', 'AppBootstrap');
|
||||||
app.useLogger(logger);
|
app.useLogger(logger);
|
||||||
|
const configService = app.get(ConfigService);
|
||||||
|
const appConfig = configService.get<AppConfig>('appConfig');
|
||||||
|
|
||||||
const swaggerOptions = new DocumentBuilder()
|
const swaggerOptions = new DocumentBuilder()
|
||||||
.setTitle('HedgeDoc')
|
.setTitle('HedgeDoc')
|
||||||
|
@ -31,12 +35,13 @@ async function bootstrap() {
|
||||||
transform: true,
|
transform: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// TODO: Get uploads directory from config
|
if (appConfig.media.backend.use === 'filesystem') {
|
||||||
app.useStaticAssets('uploads', {
|
app.useStaticAssets('uploads', {
|
||||||
prefix: '/uploads',
|
prefix: appConfig.media.backend.filesystem.uploadPath,
|
||||||
});
|
});
|
||||||
await app.listen(3000);
|
}
|
||||||
logger.log('Listening on port 3000', 'AppBootstrap');
|
await app.listen(appConfig.port);
|
||||||
|
logger.log(`Listening on port ${appConfig.port}`, 'AppBootstrap');
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export enum BackendType {
|
export enum BackendType {
|
||||||
FILEYSTEM = 'filesystem',
|
FILESYSTEM = 'filesystem',
|
||||||
S3 = 's3',
|
S3 = 's3',
|
||||||
IMGUR = 'imgur',
|
IMGUR = 'imgur',
|
||||||
AZURE = 'azure',
|
AZURE = 'azure',
|
||||||
|
|
|
@ -4,32 +4,25 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import applicationConfig, { AppConfig } from '../../config/app.config';
|
||||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||||
import { MediaBackend } from '../media-backend.interface';
|
import { MediaBackend } from '../media-backend.interface';
|
||||||
import { BackendData } from '../media-upload.entity';
|
import { BackendData } from '../media-upload.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FilesystemBackend implements MediaBackend {
|
export class FilesystemBackend implements MediaBackend {
|
||||||
// TODO: Get uploads directory from config
|
|
||||||
uploadDirectory = './uploads';
|
uploadDirectory = './uploads';
|
||||||
|
|
||||||
constructor(private readonly logger: ConsoleLoggerService) {
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@Inject(applicationConfig.KEY)
|
||||||
|
private appConfig: AppConfig,
|
||||||
|
) {
|
||||||
this.logger.setContext(FilesystemBackend.name);
|
this.logger.setContext(FilesystemBackend.name);
|
||||||
}
|
this.uploadDirectory = appConfig.media.backend.filesystem.uploadPath;
|
||||||
|
|
||||||
private getFilePath(fileName: string): string {
|
|
||||||
return join(this.uploadDirectory, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureDirectory() {
|
|
||||||
try {
|
|
||||||
await fs.access(this.uploadDirectory);
|
|
||||||
} catch (e) {
|
|
||||||
await fs.mkdir(this.uploadDirectory);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveFile(
|
async saveFile(
|
||||||
|
@ -52,4 +45,16 @@ export class FilesystemBackend implements MediaBackend {
|
||||||
// TODO: Add server address to url
|
// TODO: Add server address to url
|
||||||
return Promise.resolve('/' + filePath);
|
return Promise.resolve('/' + filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getFilePath(fileName: string): string {
|
||||||
|
return join(this.uploadDirectory, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureDirectory() {
|
||||||
|
try {
|
||||||
|
await fs.access(this.uploadDirectory);
|
||||||
|
} catch (e) {
|
||||||
|
await fs.mkdir(this.uploadDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { NotesModule } from '../notes/notes.module';
|
import { NotesModule } from '../notes/notes.module';
|
||||||
|
@ -19,6 +20,7 @@ import { MediaService } from './media.service';
|
||||||
NotesModule,
|
NotesModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
ConfigModule,
|
||||||
],
|
],
|
||||||
providers: [MediaService, FilesystemBackend],
|
providers: [MediaService, FilesystemBackend],
|
||||||
exports: [MediaService],
|
exports: [MediaService],
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import appConfigMock from '../config/app.config.mock';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { AuthorColor } from '../notes/author-color.entity';
|
import { AuthorColor } from '../notes/author-color.entity';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
|
@ -17,6 +19,7 @@ import { AuthToken } from '../users/auth-token.entity';
|
||||||
import { Identity } from '../users/identity.entity';
|
import { Identity } from '../users/identity.entity';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { FilesystemBackend } from './backends/filesystem-backend';
|
||||||
import { MediaUpload } from './media-upload.entity';
|
import { MediaUpload } from './media-upload.entity';
|
||||||
import { MediaService } from './media.service';
|
import { MediaService } from './media.service';
|
||||||
|
|
||||||
|
@ -31,8 +34,17 @@ describe('MediaService', () => {
|
||||||
provide: getRepositoryToken(MediaUpload),
|
provide: getRepositoryToken(MediaUpload),
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
FilesystemBackend,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfigMock],
|
||||||
|
}),
|
||||||
|
LoggerModule,
|
||||||
|
NotesModule,
|
||||||
|
UsersModule,
|
||||||
],
|
],
|
||||||
imports: [LoggerModule, NotesModule, UsersModule],
|
|
||||||
})
|
})
|
||||||
.overrideProvider(getRepositoryToken(AuthorColor))
|
.overrideProvider(getRepositoryToken(AuthorColor))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import * as FileType from 'file-type';
|
import * as FileType from 'file-type';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
import applicationConfig, { AppConfig } from '../config/app.config';
|
||||||
import { ClientError, NotInDBError, PermissionError } from '../errors/errors';
|
import { ClientError, NotInDBError, PermissionError } from '../errors/errors';
|
||||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
import { NotesService } from '../notes/notes.service';
|
import { NotesService } from '../notes/notes.service';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
import { BackendType } from './backends/backend-type.enum';
|
import { BackendType } from './backends/backend-type.enum';
|
||||||
import { FilesystemBackend } from './backends/filesystem-backend';
|
import { FilesystemBackend } from './backends/filesystem-backend';
|
||||||
|
import { MediaBackend } from './media-backend.interface';
|
||||||
import { MediaUpload } from './media-upload.entity';
|
import { MediaUpload } from './media-upload.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MediaService {
|
export class MediaService {
|
||||||
|
mediaBackend: MediaBackend;
|
||||||
|
mediaBackendType: BackendType;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: ConsoleLoggerService,
|
private readonly logger: ConsoleLoggerService,
|
||||||
@InjectRepository(MediaUpload)
|
@InjectRepository(MediaUpload)
|
||||||
|
@ -26,8 +31,12 @@ export class MediaService {
|
||||||
private notesService: NotesService,
|
private notesService: NotesService,
|
||||||
private usersService: UsersService,
|
private usersService: UsersService,
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
@Inject(applicationConfig.KEY)
|
||||||
|
private appConfig: AppConfig,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(MediaService.name);
|
this.logger.setContext(MediaService.name);
|
||||||
|
this.mediaBackendType = this.chooseBackendType();
|
||||||
|
this.mediaBackend = this.getBackendFromType(this.mediaBackendType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static isAllowedMimeType(mimeType: string): boolean {
|
private static isAllowedMimeType(mimeType: string): boolean {
|
||||||
|
@ -63,16 +72,14 @@ export class MediaService {
|
||||||
if (!MediaService.isAllowedMimeType(fileTypeResult.mime)) {
|
if (!MediaService.isAllowedMimeType(fileTypeResult.mime)) {
|
||||||
throw new ClientError('MIME type not allowed.');
|
throw new ClientError('MIME type not allowed.');
|
||||||
}
|
}
|
||||||
//TODO: Choose backend according to config
|
|
||||||
const mediaUpload = MediaUpload.create(
|
const mediaUpload = MediaUpload.create(
|
||||||
note,
|
note,
|
||||||
user,
|
user,
|
||||||
fileTypeResult.ext,
|
fileTypeResult.ext,
|
||||||
BackendType.FILEYSTEM,
|
this.mediaBackendType,
|
||||||
);
|
);
|
||||||
this.logger.debug(`Generated filename: '${mediaUpload.id}'`, 'saveFile');
|
this.logger.debug(`Generated filename: '${mediaUpload.id}'`, 'saveFile');
|
||||||
const backend = this.moduleRef.get(FilesystemBackend);
|
const [url, backendData] = await this.mediaBackend.saveFile(
|
||||||
const [url, backendData] = await backend.saveFile(
|
|
||||||
fileBuffer,
|
fileBuffer,
|
||||||
mediaUpload.id,
|
mediaUpload.id,
|
||||||
);
|
);
|
||||||
|
@ -96,8 +103,7 @@ export class MediaService {
|
||||||
`File '${filename}' is not owned by '${username}'`,
|
`File '${filename}' is not owned by '${username}'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const backend = this.moduleRef.get(FilesystemBackend);
|
await this.mediaBackend.deleteFile(filename, mediaUpload.backendData);
|
||||||
await backend.deleteFile(filename, mediaUpload.backendData);
|
|
||||||
await this.mediaUploadRepository.remove(mediaUpload);
|
await this.mediaUploadRepository.remove(mediaUpload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,4 +118,18 @@ export class MediaService {
|
||||||
}
|
}
|
||||||
return mediaUpload;
|
return mediaUpload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private chooseBackendType(): BackendType {
|
||||||
|
switch (this.appConfig.media.backend.use) {
|
||||||
|
case 'filesystem':
|
||||||
|
return BackendType.FILESYSTEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBackendFromType(type: BackendType): MediaBackend {
|
||||||
|
switch (type) {
|
||||||
|
case BackendType.FILESYSTEM:
|
||||||
|
return this.moduleRef.get(FilesystemBackend);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ConfigModule, registerAs } from '@nestjs/config';
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import * as request from 'supertest';
|
import * as request from 'supertest';
|
||||||
import { PublicApiModule } from '../../src/api/public/public-api.module';
|
import { PublicApiModule } from '../../src/api/public/public-api.module';
|
||||||
|
import appConfigMock from '../../src/config/app.config.mock';
|
||||||
import { GroupsModule } from '../../src/groups/groups.module';
|
import { GroupsModule } from '../../src/groups/groups.module';
|
||||||
import { LoggerModule } from '../../src/logger/logger.module';
|
import { LoggerModule } from '../../src/logger/logger.module';
|
||||||
import { NestConsoleLoggerService } from '../../src/logger/nest-console-logger.service';
|
import { NestConsoleLoggerService } from '../../src/logger/nest-console-logger.service';
|
||||||
|
@ -27,6 +29,10 @@ describe('Notes', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfigMock],
|
||||||
|
}),
|
||||||
PublicApiModule,
|
PublicApiModule,
|
||||||
MediaModule,
|
MediaModule,
|
||||||
TypeOrmModule.forRoot({
|
TypeOrmModule.forRoot({
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { ConfigModule, registerAs } from '@nestjs/config';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import * as request from 'supertest';
|
import * as request from 'supertest';
|
||||||
import { PublicApiModule } from '../../src/api/public/public-api.module';
|
import { PublicApiModule } from '../../src/api/public/public-api.module';
|
||||||
|
import appConfigMock from '../../src/config/app.config.mock';
|
||||||
import { NotInDBError } from '../../src/errors/errors';
|
import { NotInDBError } from '../../src/errors/errors';
|
||||||
import { GroupsModule } from '../../src/groups/groups.module';
|
import { GroupsModule } from '../../src/groups/groups.module';
|
||||||
import { LoggerModule } from '../../src/logger/logger.module';
|
import { LoggerModule } from '../../src/logger/logger.module';
|
||||||
|
@ -23,6 +25,10 @@ describe('Notes', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfigMock],
|
||||||
|
}),
|
||||||
PublicApiModule,
|
PublicApiModule,
|
||||||
NotesModule,
|
NotesModule,
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
|
|
74
yarn.lock
74
yarn.lock
|
@ -367,6 +367,18 @@
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
|
"@hapi/hoek@^9.0.0":
|
||||||
|
version "9.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa"
|
||||||
|
integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==
|
||||||
|
|
||||||
|
"@hapi/topo@^5.0.0":
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
|
||||||
|
integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||||
|
@ -592,6 +604,18 @@
|
||||||
tslib "2.0.3"
|
tslib "2.0.3"
|
||||||
uuid "8.3.2"
|
uuid "8.3.2"
|
||||||
|
|
||||||
|
"@nestjs/config@^0.6.1":
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-0.6.1.tgz#26e4bfd4b2f9d3a6b6bed6445a2a1e766abdf1c6"
|
||||||
|
integrity sha512-sSIEbHp0xV7bneG2/CePqJh60ELojsBXBPuRM40AcVQwuDRQQ4RAnLT5uaJbWB2xFQjQwik4zejN+27t1cCiBQ==
|
||||||
|
dependencies:
|
||||||
|
dotenv "8.2.0"
|
||||||
|
dotenv-expand "5.1.0"
|
||||||
|
lodash.get "4.4.2"
|
||||||
|
lodash.has "4.5.2"
|
||||||
|
lodash.set "4.3.2"
|
||||||
|
uuid "8.3.1"
|
||||||
|
|
||||||
"@nestjs/core@7.6.5":
|
"@nestjs/core@7.6.5":
|
||||||
version "7.6.5"
|
version "7.6.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.5.tgz#93c642d1abf8d3f09f8f78c262814eaf83bc2ad6"
|
resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.5.tgz#93c642d1abf8d3f09f8f78c262814eaf83bc2ad6"
|
||||||
|
@ -703,6 +727,23 @@
|
||||||
"@angular-devkit/core" "11.0.3"
|
"@angular-devkit/core" "11.0.3"
|
||||||
"@angular-devkit/schematics" "11.0.3"
|
"@angular-devkit/schematics" "11.0.3"
|
||||||
|
|
||||||
|
"@sideway/address@^4.1.0":
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d"
|
||||||
|
integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
|
"@sideway/formula@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||||
|
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||||
|
|
||||||
|
"@sideway/pinpoint@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
|
||||||
|
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
|
||||||
|
|
||||||
"@sinonjs/commons@^1.7.0":
|
"@sinonjs/commons@^1.7.0":
|
||||||
version "1.7.2"
|
version "1.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
|
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
|
||||||
|
@ -2454,7 +2495,12 @@ domexception@^2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
webidl-conversions "^5.0.0"
|
webidl-conversions "^5.0.0"
|
||||||
|
|
||||||
dotenv@^8.2.0:
|
dotenv-expand@5.1.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
|
||||||
|
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
|
||||||
|
|
||||||
|
dotenv@8.2.0, dotenv@^8.2.0:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||||
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||||
|
@ -4344,6 +4390,17 @@ jest@26.6.3:
|
||||||
import-local "^3.0.2"
|
import-local "^3.0.2"
|
||||||
jest-cli "^26.6.3"
|
jest-cli "^26.6.3"
|
||||||
|
|
||||||
|
joi@^17.3.0:
|
||||||
|
version "17.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2"
|
||||||
|
integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
"@hapi/topo" "^5.0.0"
|
||||||
|
"@sideway/address" "^4.1.0"
|
||||||
|
"@sideway/formula" "^3.0.0"
|
||||||
|
"@sideway/pinpoint" "^2.0.0"
|
||||||
|
|
||||||
js-tokens@^4.0.0:
|
js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
@ -4566,11 +4623,26 @@ locate-path@^5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^4.1.0"
|
p-locate "^4.1.0"
|
||||||
|
|
||||||
|
lodash.get@4.4.2:
|
||||||
|
version "4.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||||
|
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
|
||||||
|
|
||||||
|
lodash.has@4.5.2:
|
||||||
|
version "4.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
|
||||||
|
integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=
|
||||||
|
|
||||||
lodash.memoize@4.x:
|
lodash.memoize@4.x:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||||
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
||||||
|
|
||||||
|
lodash.set@4.3.2:
|
||||||
|
version "4.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
|
||||||
|
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
|
||||||
|
|
||||||
lodash.sortby@^4.7.0:
|
lodash.sortby@^4.7.0:
|
||||||
version "4.7.0"
|
version "4.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
|
|
Loading…
Reference in a new issue