diff --git a/Dockerfile.production b/Dockerfile.production index 62719d3e81..ebf44dfeb6 100644 --- a/Dockerfile.production +++ b/Dockerfile.production @@ -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 +LABEL maintainer=FormSG 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 diff --git a/frontend/package.json b/frontend/package.json index f857a44048..858d4e8fde 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/package-lock.json b/package-lock.json index 50d6af308a..c827ebde13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7779,17 +7779,6 @@ "schema-utils": "^2.6.5" }, "dependencies": { - "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, "find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -7801,6 +7790,23 @@ "pkg-dir": "^4.1.0" } }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, + "loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -25034,11 +25040,11 @@ } }, "axios": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", - "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", "requires": { - "follow-redirects": "^1.14.7" + "follow-redirects": "^1.14.8" } }, "debug": { diff --git a/package.json b/package.json index 57bf552553..3003a743dc 100644 --- a/package.json +++ b/package.json @@ -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", @@ -263,4 +264,4 @@ "webpack-merge": "^4.1.3", "worker-loader": "^2.0.0" } -} \ No newline at end of file +} diff --git a/src/app/config/config.ts b/src/app/config/config.ts index f5432b7b84..437f064447 100644 --- a/src/app/config/config.ts +++ b/src/app/config/config.ts @@ -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, } diff --git a/src/app/config/schema.ts b/src/app/config/schema.ts index 64fcf8e8d5..d74415c88f 100644 --- a/src/app/config/schema.ts +++ b/src/app/config/schema.ts @@ -290,6 +290,32 @@ export const optionalVarsSchema: Schema = { 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 = { @@ -330,10 +356,10 @@ export const prodOnlyVarsSchema: Schema = { 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') { diff --git a/src/app/loaders/express/index.ts b/src/app/loaders/express/index.ts index 6739d23bc1..4f3b963a16 100644 --- a/src/app/loaders/express/index.ts +++ b/src/app/loaders/express/index.ts @@ -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' @@ -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, @@ -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) diff --git a/src/app/loaders/express/logging.ts b/src/app/loaders/express/logging.ts index fcfaee6440..88f7adc46e 100644 --- a/src/app/loaders/express/logging.ts +++ b/src/app/loaders/express/logging.ts @@ -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 = () => { @@ -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'], diff --git a/src/app/modules/form/public-form/public-form.controller.ts b/src/app/modules/form/public-form/public-form.controller.ts index a331a24d04..d0d2b7982a 100644 --- a/src/app/modules/form/public-form/public-form.controller.ts +++ b/src/app/modules/form/public-form/public-form.controller.ts @@ -171,9 +171,9 @@ export const handleRedirect: ControllerHandler< unknown, Record > = 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) @@ -183,7 +183,7 @@ export const handleRedirect: ControllerHandler< const appUrl = baseUrl + req.originalUrl const createMetatagsResult = await PublicFormService.createMetatags({ - formId: Id, + formId, appUrl, imageBaseUrl: baseUrl, }) diff --git a/src/app/modules/form/public-form/public-form.routes.ts b/src/app/modules/form/public-form/public-form.routes.ts index bc3897d789..b7680b2f5d 100644 --- a/src/app/modules/form/public-form/public-form.routes.ts +++ b/src/app/modules/form/public-form/public-form.routes.ts @@ -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, ) diff --git a/src/app/modules/form/public-form/public-form.types.ts b/src/app/modules/form/public-form/public-form.types.ts index 14c2f66a93..d0cd7dc255 100644 --- a/src/app/modules/form/public-form/public-form.types.ts +++ b/src/app/modules/form/public-form/public-form.types.ts @@ -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 } diff --git a/src/app/modules/react-migration/react-migration.controller.ts b/src/app/modules/react-migration/react-migration.controller.ts new file mode 100644 index 0000000000..c9c8e248ec --- /dev/null +++ b/src/app/modules/react-migration/react-migration.controller.ts @@ -0,0 +1,156 @@ +import path from 'path' + +import { FormAuthType } from '../../../../shared/types' +import config from '../../config/config' +import { createLoggerWithLabel } from '../../config/logger' +import { ControllerHandler } from '../core/core.types' +import * as FormService from '../form/form.service' +import * as PublicFormController from '../form/public-form/public-form.controller' +import { RedirectParams } from '../form/public-form/public-form.types' +import * as HomeController from '../home/home.controller' + +export type SetEnvironmentParams = { + ui: 'react' | 'angular' +} + +export const RESPONDENT_COOKIE_OPTIONS = { + httpOnly: true, + sameSite: 'strict' as const, + secure: !config.isDev, +} + +export const ADMIN_COOKIE_OPTIONS = { + httpOnly: true, + maxAge: 31 * 2 * 24 * 60 * 60, // 2 months + sameSite: 'strict' as const, + secure: !config.isDev, +} + +const logger = createLoggerWithLabel(module) + +const serveFormReact: ControllerHandler = (_req, res) => { + const reactFrontendPath = path.resolve('dist/frontend') + logger.info({ + message: 'serveFormReact', + meta: { + action: 'routeReact.serveFormReact', + __dirname, + cwd: process.cwd(), + reactFrontendPath, + }, + }) + return res.sendFile(path.join(reactFrontendPath, 'index.html')) +} + +const serveFormAngular: ControllerHandler< + RedirectParams, + unknown, + unknown, + Record +> = (req, res, next) => { + return PublicFormController.handleRedirect(req, res, next) +} + +export const serveForm: ControllerHandler< + RedirectParams, + unknown, + unknown, + Record +> = async (req, res, next) => { + const formResult = await FormService.retrieveFormKeysById(req.params.formId, [ + 'authType', + ]) + let showReact: boolean | undefined = undefined + let hasAuth = true + + if (!formResult.isErr()) { + // This conditional router is not the one to do error handling + // If there's any error, hasAuth will retain its value of true, and + // the handling route will handle the error later in the usual fashion + hasAuth = formResult.value.authType !== FormAuthType.NIL + } + + const threshold = hasAuth + ? config.reactMigration.respondentRolloutAuth + : config.reactMigration.respondentRolloutNoAuth + + if (threshold <= 0) { + // Check the rollout value first, if it's 0, react is DISABLED + // And we ignore cookies entirely! + showReact = false + } else if (req.cookies) { + if (config.reactMigration.adminCookieName in req.cookies) { + // Admins are dogfooders, the choice they made for the admin environment + // also applies to the forms they need to fill themselves + showReact = req.cookies[config.reactMigration.adminCookieName] === 'react' + } else if (config.reactMigration.respondentCookieName in req.cookies) { + // Note: the respondent cookie is for the whole session, not for a specific form. + // That means that within a session, a respondent will see the same environment + // for all the forms he/she fills. + showReact = + req.cookies[config.reactMigration.respondentCookieName] === 'react' + } + } + + if (showReact === undefined) { + const rand = Math.random() * 100 + showReact = rand <= threshold + + logger.info({ + message: 'Randomly assigned UI environment', + meta: { + action: 'routeReact.random', + hasAuth, + rand, + threshold, + showReact, + }, + }) + + res.cookie( + config.reactMigration.respondentCookieName, + showReact ? 'react' : 'angular', + RESPONDENT_COOKIE_OPTIONS, + ) + } + + logger.info({ + message: 'Routing evaluation done', + meta: { + action: 'routeReact', + hasAuth, + threshold, + showReact, + cwd: process.cwd(), + }, + }) + + if (showReact) { + return serveFormReact(req, res, next) + } else { + return serveFormAngular(req, res, next) + } +} + +export const serveDefault: ControllerHandler = (req, res, next) => { + // only admin who chose react should see react, everybody else is plain angular + if (req.cookies?.[config.reactMigration.adminCookieName] === 'react') { + // react + return serveFormReact(req, res, next) + } else { + // angular + return HomeController.home(req, res, next) + } +} + +// Note: frontend is expected to refresh after executing this +export const adminChooseEnvironment: ControllerHandler< + SetEnvironmentParams, + unknown, + unknown, + Record +> = (req, res) => { + const ui = req.params.ui === 'react' ? 'react' : 'angular' + res.cookie(config.reactMigration.adminCookieName, ui, ADMIN_COOKIE_OPTIONS) + return res.json({ ui }) +} diff --git a/src/app/modules/react-migration/react-migration.routes.ts b/src/app/modules/react-migration/react-migration.routes.ts new file mode 100644 index 0000000000..2186b22e00 --- /dev/null +++ b/src/app/modules/react-migration/react-migration.routes.ts @@ -0,0 +1,16 @@ +import { Router } from 'express' + +import * as ReactMigrationController from './react-migration.controller' + +export const ReactMigrationRouter = Router() + +ReactMigrationRouter.get( + '/:formId([a-fA-F0-9]{24})', + ReactMigrationController.serveForm, +) + +ReactMigrationRouter.get('/#!/:formId([a-fA-F0-9]{24})', (req, res) => { + res.redirect(`/${req.params.formId}`) +}) + +ReactMigrationRouter.get('*', ReactMigrationController.serveDefault) diff --git a/src/app/routes/api/v3/admin/admin.routes.ts b/src/app/routes/api/v3/admin/admin.routes.ts index 352e965683..1b52d60f14 100644 --- a/src/app/routes/api/v3/admin/admin.routes.ts +++ b/src/app/routes/api/v3/admin/admin.routes.ts @@ -1,7 +1,15 @@ import { Router } from 'express' +import * as ReactMigrationController from '../../../../modules/react-migration/react-migration.controller' + import { AdminFormsRouter } from './forms' export const AdminRouter = Router() AdminRouter.use('/forms', AdminFormsRouter) + +// This endpoint doesn't reaaaallly need to be a verified admin to be used +AdminRouter.get( + '/environment/:env(react|angular)', + ReactMigrationController.adminChooseEnvironment, +) diff --git a/src/app/services/mail/mail.utils.ts b/src/app/services/mail/mail.utils.ts index de94578e36..9b584aae21 100644 --- a/src/app/services/mail/mail.utils.ts +++ b/src/app/services/mail/mail.utils.ts @@ -76,6 +76,17 @@ export const generateLoginOtpHtml = (htmlData: { ipAddress: string }): ResultAsync => { const pathToTemplate = `${__dirname}/../../views/templates/otp-email.server.view.html` + + logger.info({ + message: 'generateLoginOtpHtml', + meta: { + action: 'generateLoginOtpHtml', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) + return safeRenderFile(pathToTemplate, htmlData) } @@ -91,7 +102,7 @@ export const generateVerificationOtpHtml = ({ return dedent`

You are currently submitting a form on ${appName}.

- Your OTP is ${otp}. It will expire in ${minutesToExpiry} minutes. + Your OTP is ${otp}. It will expire in ${minutesToExpiry} minutes. Please use this to verify your submission.

If your OTP does not work, please request for a new OTP.

@@ -106,6 +117,17 @@ export const generateSubmissionToAdminHtml = ( htmlData: SubmissionToAdminHtmlData, ): ResultAsync => { const pathToTemplate = `${__dirname}/../../views/templates/submit-form-email.server.view.html` + + logger.info({ + message: 'generateSubmissionToAdminHtml', + meta: { + action: 'generateSubmissionToAdminHtml', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) + return safeRenderFile(pathToTemplate, htmlData) } @@ -120,6 +142,16 @@ export const generateBounceNotificationHtml = ( pathToTemplate = `${__dirname}/../../views/templates/bounce-notification-transient.server.view.html` } + logger.info({ + message: 'generateBounceNotificationHtml', + meta: { + action: 'generateBounceNotificationHtml', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) + return safeRenderFile(pathToTemplate, htmlData) } @@ -128,6 +160,16 @@ export const generateAutoreplyPdf = ( ): ResultAsync => { const pathToTemplate = `${__dirname}/../../views/templates/submit-form-summary-pdf.server.view.html` + logger.info({ + message: 'generateAutoreplyPdf', + meta: { + action: 'generateAutoreplyPdf', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) + return safeRenderFile(pathToTemplate, renderData).andThen((summaryHtml) => { return ResultAsync.fromPromise( generateAutoreplyPdfPromise(summaryHtml), @@ -152,6 +194,15 @@ export const generateAutoreplyHtml = ( htmlData: AutoreplyHtmlData, ): ResultAsync => { const pathToTemplate = `${__dirname}/../../views/templates/submit-form-autoreply.server.view.html` + logger.info({ + message: 'generateAutoreplyHtml', + meta: { + action: 'generateAutoreplyHtml', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) return safeRenderFile(pathToTemplate, htmlData) } @@ -178,6 +229,15 @@ export const generateSmsVerificationDisabledHtmlForAdmin = ( htmlData: AdminSmsDisabledData, ): ResultAsync => { const pathToTemplate = `${process.cwd()}/src/app/views/templates/sms-verification-disabled-admin.server.view.html` + logger.info({ + message: 'generateSmsVerificationDisabledHtmlForAdmin', + meta: { + action: 'generateSmsVerificationDisabledHtmlForAdmin', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) return safeRenderFile(pathToTemplate, htmlData) } @@ -185,6 +245,15 @@ export const generateSmsVerificationDisabledHtmlForCollab = ( htmlData: CollabSmsDisabledData, ): ResultAsync => { const pathToTemplate = `${process.cwd()}/src/app/views/templates/sms-verification-disabled-collab.server.view.html` + logger.info({ + message: 'generateSmsVerificationDisabledHtmlForCollab', + meta: { + action: 'generateSmsVerificationDisabledHtmlForCollab', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) return safeRenderFile(pathToTemplate, htmlData) } @@ -192,6 +261,15 @@ export const generateSmsVerificationWarningHtmlForAdmin = ( htmlData: AdminSmsWarningData, ): ResultAsync => { const pathToTemplate = `${process.cwd()}/src/app/views/templates/sms-verification-warning-admin.view.html` + logger.info({ + message: 'generateSmsVerificationWarningHtmlForAdmin', + meta: { + action: 'generateSmsVerificationWarningHtmlForAdmin', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) return safeRenderFile(pathToTemplate, htmlData) } @@ -199,5 +277,14 @@ export const generateSmsVerificationWarningHtmlForCollab = ( htmlData: CollabSmsWarningData, ): ResultAsync => { const pathToTemplate = `${process.cwd()}/src/app/views/templates/sms-verification-warning-collab.view.html` + logger.info({ + message: 'generateSmsVerificationWarningHtmlForCollab', + meta: { + action: 'generateSmsVerificationWarningHtmlForCollab', + pathToTemplate, + __dirname, + cwd: process.cwd(), + }, + }) return safeRenderFile(pathToTemplate, htmlData) } diff --git a/src/public/modules/core/resources/landing-examples.js b/src/public/modules/core/resources/landing-examples.js index 931df4e758..fa0b1b3897 100644 --- a/src/public/modules/core/resources/landing-examples.js +++ b/src/public/modules/core/resources/landing-examples.js @@ -3,37 +3,37 @@ const examples = [ img: '/public/modules/core/img/landing/restricted__1-MOM.png', agency: 'MOM', title: 'Report an Employment Act violation', - formLink: 'https://form.gov.sg/#!/5a0ae8bb66545d85005daf6f', + formLink: 'https://form.gov.sg/5a0ae8bb66545d85005daf6f', }, { img: '/public/modules/core/img/landing/restricted__3-NEA.png', agency: 'NEA', title: 'Update particulars of deceased next-of-kin', - formLink: 'https://form.gov.sg/#!/5a9e53d0b3a3b6006e6e0b74', + formLink: 'https://form.gov.sg/5a9e53d0b3a3b6006e6e0b74', }, { img: '/public/modules/core/img/landing/restricted__7-MFA.png', agency: 'MFA', title: 'Scholarship Mailing List', - formLink: 'https://form.gov.sg/#!/5a9f3906b3a3b6006e6ee595', + formLink: 'https://form.gov.sg/5a9f3906b3a3b6006e6ee595', }, { img: '/public/modules/core/img/landing/restricted__8-IPOS.png', agency: 'IPOS', title: 'FinTech Fast Track Initiative Registration', - formLink: 'https://form.gov.sg/#!/5ac6cced5eaf2b0030597aaa', + formLink: 'https://form.gov.sg/5ac6cced5eaf2b0030597aaa', }, { img: '/public/modules/core/img/landing/restricted__9-SCB.png', agency: 'SCB', title: 'National Science Challenge Registration', - formLink: 'https://form.gov.sg/#!/5a813986c860b96e00a3026d', + formLink: 'https://form.gov.sg/5a813986c860b96e00a3026d', }, { img: '/public/modules/core/img/landing/restricted__2-MOE.png', agency: 'MOE', title: 'School placement for returning Singaporeans', - formLink: 'https://form.gov.sg/#!/5aa5e5b1dcff52006dfd5f86', + formLink: 'https://form.gov.sg/5aa5e5b1dcff52006dfd5f86', }, ] diff --git a/src/types/config.ts b/src/types/config.ts index 8128662092..f778cfb6c8 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -52,6 +52,13 @@ export type RateLimitConfig = { sendAuthOtp: number } +export type ReactMigrationConfig = { + respondentRolloutNoAuth: number + respondentRolloutAuth: number + respondentCookieName: string + adminCookieName: string +} + export type Config = { app: AppConfig db: DbConfig @@ -75,6 +82,7 @@ export type Config = { siteBannerContent: string adminBannerContent: string rateLimitConfig: RateLimitConfig + reactMigration: ReactMigrationConfig secretEnv: string // Functions @@ -148,6 +156,12 @@ export interface IOptionalVarsSchema { submissions: number sendAuthOtp: number } + reactMigration: { + respondentRolloutNoAuth: number + respondentRolloutAuth: number + respondentCookieName: string + adminCookieName: string + } } export interface IBucketUrlSchema {