-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
340 lines (294 loc) · 10.4 KB
/
server.js
File metadata and controls
340 lines (294 loc) · 10.4 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
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
* POWERBACK Express Application Server
*
* Modernized Express.js application with comprehensive security, authentication,
* and production-ready configuration. Features JWT-only authentication system,
* proper middleware ordering, CORS protection, and structured startup logging.
*
* Key Features:
* - JWT-only authentication (Passport.js removed)
* - Comprehensive security headers (Helmet, CSP, HSTS)
* - Proper CORS configuration with origin whitelisting
* - Database connection with startup validation
* - Background job management
* - Static file serving with CORS support
* - Rate limiting and API protection
*
* @version 1.0.0
* @author fc
*/
// Core Node.js modules
const fs = require('fs');
const path = require('path');
// Environment variable loading
// Development: Load from .env.local file using dotenv
// Production: Load from SECRETS_PATH environment variable
if (process.env.NODE_ENV !== 'production') {
// Development environment - load from .env.local file
require('dotenv').config({ path: path.join(__dirname, '.env.local') });
}
// Third-party libraries
const express = require('express');
const mongoose = require('mongoose');
const helmet = require('helmet');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const { connect } = require('./services/utils/db');
// Application internal modules
const { requireLogger } = require('./services/logger');
const { csrfTokenGenerator } = require('./services/utils');
const { SERVER } = require('./constants');
const {
getTrustProxy,
setPdfHeaders,
createCorsOptions,
createHelmetConfig,
createStaticCorsOptions,
} = require('./config/server.config');
const cspHeaders = require('./cspHeaders');
const routes = require('./routes');
const errorHandler = require('./routes/api/middleware/errorHandler.js');
// Logger initialization (after requireLogger)
const logger = requireLogger(__filename);
// Environment variable loading
// Development: Load from .env.local file using dotenv
// Production: Load from SECRETS_PATH environment variable
if (process.env.NODE_ENV === 'production') {
// Production environment - load from SECRETS_PATH
const secretsPath = process.env.SECRETS_PATH;
if (!secretsPath) {
logger.error(
'SECRETS_PATH environment variable is not set. Configure SECRETS_PATH in your systemd service file.'
);
} else if (!fs.existsSync(secretsPath)) {
logger.error(`SECRETS_PATH file not found: ${secretsPath}`);
} else {
try {
const envContent = fs.readFileSync(secretsPath, 'utf8');
envContent.split('\n').forEach((line) => {
if (line.trim() && !line.startsWith('#')) {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
process.env[key] = valueParts.join('=');
}
}
});
logger.debug(
`Environment variables loaded from SECRETS_PATH (${secretsPath}) at ${new Date().toISOString()}`
);
} catch (err) {
logger.error(`Failed to load environment from SECRETS_PATH:`, err);
}
}
}
const isProductionEnv = process.env.NODE_ENV === 'production';
const runEmailLogging = Boolean(process.env.START_EMAIL_VAR_LOGGING);
const POSITION_PAPER_PATH = process.env.REACT_APP_POSITION_PAPER_PATH; // this comes from the server and is its literal name
const STATIC_PUBLIC_DIR = process.env.STATIC_PUBLIC_DIR;
const corsOptions = createCorsOptions(isProductionEnv);
const staticCorsOptions = createStaticCorsOptions();
const app = express();
// Security: Disable X-Powered-By header to reduce fingerprinting
app.disable('x-powered-by');
app.set('trust proxy', getTrustProxy(isProductionEnv));
// Apply permissive CORS to static files FIRST (before main CORS)
app.use('/static', cors(staticCorsOptions));
// Apply restrictive CORS to API routes
app.use(cors(corsOptions));
// Security headers
app.use(helmet(createHelmetConfig()));
app.use(cspHeaders);
// Safe static directory for Node-only assets
// Environment-aware path: production uses public_html, development uses client/public
const staticDir = isProductionEnv
? STATIC_PUBLIC_DIR
: path.resolve(__dirname, 'client/public');
app.use('/static', express.static(staticDir));
// Serve position paper PDF from clean URL
app.get(`/${POSITION_PAPER_PATH}`, cors(staticCorsOptions), (req, res) => {
const filename = process.env.POSITION_PAPER_FILENAME;
if (!filename) {
logger.error('POSITION_PAPER_FILENAME environment variable is not set');
return res.status(500).json({
error: {
message: 'Position paper PDF configuration error',
status: 500,
},
});
}
const pdfPath = isProductionEnv
? `${STATIC_PUBLIC_DIR}/${filename}`
: path.resolve(__dirname, `client/public/${filename}`);
if (!fs.existsSync(pdfPath)) {
logger.error(`Position paper PDF file not found: ${pdfPath}`);
return res.status(404).json({
error: {
message: 'Position paper PDF not found',
status: 404,
},
});
}
// Disable caching to ensure fresh file is served
setPdfHeaders(res, filename);
res.sendFile(
pdfPath,
{
etag: false,
lastModified: false,
},
(err) => {
if (err) {
logger.error('Error sending position paper PDF:', err);
if (!res.headersSent) {
res.status(500).json({
error: {
message: 'Error serving position paper PDF',
status: 500,
},
});
}
}
}
);
});
// Webhook routes come before any body parsing middleware
app.use('/api/webhooks', require('./routes/api/webhooks'));
app.use(cookieParser());
// CSRF token generation for all routes
app.use(csrfTokenGenerator());
// Now add body parsing middleware
app.use(express.urlencoded({ extended: true }));
app.use('/api/snapshots', require('./routes/snapshots'));
app.use(express.json());
// Log all incoming requests to /api to verify nginx is proxying correctly
app.use('/api', (req, res, next) => {
// Use INFO level so it shows in production logs
logger.info('API request received', {
method: req.method,
path: req.path,
originalUrl: req.originalUrl,
url: req.url,
headers: {
host: req.headers.host,
'x-forwarded-for': req.headers['x-forwarded-for'],
'x-real-ip': req.headers['x-real-ip'],
'user-agent': req.headers['user-agent'],
},
});
next();
});
app.use('/api', routes);
// 404 handler for API routes - must come before error handler
// This ensures API routes return JSON, not HTML
app.use('/api', (req, res, next) => {
logger.warn('API route not found', {
method: req.method,
path: req.path,
originalUrl: req.originalUrl,
});
res.status(404).json({
error: {
message: 'API route not found',
status: 404,
},
});
});
// Add error handling middleware last, after all other middleware and routes
app.use(errorHandler);
// Start the API server only if not in test mode
if (process.env.NODE_ENV !== 'test' && require.main === module) {
const PORT = process.env.PORT ?? SERVER.DEFAULT_PORT;
// Log effective configuration at startup
const {
logStartupConfig,
runApiRouteTests,
startBackgroundJobs,
startServer,
} = require('./lifecycle.js');
logStartupConfig(
runEmailLogging,
isProductionEnv,
corsOptions,
cspHeaders,
helmet,
logger,
app
);
// If MONGODB_TEST_URI is set, connect to test DB first, run tests, then switch to real DB
// Otherwise, connect directly to the real database
const testDbUri = process.env.MONGODB_TEST_URI;
const shouldRunTests = Boolean(process.env.START_API_TESTS);
if (testDbUri && shouldRunTests) {
// Two-step process: test DB first, then real DB
logger.info(
'MONGODB_TEST_URI detected - connecting to test database first for API route tests'
);
const { connectToUri, disconnect } = require('./services/utils/db');
connectToUri(testDbUri, logger)
.then(async () => {
logger.info('Test database connected - running API route tests...');
// Start server on test DB (needed for route tests)
startServer(app, PORT, logger, async () => {
logger.info(
`API Server now listening on PORT ${PORT} (test database)!`
);
if (shouldRunTests) {
await runApiRouteTests(logger);
}
// Disconnect from test database
logger.info('Disconnecting from test database...');
await disconnect();
// Connect to real database
logger.info('Connecting to production/development database...');
connect(logger)
.then(() => {
startBackgroundJobs(logger);
})
.catch((err) => {
logger.error(
'Failed to connect to production/development database:',
err.message
);
process.exit(1);
});
});
})
.catch((err) => {
logger.error('Failed to connect to test database:', err.message);
process.exit(1);
});
} else {
// Normal flow: connect directly to real database
connect(logger)
.then(() => {
// Start the server
startServer(app, PORT, logger, async () => {
logger.info(`API Server now listening on PORT ${PORT}!`);
if (isProductionEnv)
setInterval(() => {
logger.info('(heartbeat) app running');
}, 60000);
// Only start jobs after successful DB connection and server start
startBackgroundJobs(logger);
// Run API route tests if enabled (but no test DB specified)
if (shouldRunTests && !testDbUri) {
await runApiRouteTests(logger);
}
});
})
.catch((err) => {
logger.error('Failed to connect to database:', err.message);
process.exit(1);
});
}
}
// Graceful shutdown
async function shutdown() {
logger.info('Shutting down, disconnecting from MongoDB…');
await mongoose.disconnect();
process.exit(0);
}
process.on('SIGINT', shutdown); // Ctrl+C
process.on('SIGTERM', shutdown); // `kill` or container stop
// Export app for testing
module.exports = app;