import 'babel-polyfill'; import path from 'path'; import express from 'express'; import cookieParser from 'cookie-parser'; import bodyParser from 'body-parser'; import expressJwt from 'express-jwt'; import expressGraphQL from 'express-graphql'; import jwt from 'jsonwebtoken'; import React from 'react'; import ReactDOM from 'react-dom/server'; import UniversalRouter from 'universal-router'; import PrettyError from 'pretty-error'; import Bouncer from 'express-bouncer'; import App from './components/App'; import Html from './components/Html'; import { ErrorPageWithoutStyle } from './routes/error/ErrorPage'; import errorPageStyle from './routes/error/ErrorPage.css'; import passport from './core/passport'; import admin from './core/admin'; import models from './data/models'; import schema from './data/schema'; import routes from './routes'; import assets from './assets'; // eslint-disable-line import/no-unresolved import { port, auth, starters, mains, exportApiKey } from './config'; import Person from './data/models/Person'; import Settings from './data/models/Settings'; import Rollbar from './core/rollbar'; import { sendSlackMsg, sendSlackMsgWithDebounce } from './core/slack'; import sequelize from './data/sequelize'; import { sendRSVPEmailWithDebounce } from './core/emailer'; const app = express(); Rollbar.init(); const bouncer = Bouncer(10 * 60 * 1000, 60 * 60 * 1000, 5); bouncer.blocked = (req, res, next, remaining) => { let time = Math.ceil(remaining / 1000); let suffix = 'seconds'; if (time > 120) { time = Math.ceil(time / 60); suffix = 'minutes'; } const body = `You have made too many incorrect attempts. Please wait ${time} ${suffix}.`; const uAgent = req.headers['user-agent']; const ip = req.headers['x-forwarded-for']; sendSlackMsg(`Bouncer block\nIP address: ${ip}\nUser agent: ${uAgent}\nDuration: ${time} ${suffix}`, '#wedding-site'); res.status(429).send(body); }; // Tell any CSS tooling (such as Material UI) to use all vendor prefixes if the // user agent is not known. // ----------------------------------------------------------------------------- global.navigator = global.navigator || {}; global.navigator.userAgent = global.navigator.userAgent || 'all'; // // Register Node.js middleware // ----------------------------------------------------------------------------- app.use(express.static(path.join(__dirname, 'public'))); app.use(cookieParser()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); // // Authentication // ----------------------------------------------------------------------------- app.use(expressJwt({ secret: auth.jwt.secret, credentialsRequired: false, getToken: req => req.cookies.id_token, })); app.use(passport.initialize()); if (process.env.NODE_ENV !== 'production') { app.enable('trust proxy'); } app.get('/login/facebook', passport.authenticate('facebook', { scope: ['email', 'user_location'], session: false }), ); app.get('/login/facebook/return', passport.authenticate('facebook', { failureRedirect: '/login', session: false }), (req, res) => { const expiresIn = 60 * 60 * 24 * 180; // 180 days const token = jwt.sign(req.user, auth.jwt.secret, { expiresIn }); res.cookie('id_token', token, { maxAge: 1000 * expiresIn, httpOnly: true }); res.redirect('/'); }, ); // // Register API middleware // ----------------------------------------------------------------------------- app.use('/graphql', expressGraphQL(req => ({ schema, graphiql: process.env.NODE_ENV !== 'production', rootValue: { request: req }, pretty: process.env.NODE_ENV !== 'production', }))); // // Register RSVP routes // ----------------------------------------------------------------------------- // TODO: This could do with cleaning up, to prevent duplicate bouncer tests and Person queries app.get('/:code', bouncer.block, (req, res, next) => { try { // Hijack router to test if url is a RSVP code if (req.params.code.length === 4) { // Possibly a password Person.findAll({ where: { password: req.params.code } }) .then(data => { if (data.length > 0) { // It is a password bouncer.reset(req); res.redirect(`/rsvp/${req.params.code}`); } else { next(); } }).catch((err) => { Rollbar.handleError(err); next(err); }); } else { bouncer.reset(req); next(); } } catch (err) { Rollbar.handleError(err); next(err); } }); app.post('/rsvp', (req, res) => { res.redirect(`/rsvp/${req.body.code}`); }); app.get('/rsvp/:code', bouncer.block, (req, res, next) => { Person.findAll({ where: { password: req.params.code } }) .then(data => { if (data.length > 0) { bouncer.reset(req); } next(); }).catch((err) => { Rollbar.handleError(err); next(err); }); }); app.post('/rsvp/save', (req, res) => { try { Person.findOne({ where: { key: req.body.key }, }).then(data => { const endDate = new Date(data.enddate); if (new Date() < endDate) { const update = {}; update[req.body.prop] = req.body.value; return Person.update(update, { where: { key: req.body.key, }, }).then(() => { if (data.completed) { const attending = `\nAttending: ${data.attending ? 'Yes' : 'No'}`; const starter = (data.attending && data.starter !== -1) ? `\nStarter: ${starters[data.starter]}` : ''; const main = (data.attending && data.main !== -1) ? `\nMain: ${mains[data.main]}` : ''; const dietary = (data.attending && data.dietary) ? `\nDietary requirements: ${data.dietary}` : ''; sendSlackMsgWithDebounce(`${data.firstname} ${data.lastname} has saved their RSVP:${attending}${starter}${main}${dietary}`, '#wedding-rsvps', req.body.key); sendRSVPEmailWithDebounce(data); } return res.json({ success: true, prop: req.body.prop, }); }).catch(err => { Rollbar.handleError(err); res.json({ success: false, err, }); }); } return res.json({ success: false, err: 'RSVP end date has elapsed', }); }).catch(err => { Rollbar.handleError(err); res.json({ success: false, err, }); }); } catch (err) { Rollbar.handleError(err); res.json({ success: false, err, }); } }); // // Register Admin routes // ----------------------------------------------------------------------------- // app.get('/login', (req, res, next) => { try { if (req.user && admin(req.user.email)) { res.redirect('/admin'); } else { next(); } } catch (err) { Rollbar.handleError(err); next(err); } }); app.use('/admin*', (req, res, next) => { try { // Allow if api key is valid and path is /admin/people/export if (req.headers['x-api-key'] === exportApiKey && req.originalUrl === '/admin/people/export') { next(); } else if (!req.user || !admin(req.user.email)) { res.redirect(401, '/login'); } else { next(); } } catch (err) { Rollbar.handleError(err); next(err); } }); app.post('/admin/person', (req, res, next) => { try { const data = { firstname: req.body.firstname, lastname: req.body.lastname, email: req.body.email, ceremony: req.body.ceremony, enddate: new Date(req.body.enddate), }; if (req.body.password && req.body.password !== '') { data.password = req.body.password; } let promise; if (req.body.key) { promise = Person.update(data, { where: { key: req.body.key }, }); } else { promise = Person.create(data); } promise.then(() => { res.redirect('/admin/people'); }).catch(err => { Rollbar.handleError(err); next(err); }); } catch (err) { Rollbar.handleError(err); next(err); } }); app.post('/admin/person/delete', (req, res, next) => { try { if (req.body.key) { Person.destroy({ where: { key: req.body.key }, }).then(() => { res.redirect('/admin/people'); }); } } catch (err) { Rollbar.handleError(err); next(err); } }); app.get('/admin/people/export', (req, res, next) => { try { Person.findAll() .then(data => { res.json(data); }) .catch(err => { Rollbar.handleError(err); next(err); }); } catch (err) { Rollbar.handleError(err); next(err); } }); app.post('/admin/people/import', (req, res, next) => { try { if (req.body.importdata && req.body.importdata.length > 0) { const data = JSON.parse(req.body.importdata); Person.destroy({ where: {} }).then(() => { const promises = []; data.map(person => promises.push(Person.create(person))); sequelize.Promise.all(promises).then(() => { res.redirect(201, '/admin/people'); }); }).catch(err => { Rollbar.handleError(err); next(err); }); } else { res.redirect(400, '/admin/people'); } } catch (err) { Rollbar.handleError(err); next(err); } }); app.post('/admin/settings', (req, res, next) => { try { Settings.destroy({ where: {} }).then(() => { const settings = { slack: !!req.body.slack, email: !!req.body.email, }; Settings.create(settings).then(() => { res.redirect(201, '/admin'); }).catch(err => { Rollbar.handleError(err); next(err); }); }).catch(err => { Rollbar.handleError(err); next(err); }); } catch (err) { Rollbar.handleError(err); next(err); } }); // // Register server-side rendering middleware // ----------------------------------------------------------------------------- app.get('*', async (req, res, next) => { try { const css = new Set(); // Global (context) variables that can be easily accessed from any React component // https://facebook.github.io/react/docs/context.html const context = { // Enables critical path CSS rendering // https://github.com/kriasoft/isomorphic-style-loader insertCss: (...styles) => { // eslint-disable-next-line no-underscore-dangle styles.forEach(style => css.add(style._getCss())); }, }; const route = await UniversalRouter.resolve(routes, { path: req.path, query: req.query, agent: req.headers['user-agent'], }); if (route.redirect) { res.redirect(route.status || 302, route.redirect); return; } const data = { ...route }; data.children = ReactDOM.renderToString({route.component}); data.style = [...css].join(''); data.scripts = [ assets.vendor.js, assets.client.js, ]; if (assets[route.chunk]) { data.scripts.push(assets[route.chunk].js); } const html = ReactDOM.renderToStaticMarkup(); res.status(route.status || 200); res.send(`${html}`); } catch (err) { Rollbar.handleError(err); next(err); } }); // // Error handling // ----------------------------------------------------------------------------- app.use(Rollbar.errorHandler()); const pe = new PrettyError(); pe.skipNodeFiles(); pe.skipPackage('express'); app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars console.log(pe.render(err)); // eslint-disable-line no-console const html = ReactDOM.renderToStaticMarkup( {ReactDOM.renderToString()} , ); res.status(err.status || 500); res.send(`${html}`); }); // // Launch the server // ----------------------------------------------------------------------------- /* eslint-disable no-console */ models.sync().catch(err => console.error(err.stack)).then(() => { app.listen(port, () => { console.log(`The server is running at http://localhost:${port}/`); }); }); /* eslint-enable no-console */