Skip to content

Commit

Permalink
feat: conditional react routing (#3750)
Browse files Browse the repository at this point in the history
Allow backend app to serve both react and angularjs
  • Loading branch information
timotheeg authored Apr 21, 2022
1 parent 48e41fb commit 25b793b
Show file tree
Hide file tree
Showing 17 changed files with 391 additions and 64 deletions.
15 changes: 11 additions & 4 deletions Dockerfile.production
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,30 @@ WORKDIR /build
COPY package.json package-lock.json ./
COPY shared/package.json shared/package-lock.json ./shared/
COPY frontend/package.json frontend/package-lock.json ./frontend/
RUN npm ci && npm run postinstall

# Allow running of postinstall scripts
RUN npm config set unsafe-perm true
RUN npm ci

COPY . ./

ENV NODE_OPTIONS=--max-old-space-size=3072

RUN npm run build
RUN npm prune --production

# This stage builds the final container
FROM node:fermium-alpine3.13
LABEL maintainer=FormSG<formsg@data.gov.sg>
LABEL maintainer=FormSG<form@open.gov.sg>
WORKDIR /opt/formsg

# TODO: npm install ci --production instead of copying the entire node_modules from the build stage -_-

# Install build from backend-build
COPY --from=build /build/node_modules /opt/formsg/node_modules
COPY --from=build /build/shared/node_modules /opt/formsg/shared/node_modules
COPY --from=build /build/dist /opt/formsg/dist
COPY --from=build /build/package.json /opt/formsg/package.json
COPY --from=build /build/dist /opt/formsg/dist
RUN mv /opt/formsg/dist/backend/{src,shared} /opt/formsg/

# Install chromium from official docs
# https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#running-on-alpine
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
]
},
"scripts": {
"gen:theme-typings": "chakra-cli tokens src/theme/index.ts",
"gen:theme-typings": "chakra-cli tokens src/theme/index.ts || true",
"partytown": "partytown copylib public/~partytown",
"clean:emotion-types": "rimraf node_modules/@emotion/core/types",
"postinstall": "npm run gen:theme-typings && npm run clean:emotion-types && npm run partytown && npm --prefix ../shared install",
Expand Down
36 changes: 21 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@
"test:backend": "env-cmd -f tests/.test-env jest --coverage --maxWorkers=4",
"test:backend:watch": "env-cmd -f tests/.test-env jest --watch",
"test:frontend": "npm --prefix frontend test",
"build": "npm run clean && npm run build:backend && npm run build:frontend",
"build": "npm run clean && npm run build:backend && npm run build:frontend && npm run build:old-frontend",
"dev:angularjs": "webpack --config webpack.dev.js",
"build:old-frontend": "webpack --config webpack.prod.js",
"build:frontend": "npm run --prefix frontend build",
"build:backend": "tsc -p tsconfig.build.json && npm run copyfiles:backend",
"copyfiles:backend": "copyfiles -e src/public/**/*.html -u 1 src/**/*.html dist/backend/src",
"clean": "rimraf dist/",
"start": "node -r dotenv/config dist/backend/src/app/server.js",
"dev": "concurrently -k -p \"[{name}]\" -n \"api,client\" -c \"yellow.bold,green.bold\" \"docker-compose up --build\" \"npm run dev:frontend\"",
"start": "node -r dotenv/config src/app/server.js",
"dev": "concurrently -k -p \"[{name}]\" -n \"api,client\" -c \"yellow.bold,green.bold\" \"docker-compose up --build\" \"npm run dev:frontend\"",
"dev:backend": "npm run dev:angularjs & tsnd --poll --respawn --transpile-only --inspect=0.0.0.0 --exit-child -r dotenv/config -- src/app/server.ts",
"dev:frontend": "npm run --prefix frontend start",
"test": "npm run test:backend && npm run test:frontend",
Expand Down Expand Up @@ -263,4 +264,4 @@
"webpack-merge": "^4.1.3",
"worker-loader": "^2.0.0"
}
}
}
1 change: 1 addition & 0 deletions src/app/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ const config: Config = {
siteBannerContent: basicVars.banner.siteBannerContent,
adminBannerContent: basicVars.banner.adminBannerContent,
rateLimitConfig: basicVars.rateLimit,
reactMigration: basicVars.reactMigration,
configureAws,
secretEnv: basicVars.core.secretEnv,
}
Expand Down
34 changes: 30 additions & 4 deletions src/app/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,32 @@ export const optionalVarsSchema: Schema<IOptionalVarsSchema> = {
env: 'SEND_AUTH_OTP_RATE_LIMIT',
},
},
reactMigration: {
respondentRolloutNoAuth: {
doc: 'Percentage threshold to serve React for respondents for Phase 1 (forms WITHOUT Auth)',
format: 'int',
default: 0,
env: 'REACT_MIGRATION_RESP_ROLLOUT_NO_AUTH',
},
respondentRolloutAuth: {
doc: 'Percentage threshold to serve React for respondents for Phase 2 (forms WITH Auth)',
format: 'int',
default: 0,
env: 'REACT_MIGRATION_RESP_ROLLOUT_AUTH',
},
respondentCookieName: {
doc: "Name of the cookie that will store respondents' assigned environment.",
format: String,
default: 'v2-respondent-ui',
env: 'REACT_MIGRATION_RESP_COOKIE_NAME',
},
adminCookieName: {
doc: "Name of the cookie that will store admins' choice of environment.",
format: String,
default: 'v2-admin-ui',
env: 'REACT_MIGRATION_ADMIN_COOKIE_NAME',
},
},
}

