/* eslint-disable no-underscore-dangle */ import path from 'path'; import express, { NextFunction, Request, Response } from 'express'; import cookieParser from 'cookie-parser'; import bodyParser from 'body-parser'; import expressJwt, { UnauthorizedError as Jwt401Error } from 'express-jwt'; import jwt from 'jsonwebtoken'; import React from 'react'; import ReactDOM from 'react-dom/server'; import PrettyError from 'pretty-error'; import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'; import { getDataFromTree } from 'react-apollo'; import createApolloClient from '@config@/createApolloClient.server'; import App from '@config@/App'; import Html from '@config@/Html'; import { ErrorPageWithoutStyle } from '@config@/error-page/ErrorPage'; import errorPageStyle from '@config@/error-page/ErrorPage.css'; import passport from '@config@/passport'; import config from '@config@/config'; import createCache from '@config@/createCache'; import router from './router'; // import models from './data/models'; import schema from './schema'; // import assets from './asset-manifest.json'; // eslint-disable-line import/no-unresolved // @ts-ignore import chunks from './chunk-manifest.json'; // eslint-disable-line import/no-unresolved import { AppContextTypes } from './AppContext'; import { clientDefaults, clientResolvers, clientTypeDefs, } from './clientSchema'; import serverRootValueDeps from '../../__generated__/serverRootValueDeps'; const serverRootValue = serverRootValueDeps[0] ? serverRootValueDeps[0][0].default : {}; process.on('unhandledRejection', (reason, p) => { console.error('Unhandled Rejection at:', p, 'reason:', reason); // send entire app down. Process manager will restart it process.exit(1); }); // // Tell any CSS tooling (such as Material UI) to use all vendor prefixes if the // user agent is not known. // ----------------------------------------------------------------------------- // @ts-ignore global.navigator = global.navigator || {}; // @ts-ignore global.navigator.userAgent = global.navigator.userAgent || 'all'; const app = express(); // // If you are using proxy from external machine, you can set TRUST_PROXY env // Default is to trust proxy headers only from loopback interface. // ----------------------------------------------------------------------------- app.set('trust proxy', config.trustProxy); // // Register Node.js middleware // ----------------------------------------------------------------------------- // @ts-ignore app.use(express.static(path.resolve(__userDir__, 'public'))); app.use(cookieParser()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); // // Authentication // ----------------------------------------------------------------------------- app.use( expressJwt({ secret: config.auth.jwt.secret, credentialsRequired: false, getToken: req => req.cookies.id_token, }), ); // Error handler for express-jwt app.use((err: any, req: Request, res: Response, next: NextFunction) => { // eslint-disable-line no-unused-vars if (err instanceof Jwt401Error) { console.error('[express-jwt-error]', req.cookies.id_token); // `clearCookie`, otherwise user can't use web-app until cookie expires res.clearCookie('id_token'); } next(err); }); app.use(passport.initialize()); 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, config.auth.jwt.secret, { expiresIn }); res.cookie('id_token', token, { maxAge: 1000 * expiresIn, httpOnly: true }); res.redirect('/'); }, ); // // Register API middleware // ----------------------------------------------------------------------------- // https://github.com/graphql/express-graphql#options const server = new ApolloServer({ schema: makeExecutableSchema(schema), uploads: false, introspection: __DEV__, playground: __DEV__, debug: __DEV__, context: ({ req }: { req: Request }) => ({ req }), rootValue: serverRootValue, }); server.applyMiddleware({ app }); // // Register server-side rendering middleware // ----------------------------------------------------------------------------- app.get('*', async (req, res, next) => { try { const css = new Set(); // Enables critical path CSS rendering // https://github.com/kriasoft/isomorphic-style-loader const insertCss = (...styles: any[]) => { styles.forEach(style => css.add(style._getCss())); }; const apolloClient = createApolloClient({ schemaArgs: { schema: makeExecutableSchema(schema), // This is a context consumed in GraphQL Resolvers context: { req }, }, partialCacheDefaults: { user: req.user || null, }, apolloCache: createCache(), clientDefaults, clientResolvers, clientTypeDefs, }); // Global (context) variables that can be easily accessed from any React component // https://facebook.github.io/react/docs/context.html const context: AppContextTypes = { // The twins below are wild, be careful! pathname: req.path, query: req.query, }; const route = await router.resolve(context); context.params = route.params; if (route.redirect) { res.redirect(route.status || 302, route.redirect); return; } const data = { ...route }; const rootComponent = ( {route.component} ); await getDataFromTree(rootComponent); data.children = await ReactDOM.renderToString(rootComponent); data.styles = [{ id: 'css', cssText: [...css].join('') }]; const scripts = new Set(); const addChunk = (chunk: string) => { if (chunks[chunk]) { chunks[chunk].forEach((asset: any) => scripts.add(asset)); } else if (__DEV__) { throw new Error(`Chunk with name '${chunk}' cannot be found`); } }; addChunk('client'); if (route.chunk) addChunk(route.chunk); if (route.chunks) route.chunks.forEach(addChunk); data.scripts = Array.from(scripts); data.app = { apiUrl: config.api.clientUrl, // To restore apollo cache in client.js cache: apolloClient.extract(), }; const html = ReactDOM.renderToStaticMarkup(); res.status(route.status || 200); res.send(`${html}`); } catch (err) { next(err); } }); // // Error handling // ----------------------------------------------------------------------------- const pe = new PrettyError(); pe.skipNodeFiles(); pe.skipPackage('express'); // eslint-disable-next-line no-unused-vars app.use((err: any, req: Request, res: Response, _next: NextFunction) => { console.error(pe.render(err)); const html = ReactDOM.renderToStaticMarkup( {ReactDOM.renderToString()} , ); res.status(err.status || 500); res.send(`${html}`); }); // // Launch the server // ----------------------------------------------------------------------------- // const promise = models.sync().catch((err: Error) => console.error(err.stack)); if (!module.hot) { // promise.then(() => { app.listen(config.port, () => { console.info(`The server is running at http://localhost:${config.port}/`); }); // }); } // // Hot Module Replacement // ----------------------------------------------------------------------------- if (module.hot) { module.hot.accept('./router'); } export const { hot } = module; export default app;