UNPKG

11.4 kBJavaScriptView Raw
1const _ = require('lodash');
2const express = require('express');
3const cacheManager = require('cache-manager');
4const redisStore = require('cache-manager-redis-store');
5const Promise = require('bluebird');
6const URL = require('url-parse');
7const zlib = Promise.promisifyAll(require('zlib'));
8const Logger = require('le_node');
9const CircularJSON = require('circular-json-es6');
10const sizeof = require('object-sizeof');
11const deepFreeze = require('deep-freeze');
12
13const Api = require('ace-api');
14
15const defaultConfig = require('./config.default');
16
17process.on('unhandledRejection', result => console.error(result));
18
19function AceApiServer (app, customConfig = {}, customAuthMiddleware = null) {
20 const config = deepFreeze(_.merge({}, Api.defaultConfig, defaultConfig, customConfig));
21
22 // Async middleware
23
24 const asyncMiddleware = fn => (req, res, next) => {
25 Promise.resolve(fn(req, res, next))
26 .catch(next);
27 };
28
29 // Skip authorisation
30
31 const skipAuth = (req) => {
32 if (config.environment !== 'development') {
33 return false;
34 }
35 const allowedRoutes = [
36 '/token',
37 '/email/template',
38 ];
39 return allowedRoutes.indexOf(req.path) > -1;
40 };
41
42 // Default auth middleware
43
44 const defaultAuthMiddleware = (req, res, next) => {
45 if (skipAuth(req)) {
46 next();
47 return;
48 }
49
50 if (!req.session.userId) {
51 res.status(401);
52 res.send({
53 code: 401,
54 message: 'Not authorised',
55 });
56 return;
57 }
58
59 next();
60 };
61
62 const authMiddleware = customAuthMiddleware || defaultAuthMiddleware;
63
64 // Permissions middleware
65
66 const permissionMiddleware = (permission, req, res, next) => {
67 if (!req.session.role) {
68 res.status(401);
69 res.send({
70 permission,
71 message: 'Error: role not defined in session.',
72 });
73 return;
74 }
75
76 if (req.session.role === 'super') {
77 next();
78 return;
79 }
80
81 const roles = Api.Roles();
82
83 if (!roles.role(req.session.role) || roles.role(req.session.role).permissions[permission] !== true) {
84 res.status(401);
85 res.send({
86 permission,
87 message: 'Sorry, you\'re not authorised to do this.',
88 });
89 return;
90 }
91
92 next();
93 };
94
95 // Clone and extend config per request/session
96
97 const omitUndefined = (obj) => {
98 _.forIn(obj, (value, key, obj) => {
99 if (_.isPlainObject(value)) {
100 value = omitUndefined(value);
101
102 if (_.keys(value).length === 0) {
103 delete obj[key];
104 }
105 }
106
107 if (_.isUndefined(value)) {
108 delete obj[key];
109 }
110 });
111
112 return obj;
113 };
114
115 const cloneConfig = config => _.mergeWith({}, JSON.parse(JSON.stringify(config)), omitUndefined(_.cloneDeep(config)));
116
117 const getConfig = async (slug) => {
118 const configClone = cloneConfig(config);
119
120 configClone.slug = slug;
121 configClone.db.name = slug;
122
123 return configClone;
124 };
125
126 // Cache
127
128 let cache;
129
130 if (config.cache.enabled) {
131 if (config.redis.url || config.redis.host) {
132 const redisOptions = {
133 ttl: config.cache.ttl,
134 };
135
136 if (config.redis.url) {
137 redisOptions.url = config.redis.url;
138 } else {
139 redisOptions.host = config.redis.host;
140 redisOptions.port = config.redis.port;
141 redisOptions.password = config.redis.password;
142 redisOptions.db = config.redis.db;
143 }
144
145 cache = cacheManager.caching(_.merge({ store: redisStore }, redisOptions));
146
147 const redisClient = cache.store.getClient();
148 redisClient.on('ready', () => {
149 console.log('redis: ready');
150 });
151
152 } else {
153 cache = cacheManager.caching({
154 store: 'memory',
155 ttl: config.cache.ttl,
156 max: config.cache.memory.max,
157 length: (item) => {
158 // const length = Buffer.byteLength(item, 'utf8')
159 const length = sizeof(item);
160 return length;
161 },
162 });
163 }
164 }
165
166 // Cache middleware
167
168 const cacheMiddleware = asyncMiddleware(async (req, res, next) => {
169 const useCachedResponse = (
170 config.cache.enabled
171 && req.session.role === 'guest' // TODO: Replace 'guest' with constant
172 && (req.query.__cache && JSON.parse(req.query.__cache)) !== false
173 );
174
175 if (useCachedResponse) {
176 try {
177 const key = `${req.session.slug}${req.url}`;
178
179 let response = await cache.get(key);
180
181 if (typeof response === 'string') {
182 if (config.cache.compress) {
183 response = (await zlib.gunzipAsync(Buffer.from(response, 'base64'))).toString();
184 }
185
186 try {
187 response = JSON.parse(response);
188 } catch (error) {
189 //
190 }
191
192 res.set('X-Cached-Response', true);
193 res.status(200);
194 res.send(response);
195
196 return;
197 }
198 } catch (error) {
199 console.error(error);
200 }
201 }
202
203 res.set('X-Cached-Response', false);
204 next();
205 });
206
207 // Response helpers
208
209 const handleError = (req, res, error) => {
210 if (_.isObject(error)) {
211 error = JSON.parse(CircularJSON.stringify(error));
212 }
213
214 const statusCode = error.statusCode || error.code || 500;
215 const errorMessage = error.stack || error.error || error.message || error.body || error.data || error;
216
217 console.error(errorMessage);
218
219 res.status(typeof statusCode === 'string' ? 500 : statusCode);
220 res.send({
221 code: statusCode,
222 message: errorMessage,
223 });
224 };
225
226 const handleResponse = async (req, res, response, cacheResponse = false) => {
227 response = CircularJSON.stringify(response);
228
229 res.status(200);
230 res.send(JSON.parse(response));
231
232 if (cacheResponse && config.cache.enabled && req.session.role === 'guest') { // TODO: Replace 'guest' with constant
233 const key = `${req.session.slug}${req.url}`;
234
235 if (config.cache.compress) {
236 response = (await zlib.gzipAsync(Buffer.from(response))).toString('base64');
237 }
238
239 cache.set(key, response);
240 }
241 };
242
243 // Header middleware
244
245 const headerMiddleware = (req, res, next) => {
246 const headers = {
247 'Access-Control-Allow-Origin': '*',
248 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE,OPTIONS',
249 'Access-Control-Expose-Headers': 'X-Slug, X-Role, X-User-Id',
250 Vary: 'Accept-Encoding, X-Api-Token',
251 };
252
253 if (req.headers['access-control-request-headers']) {
254 headers['Access-Control-Allow-Headers'] = req.headers['access-control-request-headers'];
255 }
256
257 res.set(headers);
258
259 if (req.method === 'OPTIONS') {
260 res.sendStatus(200);
261 return;
262 }
263
264 next();
265 };
266
267 // Session middleware
268
269 const jwt = Api.Jwt(config);
270
271 const sessionMiddleware = (req, res, next) => {
272 if (skipAuth(req)) {
273 next();
274 return;
275 }
276
277 const referrer = req.headers.referrer || req.headers.referer;
278
279 if (referrer) {
280 const referrerHostname = new URL(referrer)
281 .hostname.split('.').slice(-2).join('.');
282
283 if (config.api.blacklistReferrer.indexOf(referrerHostname) > -1) {
284 res.status(401);
285 res.send({
286 code: 401,
287 message: 'Not authorised, referrer blacklisted',
288 });
289 return;
290 }
291 }
292
293 const token = req.headers['x-api-token'] || req.query.apiToken || req.session.apiToken;
294
295 if (!token) {
296 res.status(401);
297 res.send({
298 code: 401,
299 message: 'Not authorised, missing token',
300 });
301 return;
302 }
303
304 if (config.api.blacklistToken.indexOf(token) > -1) {
305 res.status(401);
306 res.send({
307 code: 401,
308 message: 'Not authorised, token blacklisted',
309 });
310 return;
311 }
312
313 try {
314 const payload = jwt.verifyToken(token);
315
316 req.session.userId = payload.userId;
317 req.session.slug = payload.slug;
318 req.session.role = payload.role || 'guest'; // TODO: Replace 'guest' with constant
319
320 } catch (error) {
321 res.status(401);
322 res.send({
323 code: 401,
324 message: `Not authorised, token verification failed (${error.message})`,
325 error,
326 });
327 return;
328 }
329
330 if (!req.session.slug) {
331 res.status(401);
332 res.send({
333 code: 401,
334 message: 'Not authorised, missing slug',
335 });
336 return;
337 }
338
339 if (!req.session.role) {
340 req.session.role = 'guest';
341 }
342
343 if (req.session.userId) {
344 res.set('X-User-Id', req.session.userId);
345 }
346
347 res.set('X-Environment', config.environment);
348 res.set('X-Slug', req.session.slug);
349 res.set('X-Role', req.session.role);
350
351 next();
352 };
353
354 // Router
355
356 const router = express.Router();
357
358 const forceHttps = (req, res, next) => {
359 if (
360 (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'] !== 'https') &&
361 (req.headers['cf-visitor'] && JSON.parse(req.headers['cf-visitor']).scheme !== 'https') // Fix for Cloudflare/Heroku flexible SSL
362 ) {
363 res.redirect(301, `https://${req.headers.host}${req.path}`);
364 return;
365 }
366 next();
367 };
368
369 if (config.environment === 'production' && config.forceHttps === true) {
370 if (app.enable) {
371 app.enable('trust proxy');
372 }
373 app.use(forceHttps);
374 }
375
376 app.use(`/${config.api.prefix}`, headerMiddleware, sessionMiddleware, router);
377
378 app.get(`/${config.api.prefix}`, (req, res) => {
379 res.send('<pre> ______\n|A |\n| /\\ |\n| / \\ |\n|( )|\n| )( |\n|______|</pre>');
380 });
381
382 // Context
383
384 const context = {
385 app,
386 router,
387 cache,
388 authMiddleware,
389 permissionMiddleware,
390 cacheMiddleware,
391 asyncMiddleware,
392 getConfig,
393 handleResponse,
394 handleError,
395 };
396
397 // Inject API into context
398
399 Object.keys(Api).forEach((key) => {
400 context[key] = Api[key];
401 });
402
403 if (config.logentriesToken) {
404 context.log = new Logger({
405 token: config.logentriesToken,
406 });
407 }
408
409 const afterResponse = (req, res) => {
410 res.removeListener('finish', afterResponse);
411 res.removeListener('close', afterResponse);
412 };
413
414 if (config.environment !== 'production') {
415 app.use((req, res, next) => {
416 res.on('finish', afterResponse.bind(null, req, res));
417 res.on('close', afterResponse.bind(null, req, res));
418 next();
419 });
420 }
421
422 // Bootstrap Routes
423
424 require('./routes/analytics')(context, config);
425 require('./routes/auth')(context, config);
426 require('./routes/cache')(context, config);
427 require('./routes/config')(context, config);
428 require('./routes/debug')(context, config);
429 require('./routes/ecommerce')(context, config);
430 require('./routes/email')(context, config);
431 require('./routes/embedly')(context, config);
432 require('./routes/entity')(context, config);
433 require('./routes/file')(context, config);
434 require('./routes/metadata')(context, config);
435 require('./routes/pdf')(context, config);
436 require('./routes/schema')(context, config);
437 require('./routes/settings')(context, config);
438 require('./routes/shippo')(context, config);
439 require('./routes/social')(context, config);
440 require('./routes/stripe')(context, config);
441 require('./routes/taxonomy')(context, config);
442 require('./routes/token')(context, config);
443 require('./routes/tools')(context, config);
444 // require('./routes/transcode')(context, config);
445 require('./routes/upload')(context, config);
446 require('./routes/user')(context, config);
447 require('./routes/zencode')(context, config);
448
449 return app;
450}
451
452module.exports = AceApiServer;