diff --git a/.ebextensions/env-file-creation.config b/.ebextensions/env-file-creation.config
index bf59ee6d6b..2d6b460e14 100644
--- a/.ebextensions/env-file-creation.config
+++ b/.ebextensions/env-file-creation.config
@@ -44,9 +44,9 @@ files:
aws ssm get-parameter --name "${ENV_TYPE}-sentry" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
aws ssm get-parameter --name "${ENV_TYPE}-sms" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
aws ssm get-parameter --name "${ENV_TYPE}-ndi" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
- aws ssm get-parameter --name "${ENV_TYPE}-sgid" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
aws ssm get-parameter --name "${ENV_TYPE}-verified-fields" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
aws ssm get-parameter --name "${ENV_TYPE}-webhook-verified-content" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
+ aws ssm get-parameter --name "${ENV_SITE_NAME}-sgid" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
aws ssm get-parameter --name "${ENV_SITE_NAME}-payment" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
aws ssm get-parameter --name "${ENV_SITE_NAME}-cron-payment" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env
diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml
index de932b94e7..6bdba671ec 100644
--- a/.github/workflows/chromatic.yml
+++ b/.github/workflows/chromatic.yml
@@ -44,8 +44,9 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
- node-version: 14
+ node-version: 18
cache: 'npm'
+ cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm i
# 👇 Adds Chromatic as a step in the workflow
diff --git a/.github/workflows/deploy-virus-scanner-production.yml b/.github/workflows/deploy-virus-scanner-production.yml
index ff79f94157..12b008938f 100644
--- a/.github/workflows/deploy-virus-scanner-production.yml
+++ b/.github/workflows/deploy-virus-scanner-production.yml
@@ -18,6 +18,6 @@ jobs:
uses: ./.github/workflows/aws-deploy-scanner.yml
with:
environment: 'production'
- provisionedConcurrency: 5
+ provisionedConcurrency: 10
checkoutBranch: 'release-al2'
secrets: inherit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83fb56625f..5e3749f87c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,26 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
-#### [v6.82.0](https://github.com/opengovsg/FormSG/compare/v6.82.0...v6.82.0)
+#### [v6.83.0](https://github.com/opengovsg/FormSG/compare/v6.83.0...v6.83.0)
+
+- fix(sgid-login): add graceful handling of errors when work email is invalid [`#6819`](https://github.com/opengovsg/FormSG/pull/6819)
+- test: fix error message for InvalidFileKeyError [`#6821`](https://github.com/opengovsg/FormSG/pull/6821)
+- chore: update serverless package.json [`#6820`](https://github.com/opengovsg/FormSG/pull/6820)
+- fix(storybook): build crash, and missing NumberField customVal [`#6808`](https://github.com/opengovsg/FormSG/pull/6808)
+
+#### [v6.83.0](https://github.com/opengovsg/FormSG/compare/v6.82.0...v6.83.0)
+
+> 19 October 2023
+
+- chore: increase hot lambda to 10 [`#6815`](https://github.com/opengovsg/FormSG/pull/6815)
+- feat(sgid-myinfo): add even more sgid myinfo fields [`#6807`](https://github.com/opengovsg/FormSG/pull/6807)
+- chore(deps-dev): bump @babel/traverse from 7.22.11 to 7.23.2 in /serverless/virus-scanner [`#6809`](https://github.com/opengovsg/FormSG/pull/6809)
+- chore(deps-dev): bump @babel/traverse from 7.20.1 to 7.23.2 in /frontend [`#6810`](https://github.com/opengovsg/FormSG/pull/6810)
+- build: merge release 6.82.0 into develop [`#6801`](https://github.com/opengovsg/FormSG/pull/6801)
+- feat(sgid-login): general availability and multi-employment [`#6763`](https://github.com/opengovsg/FormSG/pull/6763)
+- feat: add more myinfo sgid fields [`#6766`](https://github.com/opengovsg/FormSG/pull/6766)
+- build: release v6.82.0 [`#6800`](https://github.com/opengovsg/FormSG/pull/6800)
+- chore: bump version to v6.83.0 [`0e9c4bc`](https://github.com/opengovsg/FormSG/commit/0e9c4bc2b154d9d8e77e9e36a2105ef7d85d209b)
#### [v6.82.0](https://github.com/opengovsg/FormSG/compare/v6.81.0...v6.82.0)
@@ -19,7 +38,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- fix(deps): bump type-fest from 4.3.1 to 4.4.0 in /shared [`#6782`](https://github.com/opengovsg/FormSG/pull/6782)
- chore(ci): backend jest config [`#6772`](https://github.com/opengovsg/FormSG/pull/6772)
- build: release v6.81.0 [`#6777`](https://github.com/opengovsg/FormSG/pull/6777)
-- chore: bump version to v6.82.0 [`5dab965`](https://github.com/opengovsg/FormSG/commit/5dab965019d626ccd3ff6ef20f4d9f6c386db8c8)
+- chore: bump version to v6.82.0 [`9b23483`](https://github.com/opengovsg/FormSG/commit/9b234831c1eb84b6ffca9b13d10f54a029b43202)
#### [v6.81.0](https://github.com/opengovsg/FormSG/compare/v6.80.0...v6.81.0)
diff --git a/__tests__/e2e/fixtures/auth.ts b/__tests__/e2e/fixtures/auth.ts
index 6d9f51921b..61f5c4b6f5 100644
--- a/__tests__/e2e/fixtures/auth.ts
+++ b/__tests__/e2e/fixtures/auth.ts
@@ -25,7 +25,10 @@ export const test = baseTest.extend({
await page.goto(LOGIN_PAGE)
await page.getByRole('textbox', { name: /log in/i }).fill(ADMIN_EMAIL)
- await page.getByRole('button', { name: /log in/i }).click()
+ await page
+ .getByRole('button', { name: /log in/i })
+ .first()
+ .click()
// Ensure OTP success message is seen
await expect(
diff --git a/__tests__/e2e/login.spec.ts b/__tests__/e2e/login.spec.ts
index cd2192320b..a70715cd4a 100644
--- a/__tests__/e2e/login.spec.ts
+++ b/__tests__/e2e/login.spec.ts
@@ -20,7 +20,10 @@ test.describe('login', () => {
.getByRole('textbox', { name: /log in/i })
.fill('user@non-white-listed-agency.com')
- await page.getByRole('button', { name: /log in/i }).click()
+ await page
+ .getByRole('button', { name: /log in/i })
+ .first()
+ .click()
// Ensure error message is seen
await expect(
@@ -36,7 +39,10 @@ test.describe('login', () => {
await expect(page).toHaveURL(LOGIN_PAGE)
await page.getByRole('textbox', { name: /log in/i }).fill(legitUserEmail)
- await page.getByRole('button', { name: /log in/i }).click()
+ await page
+ .getByRole('button', { name: /log in/i })
+ .first()
+ .click()
// Ensure OTP success message is seen
await expect(
@@ -61,7 +67,10 @@ test.describe('login', () => {
await expect(page).toHaveURL(LOGIN_PAGE)
await page.getByRole('textbox', { name: /log in/i }).fill(legitUserEmail)
- await page.getByRole('button', { name: /log in/i }).click()
+ await page
+ .getByRole('button', { name: /log in/i })
+ .first()
+ .click()
// Ensure OTP success message is seen
await expect(
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 85bb1a0068..9745ac9ad0 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "form-frontend",
- "version": "6.82.0",
+ "version": "6.83.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "form-frontend",
- "version": "6.82.0",
+ "version": "6.83.0",
"hasInstallScript": true,
"dependencies": {
"@chakra-ui/react": "^1.8.6",
@@ -145,6 +145,7 @@
"patch-package": "^6.4.7",
"prettier": "^2.5.1",
"puppeteer": "^13.0.0",
+ "react-refresh": "^0.14.0",
"react-scripts": "^4.0.3",
"resize-observer-polyfill": "^1.5.1",
"rimraf": "^3.0.2",
@@ -184,11 +185,12 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
- "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dependencies": {
- "@babel/highlight": "^7.18.6"
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
@@ -243,13 +245,14 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.20.1",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz",
- "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.20.0",
+ "@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
@@ -386,9 +389,9 @@
}
},
"node_modules/@babel/helper-environment-visitor": {
- "version": "7.18.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
- "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -407,25 +410,25 @@
}
},
"node_modules/@babel/helper-function-name": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
- "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"dependencies": {
- "@babel/template": "^7.18.10",
- "@babel/types": "^7.19.0"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-hoist-variables": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
- "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.18.6"
+ "@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
@@ -552,29 +555,29 @@
}
},
"node_modules/@babel/helper-split-export-declaration": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
- "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.18.6"
+ "@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"engines": {
"node": ">=6.9.0"
}
@@ -618,12 +621,12 @@
}
},
"node_modules/@babel/highlight": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
- "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.18.6",
- "chalk": "^2.0.0",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
@@ -631,9 +634,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.20.1",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz",
- "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -2108,33 +2111,33 @@
}
},
"node_modules/@babel/template": {
- "version": "7.18.10",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
- "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.18.6",
- "@babel/parser": "^7.18.10",
- "@babel/types": "^7.18.10"
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.20.1",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
- "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
- "dev": true,
- "dependencies": {
- "@babel/code-frame": "^7.18.6",
- "@babel/generator": "^7.20.1",
- "@babel/helper-environment-visitor": "^7.18.9",
- "@babel/helper-function-name": "^7.19.0",
- "@babel/helper-hoist-variables": "^7.18.6",
- "@babel/helper-split-export-declaration": "^7.18.6",
- "@babel/parser": "^7.20.1",
- "@babel/types": "^7.20.0",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -2143,12 +2146,12 @@
}
},
"node_modules/@babel/types": {
- "version": "7.20.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz",
- "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -39212,6 +39215,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "node_modules/react-refresh": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
+ "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react-remove-scroll": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.1.tgz",
@@ -48459,11 +48471,12 @@
}
},
"@babel/code-frame": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
- "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"requires": {
- "@babel/highlight": "^7.18.6"
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
}
},
"@babel/compat-data": {
@@ -48504,13 +48517,14 @@
}
},
"@babel/generator": {
- "version": "7.20.1",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz",
- "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"requires": {
- "@babel/types": "^7.20.0",
+ "@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"dependencies": {
@@ -48614,9 +48628,9 @@
}
},
"@babel/helper-environment-visitor": {
- "version": "7.18.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
- "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true
},
"@babel/helper-explode-assignable-expression": {
@@ -48629,22 +48643,22 @@
}
},
"@babel/helper-function-name": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
- "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"requires": {
- "@babel/template": "^7.18.10",
- "@babel/types": "^7.19.0"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
}
},
"@babel/helper-hoist-variables": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
- "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"requires": {
- "@babel/types": "^7.18.6"
+ "@babel/types": "^7.22.5"
}
},
"@babel/helper-member-expression-to-functions": {
@@ -48738,23 +48752,23 @@
}
},
"@babel/helper-split-export-declaration": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
- "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"requires": {
- "@babel/types": "^7.18.6"
+ "@babel/types": "^7.22.5"
}
},
"@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw=="
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw=="
},
"@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="
},
"@babel/helper-validator-option": {
"version": "7.18.6",
@@ -48786,19 +48800,19 @@
}
},
"@babel/highlight": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
- "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"requires": {
- "@babel/helper-validator-identifier": "^7.18.6",
- "chalk": "^2.0.0",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
- "version": "7.20.1",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz",
- "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
@@ -49779,41 +49793,41 @@
}
},
"@babel/template": {
- "version": "7.18.10",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
- "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.18.6",
- "@babel/parser": "^7.18.10",
- "@babel/types": "^7.18.10"
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
}
},
"@babel/traverse": {
- "version": "7.20.1",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
- "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.18.6",
- "@babel/generator": "^7.20.1",
- "@babel/helper-environment-visitor": "^7.18.9",
- "@babel/helper-function-name": "^7.19.0",
- "@babel/helper-hoist-variables": "^7.18.6",
- "@babel/helper-split-export-declaration": "^7.18.6",
- "@babel/parser": "^7.20.1",
- "@babel/types": "^7.20.0",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
- "version": "7.20.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz",
- "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},
@@ -77924,6 +77938,12 @@
}
}
},
+ "react-refresh": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
+ "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
+ "dev": true
+ },
"react-remove-scroll": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 20b3e3e771..57012c320b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "form-frontend",
- "version": "6.82.0",
+ "version": "6.83.0",
"homepage": ".",
"private": true,
"dependencies": {
@@ -104,8 +104,8 @@
"build:dd-chunk": "webpack --config webpack.dd.config.js",
"test": "cross-env SKIP_PREFLIGHT_CHECK=true craco test --silent",
"eject": "react-scripts eject",
- "storybook": "start-storybook -p 6006",
- "build-storybook": "build-storybook",
+ "storybook": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006",
+ "build-storybook": "NODE_OPTIONS=--openssl-legacy-provider build-storybook",
"lint": "eslint ./ --ignore-path .gitignore && prettier . -c",
"lint:fix": "eslint ./ --ignore-path .gitignore --fix && prettier . -c --write",
"test:a11y": "npm run build-storybook && jest --config .storybook/storyshots/jest.config.js",
@@ -185,6 +185,7 @@
"patch-package": "^6.4.7",
"prettier": "^2.5.1",
"puppeteer": "^13.0.0",
+ "react-refresh": "^0.14.0",
"react-scripts": "^4.0.3",
"resize-observer-polyfill": "^1.5.1",
"rimraf": "^3.0.2",
diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx
index 577636c813..256f665304 100644
--- a/frontend/src/app/AppRouter.tsx
+++ b/frontend/src/app/AppRouter.tsx
@@ -13,8 +13,8 @@ import {
DASHBOARD_ROUTE,
LANDING_PAYMENTS_ROUTE,
LANDING_ROUTE,
+ LOGIN_CALLBACK_ROUTE,
LOGIN_ROUTE,
- OGP_LOGIN_ROUTE,
PAYMENT_PAGE_SUBROUTE,
PRIVACY_POLICY_ROUTE,
PUBLICFORM_ROUTE,
@@ -36,7 +36,7 @@ import {
ResponsesPage,
} from '~features/admin-form/responses'
import { SettingsPage } from '~features/admin-form/settings/SettingsPage'
-import { SgidLoginPage } from '~features/login'
+import { SelectProfilePage } from '~features/login'
import { FormPaymentPage } from '~features/public-form/components/FormPaymentPage/FormPaymentPage'
import { BillingPage } from '~features/user/billing'
@@ -101,8 +101,8 @@ export const AppRouter = (): JSX.Element => {
element={} />}
/>
} />}
+ path={LOGIN_CALLBACK_ROUTE}
+ element={} />}
/>
>((props, ref) => (
+
+ )),
+)
+
+export const SingpassFullLogoSvgr = chakra(MemoSingpassFullLogoSvgr)
diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts
index 01eee2bf69..cb1a9a0e9f 100644
--- a/frontend/src/constants/routes.ts
+++ b/frontend/src/constants/routes.ts
@@ -2,7 +2,7 @@ export const LANDING_ROUTE = '/'
export const LANDING_PAYMENTS_ROUTE = '/payments'
export const DASHBOARD_ROUTE = '/dashboard'
export const LOGIN_ROUTE = '/login'
-export const OGP_LOGIN_ROUTE = '/ogp-login'
+export const LOGIN_CALLBACK_ROUTE = '/login/select-profile'
export const TOU_ROUTE = '/terms'
export const PRIVACY_POLICY_ROUTE = '/privacy'
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx
index abc879eab1..60d36bde4c 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/MyInfoPanel.tsx
@@ -1,8 +1,10 @@
-import { useCallback, useMemo } from 'react'
+import { useCallback, useEffect, useMemo } from 'react'
import { Droppable } from 'react-beautiful-dnd'
import { Link as ReactLink } from 'react-router-dom'
import { Box, Text } from '@chakra-ui/react'
+import { useFeatureIsOn, useGrowthBook } from '@growthbook/growthbook-react'
+import { featureFlags } from '~shared/constants'
import {
AdminFormDto,
FormAuthType,
@@ -35,7 +37,7 @@ import { DraggableMyInfoFieldListOption } from '../FieldListOption'
import { FieldSection } from './FieldSection'
-const SGID_SUPPORTED: Set = new Set([
+const SGID_SUPPORTED_V1 = [
MyInfoAttribute.Name,
MyInfoAttribute.DateOfBirth,
MyInfoAttribute.PassportNumber,
@@ -44,26 +46,72 @@ const SGID_SUPPORTED: Set = new Set([
// phone number formats.
// MyInfo phone numbers support country code, while sgID-MyInfo does not.
// MyInfoAttribute.MobileNo,
-
- // FRM-1189: This is disabled due to slight different formatting.
- // We format the Myinfo response by separates lines in addresses with comma
- // Whereas sgID separates each line with newline.
- // This should be enabled in future work
- // MyInfoAttribute.RegisteredAddress,
-])
-
-/**
- * If sgID is used, checks if the corresponding
- * MyInfo field is supported by sgID.
- */
-const sgIDUnSupported = (
- form: AdminFormDto | undefined,
- fieldType: MyInfoAttribute,
-): boolean =>
- form?.authType === FormAuthType.SGID_MyInfo && !SGID_SUPPORTED.has(fieldType)
+]
+const SGID_SUPPORTED_V2 = [
+ ...SGID_SUPPORTED_V1,
+ MyInfoAttribute.Sex,
+ MyInfoAttribute.Race,
+ MyInfoAttribute.Nationality,
+ MyInfoAttribute.HousingType,
+ MyInfoAttribute.HdbType,
+ MyInfoAttribute.RegisteredAddress,
+ MyInfoAttribute.BirthCountry,
+ MyInfoAttribute.VehicleNo,
+ MyInfoAttribute.Employment,
+ MyInfoAttribute.WorkpassStatus,
+ MyInfoAttribute.Marital,
+ MyInfoAttribute.MobileNo,
+ MyInfoAttribute.WorkpassExpiryDate,
+ MyInfoAttribute.ResidentialStatus,
+ MyInfoAttribute.Dialect,
+ MyInfoAttribute.Occupation,
+ MyInfoAttribute.CountryOfMarriage,
+ MyInfoAttribute.MarriageCertNo,
+ MyInfoAttribute.MarriageDate,
+ MyInfoAttribute.DivorceDate,
+]
export const MyInfoFieldPanel = () => {
const { data: form, isLoading } = useCreateTabForm()
+
+ const { user } = useUser()
+
+ // FRM-1444: Remove once rollout is 100% and stable
+ const growthbook = useGrowthBook()
+
+ useEffect(() => {
+ if (growthbook) {
+ growthbook.setAttributes({
+ // Only update the `adminEmail` and `adminAgency` attributes, keep the rest the same
+ ...growthbook.getAttributes(),
+ adminEmail: user?.email,
+ adminAgency: user?.agency.shortName,
+ })
+ }
+ }, [growthbook, user])
+
+ const showSgidMyInfoV2 = useFeatureIsOn(featureFlags.myinfoSgid)
+
+ const sgidSupportedFinal = useMemo(() => {
+ return showSgidMyInfoV2 ? SGID_SUPPORTED_V2 : SGID_SUPPORTED_V1
+ }, [showSgidMyInfoV2])
+
+ /**
+ * If sgID is used, checks if the corresponding
+ * MyInfo field is supported by sgID.
+ */
+ const sgIDUnSupported = useCallback(
+ (form: AdminFormDto | undefined, fieldType: MyInfoAttribute): boolean => {
+ const sgidSupported: Set = new Set(sgidSupportedFinal)
+
+ return (
+ form?.authType === FormAuthType.SGID_MyInfo &&
+ !sgidSupported.has(fieldType)
+ )
+ },
+ [sgidSupportedFinal],
+ )
+
// myInfo should be disabled if
// 1. form response mode is not email mode
// 2. form auth type is not myInfo
@@ -83,9 +131,8 @@ export const MyInfoFieldPanel = () => {
(fieldType: MyInfoAttribute): boolean => {
return isDisabled || sgIDUnSupported(form, fieldType)
},
- [form, isDisabled],
+ [form, isDisabled, sgIDUnSupported],
)
- const { user } = useUser()
return (
<>
@@ -220,9 +267,6 @@ const MyInfoText = ({
return (
- {authType === FormAuthType.SGID_MyInfo
- ? 'Some MyInfo fields are not yet supported in your selected authentication type. '
- : null}
{`Only 30 MyInfo fields are allowed in Email mode (${numMyInfoFields}/30). `}
Learn more
diff --git a/frontend/src/features/login/LoginPage.tsx b/frontend/src/features/login/LoginPage.tsx
index 30f7a1c0e8..82e2c5e131 100644
--- a/frontend/src/features/login/LoginPage.tsx
+++ b/frontend/src/features/login/LoginPage.tsx
@@ -1,7 +1,11 @@
-import { useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
+import { useSearchParams } from 'react-router-dom'
+import { Stack } from '@chakra-ui/react'
+import { StatusCodes } from 'http-status-codes'
import { LOGGED_IN_KEY } from '~constants/localStorage'
import { useLocalStorage } from '~hooks/useLocalStorage'
+import { useToast } from '~hooks/useToast'
import { sendLoginOtp, verifyLoginOtp } from '~services/AuthService'
import {
@@ -10,7 +14,9 @@ import {
} from '~features/analytics/AnalyticsService'
import { LoginForm, LoginFormInputs } from './components/LoginForm'
+import { OrDivider } from './components/OrDivider'
import { OtpForm, OtpFormInputs } from './components/OtpForm'
+import { SgidLoginButton } from './components/SgidLoginButton'
import { LoginPageTemplate } from './LoginPageTemplate'
export type LoginOtpData = {
@@ -22,6 +28,27 @@ export const LoginPage = (): JSX.Element => {
const [email, setEmail] = useState()
const [otpPrefix, setOtpPrefix] = useState('')
+ const [params] = useSearchParams()
+ const toast = useToast({ isClosable: true, status: 'danger' })
+
+ const statusCode = params.get('status')
+ const toastMessage = useMemo(() => {
+ switch (statusCode) {
+ case null:
+ case StatusCodes.OK.toString():
+ return
+ case StatusCodes.UNAUTHORIZED.toString():
+ return 'Your sgID login session has expired. Please login again.'
+ default:
+ return 'Something went wrong. Please try again later.'
+ }
+ }, [statusCode])
+
+ useEffect(() => {
+ if (!toastMessage) return
+ toast({ description: toastMessage })
+ }, [toast, toastMessage])
+
const handleSendOtp = async ({ email }: LoginFormInputs) => {
const trimmedEmail = email.trim()
await sendLoginOtp(trimmedEmail).then(({ otpPrefix }) => {
@@ -60,7 +87,11 @@ export const LoginPage = (): JSX.Element => {
return (
{!email ? (
-
+
+
+
+
+
) : (
+type ModalErrorMessages = {
+ hideCloseButton?: boolean
+ preventBackdropDismissal?: boolean
+ header: string
+ body: string | (() => React.ReactElement)
+ cta: string
+ onCtaClick: (disclosureProps: ErrorDisclosureProps) => void
+}
+
+const MODAL_ERRORS: Record = {
+ NO_WORKEMAIL: {
+ hideCloseButton: true,
+ preventBackdropDismissal: true,
+ header: "Singpass login isn't available to you yet",
+ body: 'It is progressively being made available to agencies. In the meantime, please log in using your email address.',
+ cta: 'Back to login',
+ onCtaClick: () => window.location.assign(LOGIN_ROUTE),
+ },
+ INVALID_WORKEMAIL: {
+ header: "You don't have access to this service",
+ body: () => (
+
+ It may be available only to select agencies or authorised individuals.
+ If you believe you should have access to this service, please{' '}
+
+ contact us
+
+ .
+
+ ),
+ cta: 'Choose another account',
+ onCtaClick: (disclosureProps) => disclosureProps.onClose(),
+ },
+}
+
+const ErrorDisclosure = (
+ props: {
+ errorMessages: ModalErrorMessages | undefined
+ } & ErrorDisclosureProps,
+) => {
+ const isMobile = useIsMobile()
+ if (!props.errorMessages) {
+ return null
+ }
+ const { errorMessages, ...disclosureProps } = props
+ const {
+ onCtaClick,
+ body,
+ cta,
+ hideCloseButton,
+ header,
+ preventBackdropDismissal,
+ } = errorMessages
+ return (
+ props.onClose()}
+ closeOnOverlayClick={!preventBackdropDismissal}
+ >
+
+
+ {!hideCloseButton ? : null}
+ {header}
+
+
+ {typeof body === 'function' ? body() : {body}}
+
+
+
+
+
+
+
+ )
+}
+export const SelectProfilePage = (): JSX.Element => {
+ const profilesResponse = useSgidProfiles()
+ const [, setIsAuthenticated] = useLocalStorage(LOGGED_IN_KEY)
+ const { user } = useUser()
+ const [errorContext, setErrorContext] = useState<
+ ModalErrorMessages | undefined
+ >()
+
+ const errorDisclosure = useDisclosure()
+ const toast = useToast({ isClosable: true, status: 'danger' })
+
+ // If redirected back here but already authed, redirect to dashboard.
+ if (user) window.location.replace(DASHBOARD_ROUTE)
+ // User doesn't have any profiles, should reattempt to login
+ if (profilesResponse.error) window.location.replace(LOGIN_ROUTE)
+
+ useEffect(() => {
+ if (profilesResponse.data?.profiles.length === 0) {
+ errorDisclosure.onOpen()
+ setErrorContext(MODAL_ERRORS.NO_WORKEMAIL)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [profilesResponse.data?.profiles.length])
+
+ const handleSetProfile = async (profile: SgidPublicOfficerEmployment) => {
+ ApiService.post(SGID_PROFILES_ENDPOINT, {
+ workEmail: profile.work_email,
+ })
+ .then(() => {
+ window.location.assign(DASHBOARD_ROUTE)
+ setIsAuthenticated(true)
+ })
+ .catch((err) => {
+ console.log({ err })
+ if (err.code === StatusCodes.UNAUTHORIZED) {
+ errorDisclosure.onOpen()
+ setErrorContext(MODAL_ERRORS.INVALID_WORKEMAIL)
+ return
+ }
+ toast({ description: 'Something went wrong. Please try again later.' })
+ })
+ }
+
+ return (
+
+ }
+ >
+
+ Choose an account to continue to FormSG
+
+
+ {!profilesResponse.data ? (
+
+ ) : (
+ profilesResponse.data?.profiles.map((profile) => (
+ handleSetProfile(profile)}
+ />
+ ))
+ )}
+
+
+ Or, login manually using email and OTP
+
+
+
+
+ )
+}
+
+const ProfileItem = ({
+ profile,
+ onClick,
+}: {
+ profile: SgidPublicOfficerEmployment
+ onClick: () => void
+}) => {
+ return (
+
+
+
+ {profile.work_email}
+
+
+ {[profile.agency_name, profile.department_name].join(', ')}
+
+
+ {profile.employment_title}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/features/login/SgidLoginPage.tsx b/frontend/src/features/login/SgidLoginPage.tsx
deleted file mode 100644
index d1b4a97f37..0000000000
--- a/frontend/src/features/login/SgidLoginPage.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useEffect, useMemo } from 'react'
-import { BiLogInCircle } from 'react-icons/bi'
-import { useMutation } from 'react-query'
-import { useSearchParams } from 'react-router-dom'
-
-import { DASHBOARD_ROUTE } from '~constants/routes'
-import { useToast } from '~hooks/useToast'
-import { getSgidAuthUrl } from '~services/AuthService'
-import Button from '~components/Button'
-import { InlineMessage } from '~components/InlineMessage/InlineMessage'
-
-import { useUser } from '~features/user/queries'
-
-import { LoginPageTemplate } from './LoginPageTemplate'
-
-export const SgidLoginPage = (): JSX.Element => {
- const [params] = useSearchParams()
- const toast = useToast({ isClosable: true, status: 'danger' })
-
- const statusCode = params.get('status')
- const toastMessage = useMemo(() => {
- switch (statusCode) {
- case null:
- case '200':
- return
- case '401':
- return 'Your SGID-linked work email does not belong to a whitelisted public service email domain. Please use OTP login instead.'
- default:
- return 'Something went wrong. Please try again later.'
- }
- }, [statusCode])
-
- useEffect(() => {
- if (!toastMessage) return
- toast({ description: toastMessage })
- }, [toast, toastMessage])
-
- const { user } = useUser()
-
- // If redirected back here but already authed, redirect to dashboard.
- if (user) window.location.replace(DASHBOARD_ROUTE)
-
- const handleLoginMutation = useMutation(getSgidAuthUrl, {
- onSuccess: (data) => {
- window.location.assign(data.redirectUrl)
- },
- })
-
- return (
-
-
- This is an experimental service currently offered to OGP officers only.
-
- }
- onClick={() => handleLoginMutation.mutate()}
- isLoading={handleLoginMutation.isLoading}
- >
- Log in with Singpass app
-
-
- )
-}
diff --git a/frontend/src/features/login/components/LoginForm.tsx b/frontend/src/features/login/components/LoginForm.tsx
index 3acfb93d50..8ff4d87d02 100644
--- a/frontend/src/features/login/components/LoginForm.tsx
+++ b/frontend/src/features/login/components/LoginForm.tsx
@@ -1,16 +1,14 @@
import { useCallback } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
-import { FormControl, Stack, useBreakpointValue } from '@chakra-ui/react'
+import { FormControl, Stack } from '@chakra-ui/react'
import isEmail from 'validator/lib/isEmail'
-import { FORM_GUIDE } from '~constants/links'
import { INVALID_EMAIL_ERROR } from '~constants/validation'
import Button from '~components/Button'
import FormErrorMessage from '~components/FormControl/FormErrorMessage'
import FormLabel from '~components/FormControl/FormLabel'
import Input from '~components/Input'
-import Link from '~components/Link'
export type LoginFormInputs = {
email: string
@@ -36,8 +34,6 @@ export const LoginForm = ({ onSubmit }: LoginFormProps): JSX.Element => {
})
}
- const isMobile = useBreakpointValue({ base: true, xs: true, lg: false })
-
return (
)
diff --git a/frontend/src/features/login/components/OrDivider.tsx b/frontend/src/features/login/components/OrDivider.tsx
new file mode 100644
index 0000000000..c9c34be1a4
--- /dev/null
+++ b/frontend/src/features/login/components/OrDivider.tsx
@@ -0,0 +1,11 @@
+import { Divider, HStack, Text } from '@chakra-ui/react'
+
+export const OrDivider = (): JSX.Element => {
+ return (
+
+
+ or
+
+
+ )
+}
diff --git a/frontend/src/features/login/components/SgidLoginButton.tsx b/frontend/src/features/login/components/SgidLoginButton.tsx
new file mode 100644
index 0000000000..f19ebe0641
--- /dev/null
+++ b/frontend/src/features/login/components/SgidLoginButton.tsx
@@ -0,0 +1,43 @@
+import { useForm } from 'react-hook-form'
+import { useMutation } from 'react-query'
+import { Flex, Link, Text, VStack } from '@chakra-ui/react'
+
+import { SGID_VALID_ORG_PAGE } from '~shared/constants'
+
+import { SingpassFullLogoSvgr } from '~assets/svgrs/singpass/SingpassFullLogoSvgr'
+import { getSgidAuthUrl } from '~services/AuthService'
+import Button from '~components/Button'
+
+export const SgidLoginButton = (): JSX.Element => {
+ const { formState } = useForm()
+
+ const handleLoginMutation = useMutation(getSgidAuthUrl, {
+ onSuccess: (data) => {
+ window.location.assign(data.redirectUrl)
+ },
+ })
+ return (
+
+
+
+ For{' '}
+
+ select agencies
+
+
+
+ )
+}
diff --git a/frontend/src/features/login/index.ts b/frontend/src/features/login/index.ts
index f22bc829d7..0b15ab6929 100644
--- a/frontend/src/features/login/index.ts
+++ b/frontend/src/features/login/index.ts
@@ -1,2 +1,2 @@
export { LoginPage as default } from './LoginPage'
-export { SgidLoginPage } from './SgidLoginPage'
+export { SelectProfilePage } from './SelectProfilePage'
diff --git a/frontend/src/features/login/queries.ts b/frontend/src/features/login/queries.ts
new file mode 100644
index 0000000000..9dde41e788
--- /dev/null
+++ b/frontend/src/features/login/queries.ts
@@ -0,0 +1,23 @@
+import { useQuery, UseQueryResult } from 'react-query'
+
+import { SgidProfilesDto } from '~shared/types/auth'
+
+import { ApiError } from '~typings/core'
+
+import { ApiService } from '~services/ApiService'
+
+export const SGID_PROFILES_ENDPOINT = '/auth/sgid/profiles'
+
+const sgidProfileKeys = {
+ base: ['sgidProfiles'] as const,
+}
+
+export const useSgidProfiles = (): UseQueryResult<
+ SgidProfilesDto,
+ ApiError
+> => {
+ return useQuery(sgidProfileKeys.base, async () => {
+ const { data } = await ApiService.get(SGID_PROFILES_ENDPOINT)
+ return data
+ })
+}
diff --git a/frontend/src/mocks/msw/handlers/public-form.ts b/frontend/src/mocks/msw/handlers/public-form.ts
index e71c9e4b1f..cb1f44e0fd 100644
--- a/frontend/src/mocks/msw/handlers/public-form.ts
+++ b/frontend/src/mocks/msw/handlers/public-form.ts
@@ -168,10 +168,15 @@ export const BASE_FORM = {
{
ValidationOptions: {
_id: '6148614ee2fb650012928dd9',
- customVal: null,
selectedValidation: null,
- customMin: null,
- customMax: null,
+ LengthValidationOptions: {
+ selectedLengthValidation: null,
+ customVal: null,
+ },
+ RangeValidationOptions: {
+ customMin: null,
+ customMax: null,
+ },
id: '6148614ee2fb650012928dd9',
},
title: 'Number',
diff --git a/package-lock.json b/package-lock.json
index 4429128abb..4e0e45c8e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "FormSG",
- "version": "6.82.0",
+ "version": "6.83.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "FormSG",
- "version": "6.82.0",
+ "version": "6.83.0",
"hasInstallScript": true,
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.347.1",
diff --git a/package.json b/package.json
index a7d9462c58..a44f0c421b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "FormSG",
"description": "Form Manager for Government",
- "version": "6.82.0",
+ "version": "6.83.0",
"homepage": "https://form.gov.sg",
"authors": [
"FormSG "
diff --git a/serverless/virus-scanner/package-lock.json b/serverless/virus-scanner/package-lock.json
index 435dfa20db..f6475eca16 100644
--- a/serverless/virus-scanner/package-lock.json
+++ b/serverless/virus-scanner/package-lock.json
@@ -925,12 +925,12 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.22.10",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz",
- "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==",
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dev": true,
"dependencies": {
- "@babel/highlight": "^7.22.10",
+ "@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
},
"engines": {
@@ -1054,12 +1054,12 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.22.10",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz",
- "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.22.10",
+ "@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -1109,22 +1109,22 @@
"dev": true
},
"node_modules/@babel/helper-environment-visitor": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
- "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
- "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"dependencies": {
- "@babel/template": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
@@ -1216,9 +1216,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
- "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -1248,12 +1248,12 @@
}
},
"node_modules/@babel/highlight": {
- "version": "7.22.10",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz",
- "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dev": true,
"dependencies": {
- "@babel/helper-validator-identifier": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
@@ -1324,9 +1324,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz",
- "integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -1513,33 +1513,33 @@
}
},
"node_modules/@babel/template": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
- "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz",
- "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.10",
- "@babel/generator": "^7.22.10",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.22.11",
- "@babel/types": "^7.22.11",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -1548,13 +1548,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz",
- "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -2980,9 +2980,9 @@
}
},
"node_modules/@types/eslint": {
- "version": "8.44.2",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
- "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==",
+ "version": "8.44.6",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz",
+ "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==",
"dev": true,
"peer": true,
"dependencies": {
@@ -2991,9 +2991,9 @@
}
},
"node_modules/@types/eslint-scope": {
- "version": "3.7.4",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
- "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
+ "version": "3.7.6",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz",
+ "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==",
"dev": true,
"peer": true,
"dependencies": {
@@ -3002,9 +3002,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
- "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz",
+ "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==",
"dev": true,
"peer": true
},
@@ -3075,9 +3075,9 @@
"dev": true
},
"node_modules/@types/json-schema": {
- "version": "7.0.12",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
- "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
+ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
"dev": true,
"peer": true
},
@@ -5553,9 +5553,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
- "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
+ "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==",
"dev": true,
"peer": true
},
@@ -10386,9 +10386,9 @@
}
},
"node_modules/terser": {
- "version": "5.19.2",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
- "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz",
+ "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==",
"dev": true,
"peer": true,
"dependencies": {
@@ -11161,9 +11161,9 @@
"dev": true
},
"node_modules/webpack": {
- "version": "5.88.2",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz",
- "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==",
+ "version": "5.89.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
+ "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
"dev": true,
"peer": true,
"dependencies": {
@@ -12338,12 +12338,12 @@
}
},
"@babel/code-frame": {
- "version": "7.22.10",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz",
- "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==",
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dev": true,
"requires": {
- "@babel/highlight": "^7.22.10",
+ "@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
},
"dependencies": {
@@ -12443,12 +12443,12 @@
}
},
"@babel/generator": {
- "version": "7.22.10",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz",
- "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"requires": {
- "@babel/types": "^7.22.10",
+ "@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -12491,19 +12491,19 @@
}
},
"@babel/helper-environment-visitor": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
- "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true
},
"@babel/helper-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
- "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"requires": {
- "@babel/template": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
}
},
"@babel/helper-hoist-variables": {
@@ -12568,9 +12568,9 @@
"dev": true
},
"@babel/helper-validator-identifier": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
- "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true
},
"@babel/helper-validator-option": {
@@ -12591,12 +12591,12 @@
}
},
"@babel/highlight": {
- "version": "7.22.10",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz",
- "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dev": true,
"requires": {
- "@babel/helper-validator-identifier": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
@@ -12654,9 +12654,9 @@
}
},
"@babel/parser": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz",
- "integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true
},
"@babel/plugin-syntax-async-generators": {
@@ -12786,42 +12786,42 @@
}
},
"@babel/template": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
- "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
}
},
"@babel/traverse": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz",
- "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.22.10",
- "@babel/generator": "^7.22.10",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.22.11",
- "@babel/types": "^7.22.11",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz",
- "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},
@@ -13994,9 +13994,9 @@
}
},
"@types/eslint": {
- "version": "8.44.2",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
- "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==",
+ "version": "8.44.6",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz",
+ "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==",
"dev": true,
"peer": true,
"requires": {
@@ -14005,9 +14005,9 @@
}
},
"@types/eslint-scope": {
- "version": "3.7.4",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
- "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
+ "version": "3.7.6",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz",
+ "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==",
"dev": true,
"peer": true,
"requires": {
@@ -14016,9 +14016,9 @@
}
},
"@types/estree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
- "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz",
+ "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==",
"dev": true,
"peer": true
},
@@ -14089,9 +14089,9 @@
"dev": true
},
"@types/json-schema": {
- "version": "7.0.12",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
- "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
+ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
"dev": true,
"peer": true
},
@@ -16061,9 +16061,9 @@
}
},
"es-module-lexer": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
- "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
+ "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==",
"dev": true,
"peer": true
},
@@ -19720,9 +19720,9 @@
}
},
"terser": {
- "version": "5.19.2",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
- "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz",
+ "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==",
"dev": true,
"peer": true,
"requires": {
@@ -20278,9 +20278,9 @@
"dev": true
},
"webpack": {
- "version": "5.88.2",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz",
- "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==",
+ "version": "5.89.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
+ "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
"dev": true,
"peer": true,
"requires": {
diff --git a/serverless/virus-scanner/package.json b/serverless/virus-scanner/package.json
index ae195a3ced..973f4ab9ad 100644
--- a/serverless/virus-scanner/package.json
+++ b/serverless/virus-scanner/package.json
@@ -27,25 +27,24 @@
"@types/aws-lambda": "^8.10.101",
"@types/clamscan": "^2.0.2",
"@types/convict": "^6.1.1",
+ "@types/jest": "^29.5.1",
"@types/node": "^18.0.1",
"@types/pino": "^7.0.5",
"@types/uuid": "^9.0.2",
"@types/watch": "^1.0.3",
"dotenv-cli": "^6.0.0",
- "serverless": "^3.19.0",
- "serverless-dotenv-plugin": "^4.0.1",
- "typescript": "^4.7.4",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"jest-extended": "^3.2.4",
"jest-localstorage-mock": "^2.4.26",
"jest-mock-axios": "^4.7.2",
+ "serverless": "^3.19.0",
+ "serverless-dotenv-plugin": "^4.0.1",
"ts-jest": "^29.0.5",
"ts-loader": "^8.2.0",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"type-fest": "^3.10.0",
- "typescript": "^4.9.4",
- "@types/jest": "^29.5.1"
+ "typescript": "^4.9.4"
}
}
diff --git a/shared/constants/feature-flags.ts b/shared/constants/feature-flags.ts
index f1c57d7bbc..de9158b6a1 100644
--- a/shared/constants/feature-flags.ts
+++ b/shared/constants/feature-flags.ts
@@ -8,4 +8,5 @@ export const featureFlags = {
'encryption-boundary-shift-hard-validation' as const,
encryptionBoundaryShiftVirusScanner:
'encryption-boundary-shift-virus-scanner' as const,
+ myinfoSgid: 'myinfo-sgid' as const,
}
diff --git a/shared/constants/links.ts b/shared/constants/links.ts
index 3e08bb7ff5..2bf18b30f8 100644
--- a/shared/constants/links.ts
+++ b/shared/constants/links.ts
@@ -1,3 +1,6 @@
export const SUPPORT_FORM_LINK = 'https://go.gov.sg/formsg-support'
export const PUBLIC_PAYMENTS_GUIDE_LINK =
'https://go.gov.sg/formsg-guide-payments'
+
+export const SGID_VALID_ORG_PAGE =
+ 'https://docs.id.gov.sg/faq-users#as-a-government-officer-why-am-i-not-able-to-login-to-my-work-tool-using-sgid'
diff --git a/shared/types/auth.ts b/shared/types/auth.ts
new file mode 100644
index 0000000000..484e8efe61
--- /dev/null
+++ b/shared/types/auth.ts
@@ -0,0 +1,12 @@
+export type SgidProfilesDto = {
+ profiles: SgidPublicOfficerEmploymentList
+}
+
+export type SgidPublicOfficerEmployment = {
+ agency_name: string
+ department_name: string
+ employment_title: string
+ employment_type: string
+ work_email: string
+}
+export type SgidPublicOfficerEmploymentList = Array
diff --git a/src/app/modules/auth/auth.types.ts b/src/app/modules/auth/auth.types.ts
index ba7e1a50ee..1b41ccff46 100644
--- a/src/app/modules/auth/auth.types.ts
+++ b/src/app/modules/auth/auth.types.ts
@@ -1,3 +1,10 @@
+import { SgidPublicOfficerEmploymentList } from 'shared/types/auth'
+
import { IPopulatedUser } from 'src/types'
export type SessionUser = IPopulatedUser
+
+export type SgidUser = {
+ profiles: SgidPublicOfficerEmploymentList
+ expiry: number
+}
diff --git a/src/app/modules/auth/sgid/auth-sgid.controller.ts b/src/app/modules/auth/sgid/auth-sgid.controller.ts
index 99033b4703..fe5019de55 100644
--- a/src/app/modules/auth/sgid/auth-sgid.controller.ts
+++ b/src/app/modules/auth/sgid/auth-sgid.controller.ts
@@ -1,6 +1,8 @@
import { StatusCodes } from 'http-status-codes'
import { ErrorDto, GetSgidAuthUrlResponseDto } from 'shared/types'
+import { SgidProfilesDto } from 'shared/types/auth'
+import { resolveRedirectionUrl } from '../../../../app/utils/urls'
import { createLoggerWithLabel } from '../../../config/logger'
import { createReqMeta } from '../../../utils/request'
import { ControllerHandler } from '../../core/core.types'
@@ -37,7 +39,7 @@ export const generateAuthUrl: ControllerHandler<
error,
})
return res
- .sendStatus(StatusCodes.INTERNAL_SERVER_ERROR)
+ .status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({
message:
'Generating SGID authentication url failed. Please try again later.',
@@ -46,7 +48,14 @@ export const generateAuthUrl: ControllerHandler<
})
}
-export const handleLogin: ControllerHandler<
+/**
+ * Handler for GET /api/v3/auth/sgid/login/callback endpoint.
+ *
+ * @return 200 with redirect to frontend /login/callback if there are no errors
+ * @return 400 when code or state is not provided, or state is incorrect
+ * @return 500 when processing the code verifier cookie fails, or when an unknown error occurs
+ */
+export const handleLoginCallback: ControllerHandler<
unknown,
ErrorDto | undefined,
unknown,
@@ -57,7 +66,7 @@ export const handleLogin: ControllerHandler<
res.clearCookie(SGID_CODE_VERIFIER_COOKIE_NAME)
const logMeta = {
- action: 'handleLogin',
+ action: 'handleLoginCallback',
code,
state,
...createReqMeta(req),
@@ -65,8 +74,6 @@ export const handleLogin: ControllerHandler<
const coreErrorMessage = 'Failed to log in via SGID. Please try again later.'
- let status
-
if (!code || state !== SGID_LOGIN_OAUTH_STATE) {
logger.error({
message:
@@ -74,59 +81,178 @@ export const handleLogin: ControllerHandler<
meta: logMeta,
})
- status = StatusCodes.BAD_REQUEST
- } else if (!codeVerifier) {
+ const status = StatusCodes.BAD_REQUEST
+ res.redirect(resolveRedirectionUrl(`/login?status=${status}`))
+ return
+ }
+ if (!codeVerifier) {
logger.error({
message: 'Error logging in via sgID: code verifier cookie is empty',
meta: logMeta,
})
- status = StatusCodes.BAD_REQUEST
- } else {
- await AuthSgidService.retrieveAccessToken(code, codeVerifier)
- .andThen(({ accessToken, sub }) =>
- AuthSgidService.retrieveUserInfo(accessToken, sub),
- )
- .andThen((email) =>
- AuthService.validateEmailDomain(email).andThen((agency) =>
- UserService.retrieveUser(email, agency._id),
- ),
- )
- .map((user) => {
- if (!req.session) {
- logger.error({
- message: 'Error logging in user; req.session is undefined',
- meta: logMeta,
- })
-
- status = StatusCodes.INTERNAL_SERVER_ERROR
- return
- }
-
- // Add user info to session.
- const { _id } = user.toObject() as SessionUser
- req.session.user = { _id }
- logger.info({
- message: `Successfully logged in user ${user._id}`,
- meta: logMeta,
- })
+ const status = StatusCodes.BAD_REQUEST
+ res.redirect(resolveRedirectionUrl(`/login?status=${status}`))
+ return
+ }
+ if (!req.session) {
+ logger.error({
+ message: 'Error logging in user; req.session is undefined',
+ meta: logMeta,
+ })
- // Redirect user to the SGID login page
- status = StatusCodes.OK
- })
- // Step 3b: Error occured in one of the steps.
- .mapErr((error) => {
- logger.warn({
- message: 'Error occurred when trying to log in via SGID',
- meta: logMeta,
- error,
- })
+ const status = StatusCodes.INTERNAL_SERVER_ERROR
+ res.redirect(resolveRedirectionUrl(`/login?status=${status}`))
+ return
+ }
- const { statusCode } = mapRouteError(error, coreErrorMessage)
+ await AuthSgidService.retrieveAccessToken(code, codeVerifier)
+ .andThen(({ accessToken, sub }) =>
+ AuthSgidService.retrieveUserInfo(accessToken, sub),
+ )
+ .map((profiles) => {
+ // expire profiles after 5 minutes to avoid situations where login-jacking when
+ // the previous user navigated away without selecting a profile
+ req.session.sgid = { profiles, expiry: Date.now() + 1000 * 60 * 5 }
- status = statusCode
+ // User needs to select profile that will be used for the login
+ res.redirect(resolveRedirectionUrl(`/login/select-profile`))
+ return
+ })
+ .mapErr((error) => {
+ logger.warn({
+ message: 'Error occurred when trying to log in via SGID',
+ meta: logMeta,
+ error,
})
+
+ const { statusCode } = mapRouteError(error, coreErrorMessage)
+
+ res.redirect(resolveRedirectionUrl(`/login?status=${statusCode}`))
+ return
+ })
+}
+
+/**
+ * Handler for GET /api/v3/auth/sgid/profiles endpoint.
+ *
+ * @return 200 with list of profiles
+ * @return 400 when session or profile is invalid
+ * @return 401 when session has expired
+ */
+export const getProfiles: ControllerHandler<
+ unknown,
+ SgidProfilesDto,
+ unknown
+> = async (req, res) => {
+ const logMeta = {
+ action: 'getProfiles',
+ ...createReqMeta(req),
+ }
+ if (!req.session) {
+ logger.error({
+ message: 'Error logging in via sgID: session is invalid',
+ meta: logMeta,
+ })
+ return res.status(StatusCodes.BAD_REQUEST).send()
+ }
+
+ if (!req.session.sgid?.profiles) {
+ logger.error({
+ message: 'Error logging in via sgID: profile is invalid',
+ meta: logMeta,
+ })
+ return res.status(StatusCodes.BAD_REQUEST).send()
+ }
+
+ if (req.session.sgid.expiry < Date.now()) {
+ logger.info({
+ message: 'Error logging in via sgID: session has expired',
+ meta: logMeta,
+ })
+ res.redirect(StatusCodes.UNAUTHORIZED, resolveRedirectionUrl(`/login`))
+ return
}
- return res.redirect(`/ogp-login?status=${status}`)
+ return res
+ .status(StatusCodes.OK)
+ .json({ profiles: req.session.sgid.profiles })
+}
+
+/**
+ * Handler for POST /api/v3/auth/sgid/profiles endpoint.
+ *
+ * @return 200 when OTP has been been successfully sent
+ * @return 400 when session, profile, or workEmail is invalid
+ * @return 401 when email domain is not whitelisted
+ * @return 500 when unknown errors occurs during email validation, or creating the new account
+ */
+export const setProfile: ControllerHandler<
+ unknown,
+ { message: string } | ErrorDto,
+ { workEmail: string }
+> = async (req, res) => {
+ const logMeta = {
+ action: 'setProfile',
+ ...createReqMeta(req),
+ }
+
+ const coreErrorMessage = 'Failed to log in via SGID. Please try again later.'
+
+ if (!req.session) {
+ const message = 'Error logging in via sgID: session is invalid'
+ logger.error({
+ message,
+ meta: logMeta,
+ })
+ return res.status(StatusCodes.BAD_REQUEST).json({ message })
+ }
+
+ if (!req.session.sgid?.profiles) {
+ const message = 'Error logging in via sgID: profile is invalid'
+ logger.error({
+ message,
+ meta: logMeta,
+ })
+ return res.status(StatusCodes.BAD_REQUEST).json({ message })
+ }
+
+ const selectedProfile = req.session.sgid.profiles.find(
+ (profile) => profile.work_email === req.body.workEmail,
+ )
+ if (!selectedProfile) {
+ const message = 'Error logging in via sgID: selected profile is invalid'
+ logger.error({
+ message,
+ meta: logMeta,
+ })
+ return res.status(StatusCodes.BAD_REQUEST).json({ message })
+ }
+
+ return AuthService.validateEmailDomain(selectedProfile.work_email)
+ .andThen((agency) =>
+ UserService.retrieveUser(selectedProfile.work_email, agency._id),
+ )
+ .map((user) => {
+ // Add user info to session.
+ const { _id } = user.toObject() as SessionUser
+ req.session.user = { _id }
+ logger.info({
+ message: `Successfully logged in user ${user._id}`,
+ meta: logMeta,
+ })
+ return res.status(StatusCodes.OK).json({ message: 'Ok' })
+ })
+ .mapErr((error) => {
+ const message = 'Error occurred when trying to log in via SGID'
+ logger.warn({
+ message,
+ meta: logMeta,
+ error,
+ })
+
+ const { statusCode } = mapRouteError(error, coreErrorMessage)
+
+ return res.status(statusCode).json({ message })
+ })
}
diff --git a/src/app/modules/auth/sgid/auth-sgid.service.ts b/src/app/modules/auth/sgid/auth-sgid.service.ts
index 3322b95e75..1426c1a010 100644
--- a/src/app/modules/auth/sgid/auth-sgid.service.ts
+++ b/src/app/modules/auth/sgid/auth-sgid.service.ts
@@ -1,6 +1,7 @@
import { generatePkcePair, SgidClient } from '@opengovsg/sgid-client'
import fs from 'fs'
import { err, ok, Result, ResultAsync } from 'neverthrow'
+import { SgidPublicOfficerEmploymentList } from 'shared/types/auth'
import { ISgidVarsSchema } from 'src/types'
@@ -13,9 +14,9 @@ import {
} from '../../sgid/sgid.errors'
const logger = createLoggerWithLabel(module)
-
export const SGID_LOGIN_OAUTH_STATE = 'login'
-const SGID_OGP_WORK_EMAIL_SCOPE = 'ogpofficerinfo.work_email'
+const SGID_POCDEX_PUBLIC_OFFICER_EMPLOYMENTS_SCOPE =
+ 'pocdex.public_officer_details'
export class AuthSgidServiceClass {
private client: SgidClient
@@ -56,7 +57,9 @@ export class AuthSgidServiceClass {
try {
const result = this.client.authorizationUrl({
state: SGID_LOGIN_OAUTH_STATE,
- scope: ['openid', SGID_OGP_WORK_EMAIL_SCOPE].join(' '),
+ scope: ['openid', SGID_POCDEX_PUBLIC_OFFICER_EMPLOYMENTS_SCOPE].join(
+ ' ',
+ ),
nonce: null,
codeChallenge,
})
@@ -108,11 +111,14 @@ export class AuthSgidServiceClass {
retrieveUserInfo(
accessToken: string,
sub: string,
- ): ResultAsync {
+ ): ResultAsync {
return ResultAsync.fromPromise(
- this.client
- .userinfo({ accessToken, sub })
- .then(({ data }) => data[SGID_OGP_WORK_EMAIL_SCOPE]),
+ this.client.userinfo({ accessToken, sub }).then(({ data }) => {
+ const employments: SgidPublicOfficerEmploymentList = JSON.parse(
+ data[SGID_POCDEX_PUBLIC_OFFICER_EMPLOYMENTS_SCOPE],
+ )
+ return employments
+ }),
(error) => {
logger.error({
message: 'Failed to retrieve user info from sgID',
@@ -124,7 +130,15 @@ export class AuthSgidServiceClass {
})
return new SgidFetchUserInfoError()
},
- )
+ ).andThen((employments) => {
+ // Ensure that all emails are in lowercase
+ const cleanedProfile = employments.map((employment) => ({
+ ...employment,
+ work_email: employment.work_email.toLowerCase(),
+ }))
+
+ return ok(cleanedProfile)
+ })
}
}
diff --git a/src/app/modules/myinfo/myinfo.adapter.ts b/src/app/modules/myinfo/myinfo.adapter.ts
index 18e569f303..27d12b298b 100644
--- a/src/app/modules/myinfo/myinfo.adapter.ts
+++ b/src/app/modules/myinfo/myinfo.adapter.ts
@@ -14,6 +14,7 @@ import {
MyInfoChildVaxxStatus,
MyInfoDataTransformer,
} from '../../../../shared/types'
+import { createLoggerWithLabel } from '../../config/logger'
import {
formatAddress,
@@ -26,6 +27,8 @@ import {
} from './myinfo.format'
import { isMyInfoChildrenBirthRecords } from './myinfo.util'
+const logger = createLoggerWithLabel(module)
+
/**
* Converts an internal MyInfo attribute used in FormSG to a scope
* which can be used to retrieve data from MyInfo.
@@ -422,6 +425,14 @@ export class MyInfoData
// Above cases should be exhaustive for all attributes supported by Form.
// Fall back to leaving field editable as data shape is unknown.
default:
+ logger.error({
+ message: 'Unknown attribute found in Singpass MyInfo field',
+ meta: {
+ action: '_isDataReadOnly',
+ myInfoValue,
+ attr,
+ },
+ })
return false
}
}
diff --git a/src/app/modules/myinfo/myinfo.service.ts b/src/app/modules/myinfo/myinfo.service.ts
index 3a8f0f8047..61a3217768 100644
--- a/src/app/modules/myinfo/myinfo.service.ts
+++ b/src/app/modules/myinfo/myinfo.service.ts
@@ -281,6 +281,7 @@ export class MyInfoServiceClass {
fieldValue,
myInfoAttr,
myInfoConstantsList,
+ myInfoData,
)
}
}
diff --git a/src/app/modules/myinfo/myinfo.util.ts b/src/app/modules/myinfo/myinfo.util.ts
index 533d18a26b..c29b22f78f 100644
--- a/src/app/modules/myinfo/myinfo.util.ts
+++ b/src/app/modules/myinfo/myinfo.util.ts
@@ -44,12 +44,14 @@ import {
FormAuthNoEsrvcIdError,
FormNotFoundError,
} from '../form/form.errors'
+import { SGIDMyInfoData } from '../sgid/sgid.adapter'
import { SGID_MYINFO_LOGIN_COOKIE_NAME } from '../sgid/sgid.constants'
import {
ProcessedChildrenResponse,
ProcessedFieldResponse,
} from '../submission/submission.types'
+import { MyInfoData } from './myinfo.adapter'
import { MYINFO_LOGIN_COOKIE_NAME } from './myinfo.constants'
import {
MyInfoCookieStateError,
@@ -542,7 +544,7 @@ export const handleMyInfoChildHashResponse = (
*/
export const getMyInfoAttributeConstantsList = (
myInfoAttr: string | string[],
-) => {
+): string[] | undefined => {
switch (myInfoAttr) {
case MyInfoAttribute.Occupation:
return myInfoOccupations
@@ -576,15 +578,22 @@ export const logIfFieldValueNotInMyinfoList = (
fieldValue: string,
myInfoAttr: string | string[],
myInfoList: string[],
+ myInfoData: MyInfoData | SGIDMyInfoData,
) => {
const isFieldValueInMyinfoList = myInfoList.includes(fieldValue)
- if (!isFieldValueInMyinfoList) {
+ const myInfoSource =
+ myInfoData instanceof MyInfoData ? 'Singpass MyInfo' : 'SGID MyInfo'
+ // SGID returns NA instead of empty field values, we don't need this to be logged
+ // as this is expected behaviour
+ const isNAFromSgid = myInfoAttr === 'SGID MyInfo' && fieldValue === 'NA'
+ if (!isNAFromSgid || !isFieldValueInMyinfoList) {
logger.error({
message: 'Myinfo field value not found in existing Myinfo constants list',
meta: {
action: 'prefillAndSaveMyInfoFields',
myInfoFieldValue: fieldValue,
myInfoAttr,
+ myInfoSource,
},
})
}
diff --git a/src/app/modules/sgid/sgid.adapter.ts b/src/app/modules/sgid/sgid.adapter.ts
index 1885c239f7..ebe3d73710 100644
--- a/src/app/modules/sgid/sgid.adapter.ts
+++ b/src/app/modules/sgid/sgid.adapter.ts
@@ -2,27 +2,67 @@ import {
MyInfoAttribute as InternalAttr,
MyInfoDataTransformer,
} from '../../../../shared/types'
+import { createLoggerWithLabel } from '../../config/logger'
import {
SGID_MYINFO_NRIC_NUMBER_SCOPE,
SGIDScope as ExternalAttr,
} from './sgid.constants'
+import { formatAddress, formatVehicles } from './sgid.format'
import { SGIDScopeToValue } from './sgid.types'
+const logger = createLoggerWithLabel(module)
+
export const internalAttrToScope = (attr: InternalAttr): ExternalAttr => {
switch (attr) {
case InternalAttr.Name:
return ExternalAttr.Name
+ case InternalAttr.Sex:
+ return ExternalAttr.Sex
case InternalAttr.DateOfBirth:
return ExternalAttr.DateOfBirth
+ case InternalAttr.Race:
+ return ExternalAttr.Race
+ case InternalAttr.Nationality:
+ return ExternalAttr.Nationality
+ case InternalAttr.HousingType:
+ return ExternalAttr.HousingType
+ case InternalAttr.HdbType:
+ return ExternalAttr.HdbType
case InternalAttr.PassportNumber:
return ExternalAttr.PassportNumber
case InternalAttr.PassportExpiryDate:
return ExternalAttr.PassportExpiryDate
- case InternalAttr.MobileNo:
- return ExternalAttr.MobileNumber
case InternalAttr.RegisteredAddress:
return ExternalAttr.RegisteredAddress
+ case InternalAttr.BirthCountry:
+ return ExternalAttr.BirthCountry
+ case InternalAttr.VehicleNo:
+ return ExternalAttr.VehicleNo
+ case InternalAttr.Employment:
+ return ExternalAttr.Employment
+ case InternalAttr.WorkpassStatus:
+ return ExternalAttr.WorkpassStatus
+ case InternalAttr.WorkpassExpiryDate:
+ return ExternalAttr.WorkpassExpiryDate
+ case InternalAttr.Marital:
+ return ExternalAttr.MaritalStatus
+ case InternalAttr.MobileNo:
+ return ExternalAttr.MobileNoWithCountryCode
+ case InternalAttr.ResidentialStatus:
+ return ExternalAttr.ResidentialStatus
+ case InternalAttr.Dialect:
+ return ExternalAttr.Dialect
+ case InternalAttr.Occupation:
+ return ExternalAttr.Occupation
+ case InternalAttr.CountryOfMarriage:
+ return ExternalAttr.CountryOfMarriage
+ case InternalAttr.MarriageCertNo:
+ return ExternalAttr.MarriageCertNo
+ case InternalAttr.MarriageDate:
+ return ExternalAttr.MarriageDate
+ case InternalAttr.DivorceDate:
+ return ExternalAttr.DivorceDate
default:
// This should be removed once sgID reaches parity with MyInfo.
// For now, the returned value will be automatically filtered
@@ -44,16 +84,52 @@ const internalAttrToSGIDExternal = (
switch (attr) {
case InternalAttr.Name:
return ExternalAttr.Name
+ case InternalAttr.Sex:
+ return ExternalAttr.Sex
case InternalAttr.DateOfBirth:
return ExternalAttr.DateOfBirth
+ case InternalAttr.Race:
+ return ExternalAttr.Race
+ case InternalAttr.Nationality:
+ return ExternalAttr.Nationality
+ case InternalAttr.HousingType:
+ return ExternalAttr.HousingType
+ case InternalAttr.HdbType:
+ return ExternalAttr.HdbType
case InternalAttr.PassportNumber:
return ExternalAttr.PassportNumber
case InternalAttr.PassportExpiryDate:
return ExternalAttr.PassportExpiryDate
case InternalAttr.MobileNo:
- return ExternalAttr.MobileNumber
+ return ExternalAttr.MobileNoWithCountryCode
case InternalAttr.RegisteredAddress:
return ExternalAttr.RegisteredAddress
+ case InternalAttr.BirthCountry:
+ return ExternalAttr.BirthCountry
+ case InternalAttr.ResidentialStatus:
+ return ExternalAttr.ResidentialStatus
+ case InternalAttr.VehicleNo:
+ return ExternalAttr.VehicleNo
+ case InternalAttr.Employment:
+ return ExternalAttr.Employment
+ case InternalAttr.WorkpassStatus:
+ return ExternalAttr.WorkpassStatus
+ case InternalAttr.WorkpassExpiryDate:
+ return ExternalAttr.WorkpassExpiryDate
+ case InternalAttr.Marital:
+ return ExternalAttr.MaritalStatus
+ case InternalAttr.Dialect:
+ return ExternalAttr.Dialect
+ case InternalAttr.Occupation:
+ return ExternalAttr.Occupation
+ case InternalAttr.CountryOfMarriage:
+ return ExternalAttr.CountryOfMarriage
+ case InternalAttr.MarriageCertNo:
+ return ExternalAttr.MarriageCertNo
+ case InternalAttr.MarriageDate:
+ return ExternalAttr.MarriageDate
+ case InternalAttr.DivorceDate:
+ return ExternalAttr.DivorceDate
default:
return undefined
}
@@ -79,16 +155,37 @@ export class SGIDMyInfoData
/**
* Placeholder. For now, there are not enough public fields in
* sgID's MyInfo catalog to require significant formatting.
+ * SGID returns 'NA' for field values that do not exist (vs empty string returned by Singpass MyInfo)
* @param attr sgID's MyInfo OAuth scope.
* @returns the formatted field.
*/
_formatFieldValue(attr: ExternalAttr): string | undefined {
- return this.#payload[attr]
+ const fieldValue = this.#payload[attr]
+ switch (attr) {
+ case ExternalAttr.RegisteredAddress:
+ return formatAddress(fieldValue)
+ case ExternalAttr.VehicleNo:
+ return formatVehicles(fieldValue)
+ default:
+ // SGID returns NA if they do not have the value. We want to return an empty string instead,
+ // so that this can be processed by our frontend in the same way as Singpass MyInfo
+ return fieldValue === 'NA' ? '' : fieldValue
+ }
}
/**
- * Refer to the myInfo data catalogue to see which fields should be read-only
- * and which fields should be editable by the user.
+ * SGID only returns verified MyInfo fields, unless the field contains marriage-related information
+ * (decision by SNDGO & MSF due to overseas unregistered marriages).
+ * An empty myInfo field will always evaluate
+ * to false so that the field can be filled by form-filler.
+ *
+ * The affected marriage fields are:
+ * - marital
+ * - marriagedate
+ * - divorcedate
+ * - countryofmarriage
+ * - marriagecertno
+ *
* @param attr sgID MyInfo OAuth scope.
* @param fieldValue FormSG field value.
* @returns Whether the data/field should be readonly.
@@ -98,15 +195,43 @@ export class SGIDMyInfoData
if (!data || !fieldValue) return false
switch (attr) {
- case ExternalAttr.MobileNumber:
+ case ExternalAttr.MobileNoWithCountryCode:
case ExternalAttr.RegisteredAddress:
case ExternalAttr.Name:
case ExternalAttr.PassportNumber:
case ExternalAttr.DateOfBirth:
+ case ExternalAttr.ResidentialStatus:
case ExternalAttr.PassportExpiryDate:
+ case ExternalAttr.Sex:
+ case ExternalAttr.Race:
+ case ExternalAttr.Nationality:
+ case ExternalAttr.HousingType:
+ case ExternalAttr.HdbType:
+ case ExternalAttr.BirthCountry:
+ case ExternalAttr.VehicleNo:
+ case ExternalAttr.Employment:
+ case ExternalAttr.WorkpassStatus:
+ case ExternalAttr.WorkpassExpiryDate:
+ case ExternalAttr.Dialect:
+ case ExternalAttr.Occupation:
return !!data
+ // Fields required to always be editable according to MyInfo docs
+ case ExternalAttr.MaritalStatus:
+ case ExternalAttr.CountryOfMarriage:
+ case ExternalAttr.MarriageCertNo:
+ case ExternalAttr.MarriageDate:
+ case ExternalAttr.DivorceDate:
+ return false
// Fall back to leaving field editable as data shape is unknown.
default:
+ logger.error({
+ message: 'Unknown attribute found in SGID MyInfo field',
+ meta: {
+ action: '_isDataReadOnly',
+ fieldValue,
+ attr,
+ },
+ })
return false
}
}
@@ -127,9 +252,17 @@ export class SGIDMyInfoData
}
}
const fieldValue = this._formatFieldValue(externalAttr)
+ logger.info({
+ message: 'get field value',
+ meta: {
+ action: 'getFieldValueForAttr',
+ fieldValue,
+ externalAttr,
+ },
+ })
return {
fieldValue,
- isReadOnly: true,
+ isReadOnly: this._isDataReadOnly(externalAttr, fieldValue),
}
}
}
diff --git a/src/app/modules/sgid/sgid.constants.ts b/src/app/modules/sgid/sgid.constants.ts
index 57ccc8855a..4350ccc99a 100644
--- a/src/app/modules/sgid/sgid.constants.ts
+++ b/src/app/modules/sgid/sgid.constants.ts
@@ -33,7 +33,27 @@ export enum SGIDScope {
DateOfBirth = 'myinfo.date_of_birth',
PassportNumber = 'myinfo.passport_number',
PassportExpiryDate = 'myinfo.passport_expiry_date',
- MobileNumber = 'myinfo.mobile_number',
Email = 'myinfo.email',
RegisteredAddress = 'myinfo.registered_address',
+ Sex = 'myinfo.sex',
+ Race = 'myinfo.race',
+ Nationality = 'myinfo.nationality',
+ HousingType = 'myinfo.housingtype',
+ HdbType = 'myinfo.hdbtype',
+ BirthCountry = 'myinfo.birth_country',
+ ResidentialStatus = 'myinfo.residentialstatus',
+ VehicleNo = 'myinfo.vehicles',
+ Employment = 'myinfo.name_of_employer',
+ WorkpassStatus = 'myinfo.workpass_status',
+ WorkpassExpiryDate = 'myinfo.workpass_expiry_date',
+ MaritalStatus = 'myinfo.marital_status',
+ // SGID also has another myinfo.mobile_number field that does not contain the country code prefix.
+ // We use the one that contains prefix, as this matches our mobile number form field.
+ MobileNoWithCountryCode = 'myinfo.mobile_number_with_country_code',
+ Dialect = 'myinfo.dialect',
+ Occupation = 'myinfo.occupation',
+ CountryOfMarriage = 'myinfo.country_of_marriage',
+ MarriageCertNo = 'myinfo.marriage_certificate_number',
+ MarriageDate = 'myinfo.marriage_date',
+ DivorceDate = 'myinfo.divorce_date',
}
diff --git a/src/app/modules/sgid/sgid.controller.ts b/src/app/modules/sgid/sgid.controller.ts
index 8223649dae..966671c401 100644
--- a/src/app/modules/sgid/sgid.controller.ts
+++ b/src/app/modules/sgid/sgid.controller.ts
@@ -2,6 +2,7 @@ import { ParamsDictionary } from 'express-serve-static-core'
import { StatusCodes } from 'http-status-codes'
import { FormAuthType } from '../../../../shared/types'
+import { Environment } from '../../../types'
import config from '../../config/config'
import { createLoggerWithLabel } from '../../config/logger'
import { ControllerHandler } from '../core/core.types'
@@ -38,7 +39,15 @@ export const handleLogin: ControllerHandler<
}
const { formId, rememberMe, decodedQuery } = parsedState.value
- const target = decodedQuery ? `/${formId}${decodedQuery}` : `/${formId}`
+
+ // For local dev, we need to specify the frontend app URL as this is different from the backend's app URL
+ const redirectTargetRaw =
+ process.env.NODE_ENV === Environment.Dev
+ ? `${config.app.feAppUrl}/${formId}`
+ : `/${formId}`
+ const target = decodedQuery
+ ? `${redirectTargetRaw}${decodedQuery}`
+ : `${redirectTargetRaw}`
const formResult = await FormService.retrieveFullFormById(formId)
if (formResult.isErr()) {
diff --git a/src/app/modules/sgid/sgid.format.ts b/src/app/modules/sgid/sgid.format.ts
new file mode 100644
index 0000000000..73d93dcc4d
--- /dev/null
+++ b/src/app/modules/sgid/sgid.format.ts
@@ -0,0 +1,43 @@
+import { createLoggerWithLabel } from '../../config/logger'
+
+const logger = createLoggerWithLabel(module)
+
+/**
+ * Formats SGID MyInfo attribute as address.
+ * SGID MyInfo multi-line address are newline-separated, while MyInfo multi-line addresses are comma-separated
+ * @param addr The address to format.
+ * @example '29 ROCHDALE ROAD\n\nSINGAPORE 535842'
+ * @returns Formatted address is comma separated, same as the output of formatAddress in myinfo.format.ts
+ */
+export const formatAddress = (addr: string): string => {
+ const formattedAddress = addr.replace(/(\n)+/g, ', ')
+ return formattedAddress
+}
+
+/**
+ * Formats SGID vehicle types.
+ * SGID vehicles are a stringified array of objects, we want to output this as a string
+ * @param vehicles The vehicles to format.
+ * @example '[{"vehicle_number":"CB6171D"},{"vehicle_number":"SJQ7247B"}]'
+ * @returns Formatted address is comma separated, same as the output of formatAddress in myinfo.format.ts
+ */
+export const formatVehicles = (vehicles: string): string => {
+ if (vehicles && vehicles !== '[]') {
+ try {
+ const vehiclesObject = JSON.parse(vehicles)
+ const vehicleNos = vehiclesObject
+ .map((vehicle: { vehicle_number: string }) => vehicle['vehicle_number'])
+ .join(', ')
+ return vehicleNos
+ } catch (error) {
+ logger.error({
+ message: 'Failed to parse vehicles',
+ meta: { action: 'formatVehicles', vehicles },
+ error,
+ })
+ return ''
+ }
+ } else {
+ return ''
+ }
+}
diff --git a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.service.spec.ts b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.service.spec.ts
index 8f78e7b779..56002eb66e 100644
--- a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.service.spec.ts
+++ b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.service.spec.ts
@@ -1416,7 +1416,7 @@ describe('encrypt-submission.service', () => {
)
})
- it('should return errAsync if lambda returns an errored response (e.g. file not found)', async () => {
+ it('should return errAsync if lambda returns an errored response (e.g. file not found) when a valid file key is used', async () => {
// Arrange
const failurePayload = {
statusCode: 404,
@@ -1438,7 +1438,11 @@ describe('encrypt-submission.service', () => {
// Assert
expect(awsSpy).toHaveBeenCalledOnce()
expect(actualResult.isErr()).toEqual(true)
- expect(actualResult._unsafeUnwrapErr()).toEqual(new InvalidFileKeyError())
+ expect(actualResult._unsafeUnwrapErr()).toEqual(
+ new InvalidFileKeyError(
+ 'Invalid file key - file key is not found in the quarantine bucket. The file must be uploaded first.',
+ ),
+ )
})
it("should return errAsync if the lambda's errored response is not in the right format", async () => {
diff --git a/src/app/routes/api/v3/auth/auth-sgid.routes.ts b/src/app/routes/api/v3/auth/auth-sgid.routes.ts
index bbc1cf0a06..87490cf54a 100644
--- a/src/app/routes/api/v3/auth/auth-sgid.routes.ts
+++ b/src/app/routes/api/v3/auth/auth-sgid.routes.ts
@@ -6,4 +6,38 @@ export const AuthSGIDRouter = Router()
AuthSGIDRouter.get('/authurl', AuthSgidController.generateAuthUrl)
-AuthSGIDRouter.get('/login', AuthSgidController.handleLogin)
+/**
+ * Receives the selected login details from Sgid
+ * Sets the returned profiles in req.session.sgid
+ * @route POST /api/v3/auth/sgid/login/callback
+ *
+ * The frontend should query the available profiles through GET /api/v3/auth/sgid/profiles
+ *
+ * @return 200 with redirect to frontend /login/callback if there are no errors
+ * @return 400 when code or state is not provided, or state is incorrect
+ * @return 500 when processing the code verifier cookie fails, or when an unknown error occurs
+ */
+AuthSGIDRouter.get('/login/callback', AuthSgidController.handleLoginCallback)
+
+/**
+ * Sets the selected user profile
+ * Uses get request to retrieve available profiles
+ * @route GET /api/v3/auth/sgid/profiles
+ *
+ * @return 200 with list of profiles
+ * @return 400 when session or profile is invalid
+ * @return 401 when session has expired
+ */
+AuthSGIDRouter.get('/profiles', AuthSgidController.getProfiles)
+
+/**
+ * Sets the selected user profile
+ * Uses post request to select the workemail from the request body
+ * @route POST /api/v3/auth/sgid/profiles
+ *
+ * @return 200 when OTP has been been successfully sent
+ * @return 400 when session, profile, or workEmail is invalid
+ * @return 401 when email domain is invalid
+ * @return 500 when unknown errors occurs during email validation, or creating the new account
+ */
+AuthSGIDRouter.post('/profiles', AuthSgidController.setProfile)
diff --git a/src/app/utils/urls.ts b/src/app/utils/urls.ts
new file mode 100644
index 0000000000..99a807f414
--- /dev/null
+++ b/src/app/utils/urls.ts
@@ -0,0 +1,11 @@
+import { Environment } from '../../types'
+import config from '../config/config'
+
+export const resolveRedirectionUrl = (rootUrl: string) => {
+ // For local dev, we need to specify the frontend app URL as this is different from the backend's app URL
+ const hostname =
+ process.env.NODE_ENV === Environment.Dev ? `${config.app.feAppUrl}` : ``
+
+ const resolvedUrl = `${hostname}${rootUrl}`
+ return resolvedUrl
+}
diff --git a/src/types/vendor/express.d.ts b/src/types/vendor/express.d.ts
index 9b00e06e8e..779ced97d9 100644
--- a/src/types/vendor/express.d.ts
+++ b/src/types/vendor/express.d.ts
@@ -1,5 +1,6 @@
import { RateLimitInfo } from 'express-rate-limit'
+import { SgidUser } from '../../app/modules/auth/auth.types'
import { EncryptSubmissionDto } from '../api'
import { IPopulatedEncryptedForm, IPopulatedForm, IUserSchema } from '../types'
@@ -37,6 +38,7 @@ declare module 'express-session' {
user?: {
_id: IUserSchema['_id']
}
+ sgid?: SgidUser
}
export interface AuthedSessionData extends SessionData {