export const prodOnlyVarsSchema: Schema<IProdOnlyVarsSchema> = {
Expand Down Expand Up @@ -330,10 +356,10 @@ export const prodOnlyVarsSchema: Schema<IProdOnlyVarsSchema> = {
database: 'formsg',
hosts: [ { host: 'database', port: 27017 } ]
}
e.g. https://form.gov.sg will be parsed into:
{
scheme: 'https',
hosts: [ { host: 'form.gov.sg' } ]
e.g. https://form.gov.sg will be parsed into:
{
scheme: 'https',
hosts: [ { host: 'form.gov.sg' } ]
}
*/
if (uriObject.scheme !== 'mongodb') {
Expand Down
27 changes: 6 additions & 21 deletions src/app/loaders/express/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Connection } from 'mongoose'
import path from 'path'
import url from 'url'

import { Environment } from '../../../types'
import config from '../../config/config'
import { AnalyticsRouter } from '../../modules/analytics/analytics.routes'
import { AuthRouter } from '../../modules/auth/auth.routes'
Expand All @@ -21,6 +20,7 @@ import { FrontendRouter } from '../../modules/frontend/frontend.routes'
import * as HomeController from '../../modules/home/home.controller'
import { MYINFO_ROUTER_PREFIX } from '../../modules/myinfo/myinfo.constants'
import { MyInfoRouter } from '../../modules/myinfo/myinfo.routes'
import { ReactMigrationRouter } from '../../modules/react-migration/react-migration.routes'
import { SgidRouter } from '../../modules/sgid/sgid.routes'
import {
CorppassLoginRouter,
Expand Down Expand Up @@ -149,35 +149,20 @@ const loadExpressApp = async (connection: Connection) => {
app.use('/sgid', SgidRouter)
// Use constant for registered routes with MyInfo servers
app.use(MYINFO_ROUTER_PREFIX, MyInfoRouter)

app.use(AdminFormsRouter)
app.use(PublicFormRouter)

// New routes in preparation for API refactor.
app.use('/api', ApiRouter)

// Serve static client files only in prod
// This block must be after all our routes, since the React application is
// served in a catchall route.
if (config.nodeEnv === Environment.Prod) {
const frontendPath = path.resolve('dist/frontend')
app.use(express.static(frontendPath))
app.use(express.static(path.resolve('dist/frontend'), { index: false }))
app.use('/public', express.static(path.resolve('dist/angularjs')))
app.get('/old/', HomeController.home)

app.get('*', (_req, res) => {
res.sendFile(path.join(frontendPath, 'index.html'))
})
}

if (config.nodeEnv === Environment.Dev) {
app.use(
'/public/fonts',
express.static(path.resolve('./dist/angularjs/fonts')),
)
app.use('/public', express.static(path.resolve('./dist/angularjs')))
app.get('/old/', HomeController.home)
}
app.use('/', ReactMigrationRouter)

app.use(sentryMiddlewares())

app.use(errorHandlerMiddlewares())

const server = http.createServer(app)
Expand Down
20 changes: 20 additions & 0 deletions src/app/loaders/express/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ type LogMeta = {
contentLength?: string
transactionId?: string
trace?: string
reactMigration?: {
respRolloutAuth: number
respRolloutNoAuth: number
adminCookie: string | undefined
respCookie: string | undefined
}
}

const loggingMiddleware = () => {
Expand Down Expand Up @@ -61,6 +67,20 @@ const loggingMiddleware = () => {
if (transactionId) {
meta.transactionId = transactionId
}

// Temporary: cookies are blacklisted, but we to track the state of the rollout for this particular request
if (
req.cookies?.[config.reactMigration.adminCookieName] ||
req.cookies?.[config.reactMigration.respondentCookieName]
) {
meta.reactMigration = {
respRolloutAuth: config.reactMigration.respondentRolloutAuth,
respRolloutNoAuth: config.reactMigration.respondentRolloutNoAuth,
adminCookie: req.cookies?.[config.reactMigration.adminCookieName],
respCookie: req.cookies?.[config.reactMigration.respondentCookieName],
}
}

return meta
},
headerBlacklist: ['cookie'],
Expand Down
6 changes: 3 additions & 3 deletions src/app/modules/form/public-form/public-form.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,9 @@ export const handleRedirect: ControllerHandler<
unknown,
Record<string, string>
> = async (req, res) => {
const { state, Id } = req.params
const { state, formId } = req.params

let redirectPath = state ? `${Id}/${state}` : Id
let redirectPath = state ? `${formId}/${state}` : formId
const queryString = querystring.stringify(req.query)
if (queryString.length > 0) {
redirectPath = redirectPath + '?' + encodeURIComponent(queryString)
Expand All @@ -183,7 +183,7 @@ export const handleRedirect: ControllerHandler<
const appUrl = baseUrl + req.originalUrl

const createMetatagsResult = await PublicFormService.createMetatags({
formId: Id,
formId,
appUrl,
imageBaseUrl: baseUrl,
})
Expand Down
8 changes: 4 additions & 4 deletions src/app/modules/form/public-form/public-form.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,22 @@ PublicFormRouter.get(
* through the main index, with the form ID specified as a hashbang path
*/
PublicFormRouter.get(
'/:Id([a-fA-F0-9]{24})/:state(preview|template|use-template)?',
'/:formId([a-fA-F0-9]{24})/:state(preview|template|use-template)',
PublicFormController.handleRedirect,
)

PublicFormRouter.get(
'/:Id([a-fA-F0-9]{24})/embed',
'/:formId([a-fA-F0-9]{24})/embed',
PublicFormController.handleRedirect,
)

PublicFormRouter.get(
'/forms/:agency/:Id([a-fA-F0-9]{24})/:state(preview|template|use-template)?',
'/forms/:agency/:formId([a-fA-F0-9]{24})/:state(preview|template|use-template)?',
PublicFormController.handleRedirect,
)

PublicFormRouter.get(
'/forms/:agency/:Id([a-fA-F0-9]{24})/embed',
'/forms/:agency/:formId([a-fA-F0-9]{24})/embed',
PublicFormController.handleRedirect,
)

Expand Down
2 changes: 1 addition & 1 deletion src/app/modules/form/public-form/public-form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export type Metatags = {
export type RedirectParams = {
state?: 'preview' | 'template' | 'use-template'
// TODO(#144): Rename Id to formId after all routes have been updated.
Id: string
formId: string
}
Loading

0 comments on commit 25b793b

Please sign in to comment.