-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathexpress.js
More file actions
265 lines (249 loc) · 7.83 KB
/
Copy pathexpress.js
File metadata and controls
265 lines (249 loc) · 7.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
/**
* Module dependencies.
*/
import express from 'express';
import compress from 'compression';
import methodOverride from 'method-override';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import path from 'path';
import _ from 'lodash';
import lusca from 'lusca';
import cors from 'cors';
import morgan from 'morgan';
import fs from 'fs';
import YAML from 'js-yaml';
import swaggerUi from 'swagger-ui-express';
import config from '../../config/index.js';
import logger from './logger.js';
import requestId from '../middlewares/requestId.js';
import sentry from './sentry.js';
import AnalyticsService from './analytics.js';
import analyticsMiddleware from '../middlewares/analytics.js';
/**
* Initialize Swagger
*/
const initSwagger = (app) => {
if (config.swagger.enable) {
// Merge files.
try {
const contents = config.files.swagger.map((filePath) => YAML.load(fs.readFileSync(filePath).toString()));
const merged = contents.reduce(_.merge);
fs.writeFile('./public/swagger.yml', YAML.dump(merged), (error) => {
if (error) {
throw error;
}
});
} catch (e) {
throw new Error(e);
}
app.use(config.swagger.options.swaggerUrl, express.static('./public/swagger.yml'));
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(null, config.swagger.options));
}
};
/**
* Initialize local variables
*/
const initLocalVariables = (app) => {
// Setting application local variables
app.locals.title = config.app.title;
app.locals.description = config.app.description;
if (config.secure && config.secure.ssl === true) app.locals.secure = config.secure.ssl;
app.locals.keywords = config.app.keywords;
app.locals.env = process.env.NODE_ENV;
app.locals.domain = config.domain;
// Passing the request url to environment locals
app.use((req, res, next) => {
res.locals.host = `${req.protocol}://${req.hostname}`;
res.locals.url = `${req.protocol}://${req.headers.host}${req.originalUrl}`;
next();
});
};
/**
* Initialize pre-parser routes (mounted before body parsing and CSRF)
* @param {object} app - express application instance
* @returns {Promise<void>}
*/
const initPreParserRoutes = async (app) => {
for (const routePath of config.files.preRoutes) {
try {
const route = await import(path.resolve(routePath));
if (typeof route.default !== 'function') {
console.warn(`Pre-parser route ${routePath} does not export a default function`);
continue;
}
route.default(app);
} catch (err) {
console.error(`Failed to load pre-parser route: ${routePath}`, err);
throw err;
}
}
};
/**
* Initialize application middleware.
* @param {object} app - express application instance
*/
const initMiddleware = (app) => {
// Trust proxy — required behind reverse proxies (Nginx, Traefik, LB)
if (config.trust && config.trust.proxy) app.set('trust proxy', true);
// Should be placed before express.static
app.use(
compress({
filter(req, res) {
return /json|text|javascript|css|font|svg/.test(res.getHeader('Content-Type'));
},
level: 9,
}),
);
// Enable logger (morgan) if enabled in the configuration file
if (_.has(config, 'log.format') && process.env.NODE_ENV !== 'test') {
morgan.token('id', (req) => _.get(req, 'user.id') || 'Unknown id');
morgan.token('email', (req) => _.get(req, 'user.email') || 'Unknown email');
morgan.token('requestId', (req) => req.id || '-');
morgan.token('orgId', (req) => _.get(req, 'organization.id') || _.get(req, 'organization._id', '-'));
app.use(morgan(logger.getLogFormat(), logger.getMorganOptions()));
}
// Request body parsing middleware should be above methodOverride
app.use(
express.urlencoded({
extended: true,
}),
);
app.use(express.json(config.bodyParser));
app.use(methodOverride());
// Add the cookie parser and flash middleware
app.use(cookieParser());
app.use(
cors({
origin: config.cors.origin || [],
credentials: config.cors.credentials || false,
optionsSuccessStatus: config.cors.optionsSuccessStatus || 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
}),
);
};
/**
* Invoke modules server configuration
*/
const initModulesConfiguration = async (app) => {
for (const configPath of config.files.configs) {
const route = await import(path.resolve(configPath));
await route.default(app);
}
};
/**
* Configure Helmet headers configuration
*/
const initHelmetHeaders = (app) => {
const SIX_MONTHS = 15778476000;
app.use(helmet.frameguard());
app.use(helmet.xssFilter());
app.use(helmet.noSniff());
app.use(helmet.ieNoOpen());
app.use(
helmet.hsts({
maxAge: SIX_MONTHS,
includeSubDomains: true,
force: true,
}),
);
app.disable('x-powered-by');
};
/**
* Configure the modules static routes
*/
const initModulesClientRoutes = (app) => {
app.use('/', express.static(path.resolve('./public'), { maxAge: 86400000 }));
};
/**
* Configure the modules ACL policies via auto-discovery.
* Each policy file exports named ability builder functions
* that are registered with the central policy middleware.
* @returns {Promise<void>}
*/
const initModulesServerPolicies = async () => {
const policyMod = await import('../middlewares/policy.js');
await policyMod.default.discoverPolicies(config.files.policies);
};
/**
* Configure the modules server routes
*/
const initModulesServerRoutes = async (app) => {
for (const routePath of config.files.routes) {
const route = await import(path.resolve(routePath));
route.default(app);
}
app.get('/{*path}', (req, res) => {
res.send('<center><br /><h1>Devkit Node Api</h1><h3>Available on <a href="/api/tasks">/api</a>. #LetsGetTogether</h3></center>');
});
};
/**
* Configure error handling
*/
const initErrorRoutes = (app) => {
app.use((err, req, res, next) => {
if (!err) return next();
console.error(err.stack);
res.status(err.status || 500).send({
message: err.message,
code: err.code,
});
});
};
/**
* Initialize the Express application.
*
* @returns {Promise<import('express').Express>} Configured Express app
*/
const init = async () => {
// Initialize express app
const app = express();
// Initialize modules swagger doc
initSwagger(app);
// Initialize local variables
initLocalVariables(app);
// Assign a unique request ID before any route registration
app.use(requestId);
// Initialize pre-parser routes (before body parsing, CSRF, and analytics)
await initPreParserRoutes(app);
// Initialize analytics (PostHog) and mount auto-capture middleware
// Mounted after pre-parser routes so webhooks are not tracked
// Wrapped in try/catch so analytics failures never prevent app startup
try {
await AnalyticsService.init();
app.use(analyticsMiddleware);
} catch (err) {
logger.warn('[analytics] init failed, running without analytics: %s', err.message);
}
// Initialize Express middleware
initMiddleware(app);
// Initialize Helmet security headers
initHelmetHeaders(app);
// Initialize modules static client routes,
initModulesClientRoutes(app);
// add lusca csrf
app.use(lusca(config.csrf));
// Initialize Modules configuration
await initModulesConfiguration(app);
// Initialize modules server authorization policies
await initModulesServerPolicies(app);
// Initialize modules server routes
await initModulesServerRoutes(app);
// Mount Sentry error handler (must be after routes, before generic error handler)
sentry.setupExpressErrorHandler(app);
// Initialize error routes
initErrorRoutes(app);
return app;
};
export default {
initSwagger,
initLocalVariables,
initPreParserRoutes,
initMiddleware,
initModulesConfiguration,
initHelmetHeaders,
initModulesClientRoutes,
initModulesServerPolicies,
initModulesServerRoutes,
initErrorRoutes,
init,
};