Files
openstapps/src/app.ts
2022-06-27 14:40:09 +00:00

222 lines
7.2 KiB
TypeScript

/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
SCNotFoundErrorResponse,
SCRequestBodyTooLargeErrorResponse,
SCSyntaxErrorResponse,
SCUnsupportedMediaTypeErrorResponse,
} from '@openstapps/core';
import {Logger} from '@openstapps/logger';
import config from 'config';
import cors from 'cors';
import {Express} from 'express';
import morgan from 'morgan';
import path from 'path';
import {configFile, DEFAULT_TIMEOUT, isTestEnvironment, mailer, plugins, validator} from './common';
import {getPrometheusMiddleware} from './middleware/prometheus';
import {MailQueue} from './notification/mail-queue';
import {bulkAddRouter} from './routes/bulk-add-route';
import {bulkDoneRouter} from './routes/bulk-done-route';
import {bulkRouter} from './routes/bulk-route';
import {indexRouter} from './routes/index-route';
import {multiSearchRouter} from './routes/multi-search-route';
import {pluginRegisterRouter} from './routes/plugin-register-route';
import {searchRouter} from './routes/search-route';
import {thingUpdateRouter} from './routes/thing-update-route';
import {virtualPluginRoute} from './routes/virtual-plugin-route';
import {BulkStorage} from './storage/bulk-storage';
import {DatabaseConstructor} from './storage/database';
/**
* Configure the backend
*/
export async function configureApp(app: Express, databases: {[name: string]: DatabaseConstructor}) {
let integrationTestTimeout: NodeJS.Timeout;
// request loggers have to be the first middleware to be set in express
app.use(
morgan('dev', {
skip: (_request, response) => {
if (process.env.NODE_ENV === 'integration-test') {
clearTimeout(integrationTestTimeout);
integrationTestTimeout = setTimeout(() => {
process.exit(1);
}, DEFAULT_TIMEOUT);
return false;
}
return response.statusCode < 400;
},
stream: process.stdout,
}),
);
if (process.env.PROMETHEUS_MIDDLEWARE === 'true') {
app.use(getPrometheusMiddleware());
}
const corsOptions = {
allowedHeaders: [
'DNT',
'Keep-Alive',
'User-Agent',
'X-Requested-With',
'If-Modified-Since',
'Cache-Control',
'Content-Type',
'X-StApps-Version',
],
credentials: true,
maxAge: 1_728_000,
methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
optionsSuccessStatus: 204,
};
// allow all origins on all routes
app.use(cors(corsOptions));
// TODO: See if it can handle options request with no content-type
// allow cors preflight requests on every route
app.options('*', [cors(corsOptions)]);
// only accept json as content type for all requests
app.use((request, response, next) => {
// Only accept json as content type
if (request.is('application/json') !== 'application/json') {
// return an error in the response
const error = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment);
response.status(error.statusCode);
response.json(error);
return;
}
const bodyBuffer: Buffer[] = [];
// we don't know the full size, the only way we can get is by adding up all individual chunk sizes
let bodySize = 0;
const chunkGatherer = (chunk: Buffer) => {
bodySize += chunk.byteLength;
// when adding each chunk size to the total size, check how large it now is.
if (bodySize > configFile.backend.maxRequestBodySize) {
request.off('data', chunkGatherer);
request.off('end', endCallback);
// return an error in the response
const error = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment);
response.status(error.statusCode);
response.json(error);
return;
}
// push the chunk in the buffer
bodyBuffer.push(chunk);
};
const endCallback = () => {
request.body = Buffer.concat(bodyBuffer).toString();
try {
request.body = JSON.parse(request.body);
next();
} catch (error) {
const error_ = new SCSyntaxErrorResponse(error.message, isTestEnvironment);
response.status(error_.statusCode);
response.json(error_);
return;
}
};
request.on('data', chunkGatherer).on('end', endCallback);
});
// validate config file
await validator.addSchemas(path.join('node_modules', '@openstapps', 'core', 'lib', 'schema'));
// validate the config file
const configValidation = validator.validate(configFile, 'SCConfigFile');
// validation failed
if (configValidation.errors.length > 0) {
throw new Error(
`Validation of config file failed. Errors were: ${JSON.stringify(configValidation.errors)}`,
);
}
// check if a database name was given
if (!config.has('internal.database.name')) {
throw new Error('You have to configure a database');
}
const database = new databases[config.get<string>('internal.database.name')](
configFile,
// mailQueue
typeof mailer !== 'undefined' && config.has('internal.monitoring') ? new MailQueue(mailer) : undefined,
);
await database.init();
if (typeof database === 'undefined') {
throw new TypeError('No implementation for configured database found. Please check your configuration.');
}
Logger.ok('Validated config file successfully');
// treats /foo and /foo/ as two different routes
// see http://expressjs.com/en/api.html#app.set
app.enable('strict routing');
// make the bulk storage available to all http middlewares/routes
app.set('bulk', new BulkStorage(database));
app.set('env', process.env.NODE_ENV);
// load routes before plugins
// they now can be used or overwritten by any plugin
app.use(
bulkAddRouter,
bulkDoneRouter,
bulkRouter,
indexRouter,
multiSearchRouter,
pluginRegisterRouter,
searchRouter,
thingUpdateRouter,
);
// for plugins, as Express doesn't really want you to unregister routes (and doesn't offer any method to do so at all)
app.all('*', async (request, response, next) => {
// if the route exists then call virtual route on the plugin that registered that route
if (plugins.has(request.originalUrl)) {
try {
response.json(await virtualPluginRoute(request, plugins.get(request.originalUrl)!));
} catch (error) {
// in case of error send an error response
response.status(error.statusCode);
response.json(error);
}
} else {
// pass to the next matching route (which is 404)
next();
}
});
// add a route for a missing resource (404)
app.use((_request, response) => {
const errorResponse = new SCNotFoundErrorResponse(isTestEnvironment);
response.status(errorResponse.statusCode);
response.json(errorResponse);
});
}