diff --git a/.eslintignore b/.eslintignore index 90390fd79..2d04417b2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,16 +1,17 @@ lib/flow-typed lib/node_modules web/dist web/flow-typed web/node_modules -server/compiled +server/app_compiled +server/landing_compiled server/dist server/secrets server/facts server/fonts server/flow-typed server/node_modules server/src/lib server/src/web native/flow-typed native/node_modules diff --git a/.prettierignore b/.prettierignore index 4fe87f4b5..129f0b86d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,10 +1,11 @@ lib/flow-typed web/dist web/flow-typed -server/compiled +server/app_compiled +server/landing_compiled server/dist server/secrets server/facts server/fonts server/flow-typed native/flow-typed diff --git a/server/compiled b/server/app_compiled similarity index 100% rename from server/compiled rename to server/app_compiled diff --git a/server/landing_compiled b/server/landing_compiled new file mode 120000 index 000000000..7c811cd8a --- /dev/null +++ b/server/landing_compiled @@ -0,0 +1 @@ +../landing/dist/ \ No newline at end of file diff --git a/server/src/responders/website-responders.js b/server/src/responders/website-responders.js index 5397603bb..41f368bbf 100644 --- a/server/src/responders/website-responders.js +++ b/server/src/responders/website-responders.js @@ -1,331 +1,331 @@ // @flow import html from 'common-tags/lib/html'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import _keyBy from 'lodash/fp/keyBy'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import { Provider } from 'react-redux'; import { Route, StaticRouter } from 'react-router'; import { createStore, type Store } from 'redux'; import { promisify } from 'util'; import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer'; import { freshMessageStore } from 'lib/reducers/message-reducer'; import { mostRecentReadThread } from 'lib/selectors/thread-selectors'; import { mostRecentMessageTimestamp } from 'lib/shared/message-utils'; import { threadHasPermission } from 'lib/shared/thread-utils'; import { defaultCalendarFilters } from 'lib/types/filter-types'; import { defaultNumberPerThread } from 'lib/types/message-types'; import { defaultConnectionInfo } from 'lib/types/socket-types'; import { threadPermissions } from 'lib/types/thread-types'; import type { ServerVerificationResult } from 'lib/types/verify-types'; import { currentDateInTimeZone } from 'lib/utils/date-utils'; import { ServerError } from 'lib/utils/errors'; import { promiseAll } from 'lib/utils/promises'; import App from 'web/dist/app.build.cjs'; import { reducer } from 'web/redux/redux-setup'; import type { AppState, Action } from 'web/redux/redux-setup'; import getTitle from 'web/title/getTitle'; import { navInfoFromURL } from 'web/url-utils'; import { fetchEntryInfos } from '../fetchers/entry-fetchers'; import { fetchMessageInfos } from '../fetchers/message-fetchers'; import { fetchThreadInfos } from '../fetchers/thread-fetchers'; import { fetchCurrentUserInfo, fetchKnownUserInfos, } from '../fetchers/user-fetchers'; import { handleCodeVerificationRequest } from '../models/verification'; import { setNewSession } from '../session/cookies'; import { Viewer } from '../session/viewer'; import { streamJSON, waitForStream } from '../utils/json-stream'; import { getAppURLFacts } from '../utils/urls'; const { basePath, baseDomain } = getAppURLFacts(); const { renderToNodeStream } = ReactDOMServer; const baseURL = basePath.replace(/\/$/, ''); const baseHref = baseDomain + baseURL; const access = promisify(fs.access); const googleFontsURL = 'https://fonts.googleapis.com/css?family=Open+Sans:300,600%7CAnaheim'; const localFontsURL = 'fonts/local-fonts.css'; async function getFontsURL() { try { await access(localFontsURL); return localFontsURL; } catch { return googleFontsURL; } } type AssetInfo = {| jsURL: string, fontsURL: string, cssInclude: string |}; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'dev') { const fontsURL = await getFontsURL(); assetInfo = { jsURL: 'http://localhost:8080/dev.build.js', fontsURL, cssInclude: '', }; return assetInfo; } - // $FlowFixMe compiled/assets.json doesn't always exist - const { default: assets } = await import('../../compiled/assets'); + // $FlowFixMe app_compiled/assets.json doesn't always exist + const { default: assets } = await import('../../app_compiled/assets'); assetInfo = { jsURL: `compiled/${assets.browser.js}`, fontsURL: googleFontsURL, cssInclude: html` `, }; return assetInfo; } async function websiteResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { let initialNavInfo; try { initialNavInfo = navInfoFromURL(req.url, { now: currentDateInTimeZone(viewer.timeZone), }); } catch (e) { throw new ServerError(e.message); } const calendarQuery = { startDate: initialNavInfo.startDate, endDate: initialNavInfo.endDate, filters: defaultCalendarFilters, }; const threadSelectionCriteria = { joinedThreads: true }; const initialTime = Date.now(); const assetInfoPromise = getAssetInfo(); const threadInfoPromise = fetchThreadInfos(viewer); const messageInfoPromise = fetchMessageInfos( viewer, threadSelectionCriteria, defaultNumberPerThread, ); const entryInfoPromise = fetchEntryInfos(viewer, [calendarQuery]); const currentUserInfoPromise = fetchCurrentUserInfo(viewer); const serverVerificationResultPromise = handleVerificationRequest( viewer, initialNavInfo.verify, ); const userInfoPromise = fetchKnownUserInfos(viewer); const sessionIDPromise = (async () => { if (viewer.loggedIn) { await setNewSession(viewer, calendarQuery, initialTime); } return viewer.sessionID; })(); const threadStorePromise = (async () => { const { threadInfos } = await threadInfoPromise; return { threadInfos, inconsistencyReports: [] }; })(); const messageStorePromise = (async () => { const [ { threadInfos }, { rawMessageInfos, truncationStatuses }, ] = await Promise.all([threadInfoPromise, messageInfoPromise]); return freshMessageStore( rawMessageInfos, truncationStatuses, mostRecentMessageTimestamp(rawMessageInfos, initialTime), threadInfos, ); })(); const entryStorePromise = (async () => { const { rawEntryInfos } = await entryInfoPromise; return { entryInfos: _keyBy('id')(rawEntryInfos), daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos), lastUserInteractionCalendar: initialTime, inconsistencyReports: [], }; })(); const userStorePromise = (async () => { const userInfos = await userInfoPromise; return { userInfos, inconsistencyReports: [] }; })(); const navInfoPromise = (async () => { const [{ threadInfos }, messageStore] = await Promise.all([ threadInfoPromise, messageStorePromise, ]); const finalNavInfo = initialNavInfo; const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID; if ( requestedActiveChatThreadID && !threadHasPermission( threadInfos[requestedActiveChatThreadID], threadPermissions.VISIBLE, ) ) { finalNavInfo.activeChatThreadID = null; } if (!finalNavInfo.activeChatThreadID) { const mostRecentThread = mostRecentReadThread(messageStore, threadInfos); if (mostRecentThread) { finalNavInfo.activeChatThreadID = mostRecentThread; } } return finalNavInfo; })(); const { jsURL, fontsURL, cssInclude } = await assetInfoPromise; // prettier-ignore res.write(html` ${getTitle(0)} ${cssInclude}
`); const statePromises = { navInfo: navInfoPromise, currentUserInfo: currentUserInfoPromise, sessionID: sessionIDPromise, serverVerificationResult: serverVerificationResultPromise, entryStore: entryStorePromise, threadStore: threadStorePromise, userStore: userStorePromise, messageStore: messageStorePromise, updatesCurrentAsOf: initialTime, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, // We can use paths local to the on web urlPrefix: '', windowDimensions: { width: 0, height: 0 }, baseHref, connection: { ...defaultConnectionInfo('web', viewer.timeZone), actualizedCalendarQuery: calendarQuery, }, watchedThreadIDs: [], lifecycleState: 'active', nextLocalID: 0, queuedReports: [], timeZone: viewer.timeZone, userAgent: viewer.userAgent, cookie: undefined, deviceToken: undefined, dataLoaded: viewer.loggedIn, windowActive: true, }; const stateResult = await promiseAll(statePromises); const state: AppState = { ...stateResult }; const store: Store = createStore(reducer, state); const routerContext = {}; const reactStream = renderToNodeStream( , ); if (routerContext.url) { throw new ServerError('URL modified during server render!'); } reactStream.pipe(res, { end: false }); await waitForStream(reactStream); res.write(html`
`); } async function handleVerificationRequest( viewer: Viewer, code: ?string, ): Promise { if (!code) { return null; } try { return await handleCodeVerificationRequest(viewer, code); } catch (e) { if (e instanceof ServerError && e.message === 'invalid_code') { return { success: false }; } throw e; } } export { websiteResponder }; diff --git a/server/src/server.js b/server/src/server.js index 1a4afa414..a6a845327 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -1,95 +1,102 @@ // @flow import cluster from 'cluster'; import cookieParser from 'cookie-parser'; import express from 'express'; import expressWs from 'express-ws'; import os from 'os'; import './cron/cron'; import { jsonEndpoints } from './endpoints'; import { jsonHandler, downloadHandler, htmlHandler, uploadHandler, } from './responders/handlers'; import { errorReportDownloadResponder } from './responders/report-responders'; import { websiteResponder } from './responders/website-responders'; import { onConnection } from './socket/socket'; import { multerProcessor, multimediaUploadResponder, uploadDownloadResponder, } from './uploads/uploads'; import { getGlobalURLFacts } from './utils/urls'; const { baseRoutePath } = getGlobalURLFacts(); if (cluster.isMaster) { const cpuCount = os.cpus().length; for (let i = 0; i < cpuCount; i++) { cluster.fork(); } cluster.on('exit', () => cluster.fork()); } else { const server = express(); expressWs(server); server.use(express.json({ limit: '50mb' })); server.use(cookieParser()); const router = express.Router(); router.use('/images', express.static('images')); if (process.env.NODE_ENV === 'dev') { router.use('/fonts', express.static('fonts')); } router.use('/misc', express.static('misc')); router.use( '/.well-known', express.static( '.well-known', // Necessary for apple-app-site-association file { setHeaders: (res) => res.setHeader('Content-Type', 'application/json'), }, ), ); const compiledFolderOptions = process.env.NODE_ENV === 'dev' ? undefined : { maxAge: '1y', immutable: true }; - router.use('/compiled', express.static('compiled', compiledFolderOptions)); + router.use( + '/compiled', + express.static('app_compiled', compiledFolderOptions), + ); + router.use( + '/commlanding/compiled', + express.static('landing_compiled', compiledFolderOptions), + ); router.use('/', express.static('icons')); for (const endpoint in jsonEndpoints) { // $FlowFixMe Flow thinks endpoint is string const responder = jsonEndpoints[endpoint]; const expectCookieInvalidation = endpoint === 'log_out'; router.post( `/${endpoint}`, jsonHandler(responder, expectCookieInvalidation), ); } router.get( '/download_error_report/:reportID', downloadHandler(errorReportDownloadResponder), ); router.get( '/upload/:uploadID/:secret', downloadHandler(uploadDownloadResponder), ); // $FlowFixMe express-ws has side effects that can't be typed router.ws('/ws', onConnection); router.get('*', htmlHandler(websiteResponder)); router.post( '/upload_multimedia', multerProcessor, uploadHandler(multimediaUploadResponder), ); server.use(baseRoutePath, router); server.listen(parseInt(process.env.PORT, 10) || 3000, 'localhost'); }