-
Notifications
You must be signed in to change notification settings - Fork 9
/
app.js
305 lines (260 loc) · 15.1 KB
/
app.js
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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
const RunMode = require('run-mode');
if (!RunMode.isValid()) {
console.log("FATAL ERROR: Unknown NODE_ENV '" + process.env.NODE_ENV + "'. Must be one of: " + RunMode.getValidModes());
process.exit(1);
}
if (RunMode.isTest()) {
process.env['NEW_RELIC_APP_NAME'] = "ESDR Test";
}
const nr = require('newrelic');
const log4js = require('log4js');
log4js.configure('log4js-config-' + RunMode.get() + '.json');
const log = log4js.getLogger('esdr');
log.info("Run Mode: " + RunMode.get());
log.info("New Relic enabled for app: " + ((nr.agent && nr.agent.config && nr.agent.config.app_name) ? nr.agent.config.app_name : "unknown"));
// dependencies
const config = require('./config');
const BodyTrackDatastore = require('bodytrack-datastore');
const express = require('express');
const app = express();
const cors = require('cors');
const expressHandlebars = require('express-handlebars');
const path = require('path');
const favicon = require('serve-favicon');
const compress = require('compression');
const bodyParser = require('body-parser');
const passport = require('passport');
const Database = require('./models/Database');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const SessionStore = require('express-mysql-session');
const httpStatus = require('http-status');
// instantiate the datastore
// noinspection JSUnusedLocalSymbols
const datastore = new BodyTrackDatastore({
binDir : config.get("datastore:binDirectory"),
dataDir : config.get("datastore:dataDirectory")
});
// decorate express.response with JSend methods
// noinspection JSCheckFunctionSignatures
require('jsend-utils').decorateExpressResponse(require('express').response);
const gracefulExit = function() {
log.info("Shutting down...");
process.exit(0);
};
// If the Node process ends, then do a graceful shutdown
process
.on('SIGINT', gracefulExit)
.on('SIGTERM', gracefulExit);
// start by initializing the database and getting a reference to it
Database.create(function(err, db) {
if (err) {
log.error("Failed to initialize the database!" + err);
}
else {
log.info("Database initialized, starting app server...");
// configure the app
try {
// VIEW -------------------------------------------------------------------------------------------------------------
// setup view engine
const viewsDir = path.join(__dirname, 'views');
app.set('views', viewsDir);
// noinspection JSUnusedGlobalSymbols
const handlebars = expressHandlebars.create({
extname : '.hbs',
defaultLayout : 'main-layout',
layoutsDir : path.join(viewsDir, "layouts"),
partialsDir : path.join(viewsDir, "partials"),
helpers : {
// Got this from http://stackoverflow.com/a/9405113
ifEqual : function(v1, v2, options) {
if (v1 === v2) {
return options.fn(this);
}
return options.inverse(this);
}
}
});
app.engine('hbs', handlebars.engine);
app.set('view engine', '.hbs');
app.set('view cache', RunMode.isStaging() || RunMode.isProduction()); // only cache views in staging and production
log.info("View cache enabled = " + app.enabled('view cache'));
// MIDDLEWARE -------------------------------------------------------------------------------------------------
const oauthServer = require('./middleware/oauth2')(db.users, db.tokens); // create and configure OAuth2 server
const error_handlers = require('./middleware/error_handlers');
app.use(favicon(path.join(__dirname, 'public/favicon.ico'))); // favicon serving
app.use(compress()); // enables gzip compression
// Catch requests to /explore and redirect before doing anything else. We need this here because the /explore
// stuff is under /public, so we need to intercept here before the static file serving rule below
app.use('/explore', function(req, res) {
res.render('explore', { title : "Redirecting to environmentaldata.org..." });
});
app.use(express.static(path.join(__dirname, 'public'))); // static file serving
// configure request logging, if enabled (do this AFTER the static file serving so we don't log those)
if (config.get("requestLogging:isEnabled")) {
const requestLogger = require('morgan');
// enable logging of the user ID, if authenticated
requestLogger.token('uid', function(req) {
if (req['user']) {
return req['user'].id;
}
return '-';
});
// we'll only log to a file if we're in staging or production
if (RunMode.isStaging() || RunMode.isProduction()) {
// create a write stream (in append mode)
const fs = require('fs');
const logFile = config.get("requestLogging:logFile");
log.info("HTTP access log: " + logFile);
const accessLogStream = fs.createWriteStream(logFile, { flags : 'a' });
// get the correct remote address from the X-Forwarded-For header
// noinspection JSCheckFunctionSignatures
requestLogger.token('remote-addr', function(req) {
return req.headers['x-forwarded-for'];
});
// This is just the "combined" format with response time and UID appended to the end
const logFormat = ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :response-time ms :uid';
app.use(requestLogger(logFormat, { stream : accessLogStream }));
}
else {
app.use(requestLogger(':method :url :status :res[content-length] :response-time ms :uid')); // simple console request logging when in non-production mode
}
}
else {
log.info("HTTP access logging is DISABLED (see ESDR config setting requestLogging:isEnabled)");
}
app.use(bodyParser.urlencoded({ extended : true })); // form parsing
app.use(bodyParser.json({ limit : '25mb' })); // json body parsing (25 MB limit)
app.use(function(err, req, res, next) { // function MUST have arity 4 here!
// catch body parser error (beefed up version of http://stackoverflow.com/a/15819808/703200)
if (err) {
const statusCode = err.status || httpStatus.INTERNAL_SERVER_ERROR;
const message = err.message || (statusCode < httpStatus.INTERNAL_SERVER_ERROR ? "Bad Request" : "Internal Server Error");
const data = err;
// Manually set the CORS header here--I couldn't figure out how to get it working with the CORS
// middleware. We need to do this so that client-side AJAX uploads which try to send files larger than
// the limit get a proper 413 response. Without it, the browser will complain that the CORS header is
// missing.
res.set('Access-Control-Allow-Origin', '*');
if (statusCode < httpStatus.INTERNAL_SERVER_ERROR) {
res.jsendClientError(message, data, statusCode);
}
else {
res.jsendServerError(message, data, statusCode);
}
}
else {
next();
}
});
// configure passport
const authHelper = require('./middleware/auth')(db.clients, db.users, db.tokens, db.feeds);
// CUSTOM MIDDLEWARE ------------------------------------------------------------------------------------------
if (RunMode.isStaging() || RunMode.isProduction()) {
app.set('trust proxy', 1); // trust first proxy
}
// define the various middleware required for routes which need session support
const sessionSupport = [
cookieParser(), // cookie parsing--MUST come before setting up session middleware!
session({ // configure support for storing sessions in the database
key : config.get("cookie:name"),
secret : config.get("cookie:secret"),
store : new SessionStore({
host : config.get("database:host"),
port : config.get("database:port"),
database : config.get("database:database"),
user : config.get("database:username"),
password : config.get("database:password")
}),
rolling : false,
cookie : {
httpOnly : true,
secure : config.get("cookie:isSecure") // whether to enable secure cookies (must be true when using HTTPS)
},
proxy : RunMode.isStaging() || RunMode.isProduction(), // we use a proxy in staging and production
saveUninitialized : true,
resave : true,
unset : "destroy"
}),
passport.initialize(), // initialize passport (must come AFTER session middleware)
passport.session(), // enable session support for passport
function(req, res, next) {
log.debug("req.isAuthenticated()=[" + req.isAuthenticated() + "]");
res.locals.isAuthenticated = req.isAuthenticated();
if (req.isAuthenticated()) {
res.locals.user = {
id : req.user.id
};
delete req.session.redirectToAfterLogin;
delete res.locals.redirectToAfterLogin;
}
else {
// expose the redirectToAfterLogin page to the view
res.locals.redirectToAfterLogin = req.session.redirectToAfterLogin;
}
next();
},
require('./middleware/accessToken').refreshAccessToken()
];
// define the various middleware required for routes which don't need (and should not have!) session support
const noSessionSupport = [
passport.initialize() // initialize passport
];
// create the FeedRouteHelper
const FeedRouteHelper = require('./routes/api/feed-route-helper');
const feedRouteHelper = new FeedRouteHelper(db.feeds);
// define CORS options and apply CORS to specific route groups
const corsSupport = cors({
origin : '*'
});
// ensure the user is authenticated before serving up the page
const ensureAuthenticated = function(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
// remember where the user was trying to go and then redirect to the login page
req.session.redirectToAfterLogin = req.originalUrl;
res.redirect('/login')
};
// ROUTING ----------------------------------------------------------------------------------------------------
app.use('/oauth/*', noSessionSupport, corsSupport);
app.use('/api/v1/*', noSessionSupport, corsSupport);
app.use('/oauth', require('./routes/oauth')(oauthServer));
app.use('/api/v1/time', require('./routes/api/time'));
app.use('/api/v1/clients', require('./routes/api/clients')(db.clients));
app.use('/api/v1/users', require('./routes/api/users')(db.users, db.userProperties));
app.use('/api/v1/products', require('./routes/api/products')(db.products, db.devices));
app.use('/api/v1/devices', require('./routes/api/devices')(db.devices, db.deviceProperties, db.feeds));
app.use('/api/v1/feed', require('./routes/api/feed')(db.feeds, feedRouteHelper, authHelper));
app.use('/api/v1/feeds', require('./routes/api/feeds')(db.feeds, db.feedProperties, feedRouteHelper));
app.use('/api/v1/multifeeds', require('./routes/api/multifeeds')(db.feeds, db.multifeeds));
app.use('/api/v1/mirrors', require('./routes/api/mirrors')(db.products, db.mirrorRegistrations));
app.use('/api/v1/user-verification', require('./routes/api/user-verification')(db.users));
app.use('/api/v1/password-reset', require('./routes/api/password-reset')(db.users));
// configure routing
app.use('/signup', sessionSupport, require('./routes/signup'));
app.use('/login', sessionSupport, require('./routes/login'));
app.use('/logout', sessionSupport, require('./routes/logout')());
app.use('/verification', sessionSupport, require('./routes/verification'));
app.use('/password-reset', sessionSupport, require('./routes/password-reset'));
app.use('/access-token', sessionSupport, require('./routes/access-token')(db.tokens));
app.use('/home', sessionSupport, ensureAuthenticated, require('./routes/home/index'));
app.use('/', sessionSupport, require('./routes/index'));
// ERROR HANDLERS ---------------------------------------------------------------------------------------------
// custom 404
// noinspection JSCheckFunctionSignatures
app.use(error_handlers.http404);
// dev and prod should handle errors differently: e.g. don't show stacktraces in staging or production
app.use(RunMode.isStaging() || RunMode.isProduction() ? error_handlers.prod : error_handlers.dev);
// ------------------------------------------------------------------------------------------------------------
// set the port and start the server
app.set('port', config.get("server:port"));
const server = app.listen(app.get('port'), function() {
log.info('Express server listening on port ' + server.address().port);
});
}
catch (err) {
log.error("Sever initialization failed ", err.message);
}
}
});