mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-19 08:02:55 +00:00
Also: - Add functionality for serving the responses from plugins - Add tests for related methods and routes Closes #2, #37
227 lines
7.2 KiB
TypeScript
227 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 * as config from 'config';
|
|
import * as cors from 'cors';
|
|
import * as express from 'express';
|
|
import * as morgan from 'morgan';
|
|
import {join} from 'path';
|
|
import {configFile, isTestEnvironment, mailer, plugins, validator} from './common';
|
|
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';
|
|
import {Elasticsearch} from './storage/elasticsearch/elasticsearch';
|
|
|
|
/**
|
|
* Created express application
|
|
*/
|
|
export const app = express();
|
|
|
|
/**
|
|
* Configure the backend
|
|
*/
|
|
export async function configureApp() {
|
|
// request loggers have to be the first middleware to be set in express
|
|
app.use(morgan('dev'));
|
|
|
|
// only accept json as content type for all requests
|
|
app.use((req, res, next) => {
|
|
// get the content type
|
|
let contentType = '';
|
|
// the content type can be string, string[] or undefined
|
|
if (typeof req.headers['Content-Type'] === 'string') {
|
|
// weird type definitions require an explicit cast
|
|
contentType = req.headers['Content-Type'] as string;
|
|
} else if (typeof req.headers['content-type'] === 'string') {
|
|
// weird type definitions require no cast here though...
|
|
contentType = req.headers['content-type'];
|
|
}
|
|
|
|
// Only accept json as content type
|
|
if (contentType === '' || contentType.match(/^application\/json$/) === null) {
|
|
// return an error in the response
|
|
const err = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment);
|
|
res.status(err.statusCode);
|
|
res.json(err);
|
|
|
|
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) {
|
|
req.off('data', chunkGatherer);
|
|
req.off('end', endCallback);
|
|
// return an error in the response
|
|
const err = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment);
|
|
res.status(err.statusCode);
|
|
res.json(err);
|
|
|
|
return;
|
|
}
|
|
// push the chunk in the buffer
|
|
bodyBuffer.push(chunk);
|
|
};
|
|
|
|
const endCallback = () => {
|
|
req.body = Buffer.concat(bodyBuffer)
|
|
.toString();
|
|
|
|
try {
|
|
req.body = JSON.parse(req.body);
|
|
next();
|
|
} catch (catchErr) {
|
|
const err = new SCSyntaxErrorResponse(catchErr.message, isTestEnvironment);
|
|
res.status(err.statusCode);
|
|
res.json(err);
|
|
|
|
return;
|
|
}
|
|
};
|
|
req.on('data', chunkGatherer)
|
|
.on('end', endCallback);
|
|
});
|
|
|
|
const databases: {[name: string]: DatabaseConstructor; } = {
|
|
elasticsearch: Elasticsearch,
|
|
};
|
|
|
|
// validate config file
|
|
await validator.addSchemas(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 Error('No implementation for configured database found. Please check your configuration.');
|
|
}
|
|
|
|
Logger.ok('Validated config file sucessfully');
|
|
|
|
// 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);
|
|
|
|
const corsOptions = {
|
|
allowedHeaders: [
|
|
'DNT',
|
|
'Keep-Alive',
|
|
'User-Agent',
|
|
'X-Requested-With',
|
|
'If-Modified-Since',
|
|
'Cache-Control',
|
|
'Content-Type',
|
|
'X-StApps-Version',
|
|
],
|
|
credentials: true,
|
|
maxAge: 1728000,
|
|
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));
|
|
|
|
// 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 (req, res, next) => {
|
|
// if the route exists then call virtual route on the plugin that registered that route
|
|
if (plugins.has(req.originalUrl)) {
|
|
try {
|
|
res.json(await virtualPluginRoute(req, plugins.get(req.originalUrl)!));
|
|
} catch (e) {
|
|
// in case of error send an error response
|
|
res.status(e.statusCode);
|
|
res.json(e);
|
|
}
|
|
} else {
|
|
// pass to the next matching route (which is 404)
|
|
next();
|
|
}
|
|
});
|
|
|
|
// add a route for a missing resource (404)
|
|
app.use((_req, res) => {
|
|
const errorResponse = new SCNotFoundErrorResponse(isTestEnvironment);
|
|
res.status(errorResponse.statusCode);
|
|
res.json(errorResponse);
|
|
});
|
|
}
|