mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-09-20 20:32:47 +02:00
318 lines
9.5 KiB
JavaScript
318 lines
9.5 KiB
JavaScript
/* eslint-disable no-param-reassign, no-console, strict, global-require, no-unused-vars, func-names */
|
|
|
|
'use strict';
|
|
|
|
/* ********* Polyfills (for node) ********* */
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
require('@babel/register')({
|
|
// This will override `node_modules` ignoring - you can alternatively pass
|
|
// an array of strings to be explicitly matched or a regex / glob
|
|
ignore: [
|
|
/node_modules\/(?!react-leaflet|@babel\/runtime\/helpers\/esm|@digitransit-util)/,
|
|
],
|
|
});
|
|
|
|
global.fetch = require('node-fetch');
|
|
const proxy = require('express-http-proxy');
|
|
|
|
global.self = { fetch: global.fetch };
|
|
|
|
let Raven;
|
|
const devhost = '';
|
|
|
|
if (process.env.NODE_ENV === 'production' && process.env.SENTRY_SECRET_DSN) {
|
|
Raven = require('raven');
|
|
Raven.config(process.env.SENTRY_SECRET_DSN, {
|
|
captureUnhandledRejections: true,
|
|
}).install();
|
|
} else {
|
|
process.on('unhandledRejection', (reason, p) => {
|
|
console.log('Unhandled Rejection at:', p, 'reason:', reason);
|
|
});
|
|
}
|
|
|
|
/* ********* Server ********* */
|
|
const express = require('express');
|
|
const expressStaticGzip = require('express-static-gzip');
|
|
const cookieParser = require('cookie-parser');
|
|
const bodyParser = require('body-parser');
|
|
const logger = require('morgan');
|
|
const { retryFetch } = require('../app/util/fetchUtils');
|
|
const config = require('../app/config').getConfiguration();
|
|
|
|
/* ********* Global ********* */
|
|
const port = config.PORT || 8080;
|
|
const app = express();
|
|
const { indexPath, hostnames } = config;
|
|
|
|
/* Setup functions */
|
|
function setUpOpenId() {
|
|
const setUpOIDC = require('./passport-openid-connect/openidConnect').default;
|
|
if (process.env.DEBUGLOGGING) {
|
|
app.use(logger('dev'));
|
|
}
|
|
app.use(bodyParser.json());
|
|
app.use(bodyParser.urlencoded({ extended: false }));
|
|
app.use(cookieParser());
|
|
app.use(
|
|
require('helmet')({
|
|
contentSecurityPolicy: false,
|
|
referrerPolicy: false,
|
|
expectCt: false,
|
|
}),
|
|
);
|
|
setUpOIDC(app, port, indexPath, hostnames);
|
|
}
|
|
|
|
function setUpStaticFolders() {
|
|
// First set up a specific path for sw.js
|
|
if (process.env.ASSET_URL) {
|
|
const swText = fs.readFileSync(
|
|
path.join(process.cwd(), '_static', 'sw.js'),
|
|
{ encoding: 'utf8' },
|
|
);
|
|
const injectionPoint = swText.indexOf(';') + 2;
|
|
const swPreText = swText.substring(0, injectionPoint);
|
|
const swPostText = swText.substring(injectionPoint);
|
|
const swInjectionText = fs
|
|
.readFileSync(path.join(process.cwd(), 'server', 'swInjection.js'), {
|
|
encoding: 'utf8',
|
|
})
|
|
.replace(/ASSET_URL/g, process.env.ASSET_URL);
|
|
const swTextInjected = swPreText + swInjectionText + swPostText;
|
|
|
|
app.get(`${config.APP_PATH}/sw.js`, (req, res) => {
|
|
res.setHeader('Cache-Control', 'public, max-age=0');
|
|
res.setHeader('Content-type', 'application/javascript; charset=UTF-8');
|
|
res.send(swTextInjected);
|
|
});
|
|
}
|
|
|
|
const staticFolder = path.join(process.cwd(), '_static');
|
|
// Sert cache for 1 week
|
|
const oneDay = 86400000;
|
|
app.use(
|
|
config.APP_PATH,
|
|
expressStaticGzip(staticFolder, {
|
|
enableBrotli: true,
|
|
index: false,
|
|
maxAge: 14 * oneDay,
|
|
setHeaders(res, reqPath) {
|
|
if (
|
|
reqPath.toLowerCase().includes('sw.js') ||
|
|
reqPath.toLowerCase().includes('appcache')
|
|
) {
|
|
res.setHeader('Cache-Control', 'public, max-age=0');
|
|
}
|
|
// Always set cors header
|
|
res.header('Access-Control-Allow-Origin', '*');
|
|
},
|
|
}),
|
|
);
|
|
|
|
if (config.localStorageEmitter) {
|
|
app.use(
|
|
'/local-storage-emitter',
|
|
express.static(path.join(staticFolder, 'emitter')),
|
|
);
|
|
}
|
|
}
|
|
|
|
function setUpMiddleware() {
|
|
app.use(cookieParser());
|
|
app.use(bodyParser.raw());
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const hotloadPort = process.env.HOT_LOAD_PORT || 9000;
|
|
// proxy for dev-bundle
|
|
app.use('/proxy/', proxy(`http://localhost:${hotloadPort}/`));
|
|
}
|
|
}
|
|
|
|
function onError(err, req, res) {
|
|
res.statusCode = 500;
|
|
res.end(err.message + err.stack);
|
|
}
|
|
|
|
function setUpRaven() {
|
|
if (process.env.NODE_ENV === 'production' && process.env.SENTRY_SECRET_DSN) {
|
|
app.use(Raven.requestHandler());
|
|
}
|
|
}
|
|
|
|
function setUpErrorHandling() {
|
|
if (process.env.NODE_ENV === 'production' && process.env.SENTRY_SECRET_DSN) {
|
|
app.use(Raven.errorHandler());
|
|
}
|
|
|
|
app.use(onError);
|
|
}
|
|
|
|
function setUpRoutes() {
|
|
app.use(
|
|
['/', '/fi/', '/en/', '/sv/', '/ru/', '/slangi/'],
|
|
require('./reittiopasParameterMiddleware').default,
|
|
);
|
|
app.use(require('../app/server').default);
|
|
|
|
// Make sure req has the correct hostname extracted from the proxy info
|
|
app.enable('trust proxy');
|
|
}
|
|
|
|
function setUpAvailableRouteTimetables() {
|
|
return new Promise(resolve => {
|
|
// Stores available route pdf names to config.availableRouteTimetables.HSL
|
|
// All routes don't have available pdf and some have their timetable inside other route
|
|
// so there is a mapping between route's gtfsId (without HSL: part) and similar gtfsId of
|
|
// route that contains timetables
|
|
if (config.timetables.HSL) {
|
|
// try to fetch available route timetables every four seconds with 4 retries
|
|
retryFetch(
|
|
`${config.URL.ROUTE_TIMETABLES.HSL}routes.json`,
|
|
{},
|
|
4,
|
|
4000,
|
|
config,
|
|
)
|
|
.then(res => res.json())
|
|
.then(
|
|
result => {
|
|
config.timetables.HSL.setAvailableRouteTimetables(result);
|
|
console.log('availableRouteTimetables.HSL loaded');
|
|
resolve();
|
|
},
|
|
err => {
|
|
console.log(err);
|
|
// If after 5 tries no timetable data is found, start server anyway
|
|
resolve();
|
|
console.log('availableRouteTimetables.HSL loader failed');
|
|
// Continue attempts to fetch available routes in the background for one day once every minute
|
|
retryFetch(
|
|
`${config.URL.ROUTE_TIMETABLES.HSL}routes.json`,
|
|
{},
|
|
1440,
|
|
60000,
|
|
config,
|
|
)
|
|
.then(res => res.json())
|
|
.then(
|
|
result => {
|
|
config.timetables.HSL.setAvailableRouteTimetables(result);
|
|
console.log(
|
|
'availableRouteTimetables.HSL loaded after retry',
|
|
);
|
|
},
|
|
error => {
|
|
console.log(error);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
function processTicketTypeResult(result) {
|
|
const resultData = result.data;
|
|
if (config.availableTickets) {
|
|
if (resultData && Array.isArray(resultData.ticketTypes)) {
|
|
resultData.ticketTypes.forEach(ticket => {
|
|
const ticketFeed = ticket.fareId.split(':')[0];
|
|
if (config.availableTickets[ticketFeed] === undefined) {
|
|
config.availableTickets[ticketFeed] = {};
|
|
}
|
|
config.availableTickets[ticketFeed][ticket.fareId] = {
|
|
price: ticket.price,
|
|
zones: ticket.zones,
|
|
};
|
|
});
|
|
console.log('availableTickets loaded');
|
|
} else {
|
|
console.log('could not load availableTickets, result was invalid');
|
|
}
|
|
} else {
|
|
console.log(
|
|
'availableTickets not loaded, missing availableTickets object from config-file',
|
|
);
|
|
}
|
|
}
|
|
|
|
function setUpAvailableTickets() {
|
|
return new Promise(resolve => {
|
|
const options = {
|
|
method: 'POST',
|
|
body: '{ ticketTypes { price fareId zones } }',
|
|
headers: { 'Content-Type': 'application/graphql' },
|
|
};
|
|
const queryParameters = config.hasAPISubscriptionQueryParameter
|
|
? `?${config.API_SUBSCRIPTION_QUERY_PARAMETER_NAME}=${config.API_SUBSCRIPTION_TOKEN}`
|
|
: '';
|
|
// try to fetch available ticketTypes every four seconds with 4 retries
|
|
retryFetch(
|
|
`${config.URL.OTP}index/graphql${queryParameters}`,
|
|
options,
|
|
4,
|
|
4000,
|
|
)
|
|
.then(res => res.json())
|
|
.then(
|
|
result => {
|
|
processTicketTypeResult(result);
|
|
resolve();
|
|
},
|
|
err => {
|
|
console.log(err);
|
|
if (process.env.BASE_CONFIG) {
|
|
// Patching of availableTickets into cached configs would not work with BASE_CONFIG
|
|
// if availableTickets are fetched after launch
|
|
console.log('failed to load availableTickets at launch, exiting');
|
|
process.exit(1);
|
|
} else {
|
|
// If after 5 tries no available ticketTypes are found, start server anyway
|
|
resolve();
|
|
console.log('failed to load availableTickets at launch, retrying');
|
|
// Continue attempts to fetch available ticketTypes in the background for one day once every minute
|
|
retryFetch(
|
|
`${config.URL.OTP}index/graphql${queryParameters}`,
|
|
options,
|
|
1440,
|
|
60000,
|
|
)
|
|
.then(res => res.json())
|
|
.then(
|
|
result => {
|
|
processTicketTypeResult(result);
|
|
},
|
|
error => {
|
|
console.log(error);
|
|
},
|
|
);
|
|
}
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
function startServer() {
|
|
const server = app.listen(port, () =>
|
|
console.log('Digitransit-ui available on port %d', server.address().port),
|
|
);
|
|
}
|
|
|
|
/* ********* Init ********* */
|
|
if (process.env.OIDC_CLIENT_ID) {
|
|
setUpOpenId();
|
|
}
|
|
setUpRaven();
|
|
setUpStaticFolders();
|
|
setUpMiddleware();
|
|
setUpRoutes();
|
|
setUpErrorHandling();
|
|
Promise.all([
|
|
setUpAvailableRouteTimetables(),
|
|
setUpAvailableTickets(),
|
|
]).then(() => startServer());
|
|
|
|
module.exports.app = app;
|