diff --git a/keyserver/src/responders/landing-handler.js b/keyserver/src/responders/landing-handler.js index e7af4e5df..4b5c212a8 100644 --- a/keyserver/src/responders/landing-handler.js +++ b/keyserver/src/responders/landing-handler.js @@ -1,262 +1,270 @@ // @flow import html from 'common-tags/lib/html/index.js'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import * as React from 'react'; // eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; import { promisify } from 'util'; import { type SIWEMessageType } from 'lib/types/siwe-types.js'; import { isValidPrimaryIdentityPublicKey, isValidSIWENonce, isValidSIWEMessageType, isValidSIWEIssuedAt, } from 'lib/utils/siwe-utils.js'; import { getMessageForException } from './utils.js'; import { type LandingSSRProps } from '../landing/landing-ssr.react.js'; import { waitForStream } from '../utils/json-stream.js'; import { getAndAssertLandingURLFacts } from '../utils/urls.js'; async function landingHandler(req: $Request, res: $Response) { try { await landingResponder(req, res); } catch (e) { console.warn(e); if (!res.headersSent) { res.status(500).send(getMessageForException(e)); } } } const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500&family=IBM+Plex+Sans:wght@400;500&display=swap'; const iaDuoFontsURL = 'fonts/duo.css'; const localFontsURL = 'fonts/local-fonts.css'; async function getDevFontURLs(): Promise<$ReadOnlyArray> { try { await access(localFontsURL); return [localFontsURL, iaDuoFontsURL]; } catch { return [googleFontsURL, iaDuoFontsURL]; } } type AssetInfo = { +jsURL: string, +fontURLs: $ReadOnlyArray, +cssInclude: string, }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontURLs = await getDevFontURLs(); assetInfo = ({ jsURL: 'http://localhost:8082/dev.build.js', fontURLs, cssInclude: '', }: AssetInfo); return assetInfo; } try { const manifestString = await readFile( '../landing/dist/manifest.json', 'utf8', ); const manifest = JSON.parse(manifestString); assetInfo = ({ jsURL: `compiled/${manifest['browser.js']}`, fontURLs: [googleFontsURL, iaDuoFontsURL], cssInclude: html` `, }: AssetInfo); return assetInfo; } catch (e) { console.warn(e); throw new Error( 'Could not load manifest.json for landing build. ' + 'Did you forget to run `yarn dev` in the landing folder?', ); } } type LandingApp = React.ComponentType; let webpackCompiledRootComponent: ?LandingApp = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe landing/dist doesn't always exist const webpackBuild = await import('landing/dist/landing.build.cjs'); webpackCompiledRootComponent = webpackBuild.landing.default; return webpackCompiledRootComponent; } catch (e) { console.warn(e); throw new Error( 'Could not load landing.build.cjs. ' + 'Did you forget to run `yarn dev` in the landing folder?', ); } } // eslint-disable-next-line react/no-deprecated const { renderToNodeStream } = ReactDOMServer; async function landingResponder(req: $Request, res: $Response) { const siweNonce = req.header('siwe-nonce'); if ( siweNonce !== null && siweNonce !== undefined && !isValidSIWENonce(siweNonce) ) { res.status(400).send({ message: 'Invalid nonce in siwe-nonce header.', }); return; } const siwePrimaryIdentityPublicKey = req.header( 'siwe-primary-identity-public-key', ); if ( siwePrimaryIdentityPublicKey !== null && siwePrimaryIdentityPublicKey !== undefined && !isValidPrimaryIdentityPublicKey(siwePrimaryIdentityPublicKey) ) { res.status(400).send({ message: 'Invalid primary identity public key in siwe-primary-identity-public-key header.', }); return; } const siweMessageTypeRawString = req.header('siwe-message-type'); if ( siweMessageTypeRawString !== null && siweMessageTypeRawString !== undefined && !isValidSIWEMessageType(siweMessageTypeRawString) ) { res.status(400).send({ message: 'Invalid siwe message type.', }); return; } const siweMessageType = ((siweMessageTypeRawString: any): SIWEMessageType); const siweMessageIssuedAt = req.header('siwe-message-issued-at'); if ( siweMessageIssuedAt !== null && siweMessageIssuedAt !== undefined && !isValidSIWEIssuedAt(siweMessageIssuedAt) ) { res.status(400).send({ message: 'Invalid siwe message issued at.', }); return; } const [{ jsURL, fontURLs, cssInclude }, LandingSSR] = await Promise.all([ getAssetInfo(), getWebpackCompiledRootComponentForSSR(), ]); const fontsInclude = fontURLs .map(url => ``) .join(''); const urlFacts = getAndAssertLandingURLFacts(); const { basePath } = urlFacts; // prettier-ignore res.write(html` Comm ${fontsInclude} ${cssInclude} +
`); // We remove trailing slash for `react-router` const routerBasename = basePath.replace(/\/$/, ''); const clientPath = routerBasename + req.url; const reactStream = renderToNodeStream( , ); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); const siweNonceString = siweNonce ? `"${siweNonce}"` : 'null'; const siwePrimaryIdentityPublicKeyString = siwePrimaryIdentityPublicKey ? `"${siwePrimaryIdentityPublicKey}"` : 'null'; const siweMessageTypeString = siweMessageType ? `"${siweMessageType}"` : 'null'; const siweMessageIssuedAtString = siweMessageIssuedAt ? `"${siweMessageIssuedAt}"` : 'null'; // prettier-ignore res.end(html`
`); } export default landingHandler; diff --git a/landing/script.js b/landing/script.js index 62004ffa9..57b50d000 100644 --- a/landing/script.js +++ b/landing/script.js @@ -1,17 +1,15 @@ // @flow -localStorage.clear(); - import 'isomorphic-fetch'; import invariant from 'invariant'; import React from 'react'; // eslint-disable-next-line import/extensions import { hydrateRoot } from 'react-dom/client'; import Root from './root.js'; const root = document.getElementById('react-root'); invariant(root, "cannot find id='react-root' element!"); hydrateRoot(root, );