UNPKG

13.7 kBJavaScriptView Raw
1"use strict";
2/**
3 * This file is part of the @egodigital/egoose distribution.
4 * Copyright (c) e.GO Digital GmbH, Aachen, Germany (https://www.e-go-digital.com/)
5 *
6 * @egodigital/egoose is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * @egodigital/egoose is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18Object.defineProperty(exports, "__esModule", { value: true });
19const _ = require("lodash");
20const bodyParser = require("body-parser");
21const errorHandler = require("errorhandler");
22const index_1 = require("../mongo/index");
23const logger_1 = require("../diagnostics/logger");
24const express = require("express");
25const http = require("http");
26const MergeDeep = require("merge-deep");
27const dev_1 = require("../dev");
28const index_2 = require("../index");
29const util = require("util");
30/**
31 * An API host.
32 */
33class ApiHost {
34 constructor() {
35 this._poweredBy = '@egodigital/egoose';
36 this._useBodyParser = true;
37 }
38 /**
39 * Gets the underlying Express app instance.
40 */
41 get app() {
42 return this._app;
43 }
44 authorizer(newValue) {
45 if (arguments.length > 0) {
46 this._authorizer = newValue;
47 return this;
48 }
49 return this._authorizer;
50 }
51 /**
52 * (Re-)Initializes the host.
53 *
54 * @param {InitializeApiHostOptions} [opts] Custom options.
55 */
56 initialize(opts) {
57 if (_.isNil(opts)) {
58 opts = {};
59 }
60 const OLD_SERVER = this._server;
61 if (OLD_SERVER) {
62 OLD_SERVER.close();
63 this._server = null;
64 }
65 const NEW_APP = express();
66 if (opts.onAppCreated) {
67 opts.onAppCreated
68 .apply(this, [NEW_APP]);
69 }
70 const NEW_LOGGER = new logger_1.Logger();
71 const NEW_API_ROOT = express.Router();
72 NEW_APP.use('/api', NEW_API_ROOT);
73 if (this._useBodyParser) {
74 let ubpOpts = {
75 defaultCharset: 'utf8',
76 inflate: true,
77 strict: true,
78 extended: true
79 };
80 if (true !== this._useBodyParser) {
81 ubpOpts = MergeDeep(ubpOpts, this._useBodyParser);
82 }
83 NEW_API_ROOT.use(bodyParser.json(ubpOpts));
84 }
85 const POWERED_BY = index_2.toStringSafe(this._poweredBy).trim();
86 if ('' !== POWERED_BY) {
87 NEW_API_ROOT.use((req, res, next) => {
88 res.header('X-Powered-By', POWERED_BY);
89 next();
90 });
91 }
92 const AUTHORIZER = this._authorizer;
93 if (!_.isNil(AUTHORIZER)) {
94 NEW_API_ROOT.use(async (req, res, next) => {
95 const IS_VALID = await Promise.resolve(AUTHORIZER(req));
96 if (IS_VALID) {
97 next();
98 return;
99 }
100 return res.status(401)
101 .send();
102 });
103 }
104 if (dev_1.IS_LOCAL_DEV) {
105 // trace request
106 NEW_API_ROOT.use((req, res, next) => {
107 try {
108 NEW_LOGGER.trace({
109 request: {
110 headers: req.headers,
111 method: req.method,
112 query: req.query,
113 }
114 }, 'request');
115 }
116 catch (_a) { }
117 next();
118 });
119 // only for test use
120 NEW_API_ROOT.use((req, res, next) => {
121 res.header("Access-Control-Allow-Origin", "*");
122 res.header("Access-Control-Allow-Headers", "*");
123 res.header("Access-Control-Allow-Methods", "*");
124 next();
125 });
126 }
127 this.setupLogger(NEW_LOGGER);
128 this._logger = NEW_LOGGER;
129 this.setupApi(NEW_APP, NEW_API_ROOT);
130 // error handler
131 {
132 let errHandlerOpts = this._useErrorHandler;
133 if (_.isNil(errHandlerOpts)) {
134 errHandlerOpts = false;
135 }
136 if (errHandlerOpts) {
137 if (true === errHandlerOpts) {
138 errHandlerOpts = {
139 log: (err, str, req) => {
140 const LOG_MSG = `Error in [${req.method}] '${req.url}':
141
142${str}`;
143 this.logger
144 .err(LOG_MSG, 'unhandled_error');
145 },
146 };
147 }
148 NEW_API_ROOT.use(errorHandler(errHandlerOpts));
149 }
150 }
151 this._app = NEW_APP;
152 this._root = NEW_API_ROOT;
153 }
154 /**
155 * Gets if the host is currently running or not.
156 */
157 get isRunning() {
158 return !_.isNil(this._server);
159 }
160 /**
161 * Gets the underlying logger.
162 */
163 get logger() {
164 return this._logger;
165 }
166 poweredBy(newValue) {
167 if (arguments.length > 0) {
168 this._poweredBy = index_2.toStringSafe(newValue).trim();
169 return this;
170 }
171 return this._poweredBy;
172 }
173 /**
174 * Gets the root endpoint.
175 */
176 get root() {
177 return this._root;
178 }
179 /**
180 * Sets a 'Basic Auth' based authorizer.
181 *
182 * @param {BasicAuthAuthorizer} authorizer The authorizer.
183 *
184 * @return this
185 */
186 setBasicAuth(authorizer) {
187 return this.setPrefixedAuthorizer(async (token) => {
188 try {
189 let username;
190 let password;
191 const USERNAME_AND_PASSWORD = index_2.toStringSafe(token).trim();
192 if ('' !== USERNAME_AND_PASSWORD) {
193 const UNAME_PWD = (Buffer.from(USERNAME_AND_PASSWORD, 'base64')).toString('utf8');
194 const USER_PWD_SEP = UNAME_PWD.indexOf(':');
195 if (USER_PWD_SEP > -1) {
196 username = UNAME_PWD.substr(0, USER_PWD_SEP);
197 password = UNAME_PWD.substr(USER_PWD_SEP + 1);
198 }
199 else {
200 username = UNAME_PWD;
201 }
202 }
203 username = index_2.normalizeString(username);
204 password = index_2.toStringSafe(password);
205 return await Promise.resolve(authorizer(username, password));
206 }
207 catch (_a) { }
208 return false;
209 }, 'basic');
210 }
211 /**
212 * Sets a prefixed based authorizer.
213 *
214 * @param {TokenAuthorizer} authorizer The authorizer.
215 * @param {string} [prefix] The prefix.
216 *
217 * @return this
218 */
219 setPrefixedAuthorizer(authorizer, prefix = 'bearer') {
220 prefix = index_2.normalizeString(prefix);
221 return this.authorizer(async (req) => {
222 const AUTH = index_2.toStringSafe(req.headers['authorization']).trim();
223 if (AUTH.toLowerCase().startsWith(prefix + ' ')) {
224 return await Promise.resolve(authorizer(AUTH.substr(prefix.length + 1)));
225 }
226 return false;
227 });
228 }
229 /**
230 * Sets up a new api / app instance.
231 *
232 * @param {express.Express} newApp The instance to setup.
233 * @param {express.Router} newRoot The API root.
234 */
235 setupApi(newApp, newRoot) {
236 }
237 /**
238 * Sets up a new logger instance.
239 *
240 * @param {Logger} newLogger The instance to setup.
241 */
242 setupLogger(newLogger) {
243 }
244 /**
245 * Starts the host.
246 *
247 * @param {number} [port] The custom port to use. By default 'APP_PORT' environment variable is used.
248 * Otherwise 80 is the default port.
249 *
250 * @return {Promise<boolean>} The promise, which indicates if operation successful or not.
251 */
252 start(port) {
253 if (arguments.length < 1) {
254 port = parseInt(index_2.toStringSafe(process.env.APP_PORT).trim());
255 }
256 else {
257 port = parseInt(index_2.toStringSafe(port).trim());
258 }
259 return new Promise((resolve, reject) => {
260 if (this.isRunning) {
261 resolve(false);
262 return;
263 }
264 try {
265 let serverFactory;
266 // TODO: implement secure HTTP support
267 serverFactory = () => {
268 if (isNaN(port)) {
269 port = 80;
270 }
271 return http.createServer(this.app);
272 };
273 const NEW_SERVER = serverFactory();
274 NEW_SERVER.listen(port, () => {
275 this._server = NEW_SERVER;
276 resolve(true);
277 });
278 }
279 catch (e) {
280 reject(e);
281 }
282 });
283 }
284 /**
285 * Stops the host.
286 *
287 * @return {Promise<boolean>} The promise, which indicates if operation successful or not.
288 */
289 stop() {
290 return new Promise((resolve, reject) => {
291 const OLD_SERVER = this._server;
292 if (_.isNil(OLD_SERVER)) {
293 resolve(false);
294 return;
295 }
296 try {
297 OLD_SERVER.close(() => {
298 this._server = null;
299 resolve(true);
300 });
301 }
302 catch (e) {
303 reject(e);
304 }
305 });
306 }
307 useBodyParser(newValue) {
308 if (arguments.length > 0) {
309 this._useBodyParser = newValue;
310 return this;
311 }
312 return this._useBodyParser;
313 }
314 useErrorHandler(newValue) {
315 if (arguments.length > 0) {
316 this._useErrorHandler = newValue;
317 return this;
318 }
319 return this._useErrorHandler;
320 }
321}
322exports.ApiHost = ApiHost;
323/**
324 * An API with MongoDB helper methods.
325 */
326class MongoApiHost extends ApiHost {
327 /**
328 * Returns the database class.
329 *
330 * @return {MongoDatabase} The class.
331 */
332 getDatabaseClass() {
333 return index_1.MongoDatabase;
334 }
335 /**
336 * Log something into the database.
337 * This requires a 'logs' collection, described by 'LogsDocument' interface.
338 *
339 * @param {any} message The message.
340 * @param {LogType} type The type.
341 * @param {any} [payload] The (optional) payload.
342 */
343 async log(message, type, payload) {
344 const NOW = index_2.utc();
345 try {
346 await this.withDatabase(async (db) => {
347 await db.model('Logs').insertMany([{
348 created: NOW.toDate(),
349 message: toSerializableLogValue(message),
350 payload: toSerializableLogValue(payload),
351 type: type,
352 uuid: index_2.guid(),
353 }]);
354 });
355 }
356 catch (error) {
357 console.log('logging failed', error);
358 }
359 }
360 /**
361 * Options a new connection to a database.
362 *
363 * @param {TOptions} [opts] The custom options to use.
364 *
365 * @return {TDatabase} The new, opened, database.
366 */
367 async openDatabase(opts) {
368 const DB_CLASS = this.getDatabaseClass();
369 let db;
370 if (_.isNil(opts)) {
371 // use environment variables
372 db = new DB_CLASS({
373 database: process.env.MONGO_DB,
374 host: process.env.MONGO_HOST,
375 options: process.env.MONGO_OPTIONS,
376 port: parseInt(process.env.MONGO_PORT),
377 password: process.env.MONGO_PASSWORD,
378 user: process.env.MONGO_USER,
379 });
380 }
381 else {
382 db = new DB_CLASS(opts);
383 }
384 await db.connect();
385 return db;
386 }
387 /**
388 * Opens a data connection and invokes an action for it.
389 * After invokation, the database is closed automatically.
390 *
391 * @param {Function} action The action to invoke.
392 * @param {TOptions} [opts] Custom database options.
393 *
394 * @return {Promise<TResult>} The promise with the result of the action.
395 */
396 async withDatabase(action, opts) {
397 const DB = await this.openDatabase(opts);
398 try {
399 return await Promise.resolve(action(DB));
400 }
401 finally {
402 await DB.disconnect();
403 }
404 }
405}
406exports.MongoApiHost = MongoApiHost;
407function toSerializableLogValue(value) {
408 if (_.isNil(value)) {
409 value = null;
410 }
411 else {
412 if (Array.isArray(value) || _.isObjectLike(value)) {
413 try {
414 value = util.inspect(value, {
415 depth: null,
416 maxArrayLength: 512,
417 showHidden: false,
418 });
419 }
420 catch (e) {
421 value = index_2.toStringSafe(value)
422 .trim();
423 }
424 }
425 else if (value instanceof Error) {
426 value = `(ERR::${value.name}) '${value.message}'
427
428${value.stack}`;
429 }
430 else {
431 value = index_2.toStringSafe(value)
432 .trim();
433 }
434 }
435 return value;
436}
437//# sourceMappingURL=host.js.map
\No newline at end of file