Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(admin-form): support nric masking #7388

Merged
merged 37 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1b64eaf
feat(admin-form): add frontend nric mask toggle to singpass auth sett…
kevin9foong Jun 11, 2024
62006d3
fix(admin-form): fix spacing and change units from px to rem
kevin9foong Jun 11, 2024
34cf651
feat(admin-form): create isNricMaskingEnabled field in database when …
kevin9foong Jun 13, 2024
8beeb81
feat(admin-form): enable fetching and updating isNricMaskingEnabled f…
kevin9foong Jun 13, 2024
a9f55da
feat(admin-form): connect frontend component with server api routes
kevin9foong Jun 13, 2024
4ff31f9
fix(admin-form): make code terser, remove unused params, fix nullish-…
kevin9foong Jun 14, 2024
0452007
fix(admin-page): fix flickering bug
kevin9foong Jun 14, 2024
21492ee
feat(duplicate-api): implement duplication of isNricMaskEnabled field
kevin9foong Jun 14, 2024
4c10795
feat(public-form): prevent nric from reaching frontend if masked
kevin9foong Jun 16, 2024
975b31f
feat(nric-mask): apply masking when submitting forms
kevin9foong Jun 16, 2024
1a00b76
feat(nric-mask): update frontend template to reflect masking toggle c…
kevin9foong Jun 16, 2024
2c27e7d
fix(index.html): checkout to develop branch
kevin9foong Jun 16, 2024
b3170bf
fix: remove console.log
kevin9foong Jun 16, 2024
0669869
fix(toggle-component): replace nullish coalesce with ternary operator
kevin9foong Jun 16, 2024
9724ac1
chore(public-form-controller): remove masking when fetching public fo…
kevin9foong Jun 18, 2024
7dffd40
fix: refactor email-submission-controller to mask at single location
kevin9foong Jun 18, 2024
16a494f
fix: clean up nric-mask util function and unused imports
kevin9foong Jun 18, 2024
00db110
fix: update field with evaluated map value
kevin9foong Jun 18, 2024
444dc40
feat: add nric masking to the frontend public form provider
kevin9foong Jun 18, 2024
8af1ce7
feat: remove tooltip and labelComponentRight from Toggle to match design
kevin9foong Jun 19, 2024
700897b
feat: update settings auth page to reflect design master
kevin9foong Jun 19, 2024
2f164f7
feat: add err message
kevin9foong Jun 19, 2024
65ee4bc
feat: change px to rem
kevin9foong Jun 20, 2024
5e38e86
feat: disable independently for nric masking and auth toggle
kevin9foong Jun 20, 2024
14b1be1
fix: change MyInfo to Myinfo in description message
kevin9foong Jun 20, 2024
6c3deeb
fix: update form model test suite to expect new isNricMaskEnabled field
kevin9foong Jun 21, 2024
c065b81
feat: add tests for nric mask util and masking for email submission
kevin9foong Jun 21, 2024
5f45d03
feat: add test for isNricMaskDisabled for email submission
kevin9foong Jun 21, 2024
9829c1c
feat: add tests for storage mode submission masking
kevin9foong Jun 21, 2024
d8acc82
fix: fix bug with submission not being able to save authType and defa…
kevin9foong Jun 21, 2024
81c5177
feat: add stories for updated settings auth page
kevin9foong Jun 24, 2024
4d7be78
feat: update playwright test helper to support new auth setting flow
kevin9foong Jun 24, 2024
8460f1e
fix: fix review comments and run frontend lint fix
kevin9foong Jun 24, 2024
ac36e6e
fix: fix based on chromatic review
kevin9foong Jun 25, 2024
a08fc76
fix: fix review comments
kevin9foong Jun 26, 2024
0e3abd2
feat: add testing for getDuplicateParams to ensure that nricmask is c…
kevin9foong Jun 26, 2024
b0b606e
refactor: extract skeleton component out to own file
KenLSM Jun 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions __tests__/e2e/helpers/createForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,20 +291,30 @@ const addAuthSettings = async (
await page.getByRole('tab', { name: 'Singpass' }).click()

// Ensure that we are on the auth page
await expect(
page.getByRole('heading', { name: 'Enable Singpass authentication' }),
).toBeVisible()
await expect(page.getByRole('heading', { name: 'Singpass' })).toBeVisible()

await page
.locator('label', {
has: page.locator(
`input[type='radio'][value='${formSettings.authType}']`,
),
has: page.locator('[aria-label="Enable Singpass authentication"]'),
})
.first() // Since 'Singpass' will match all radio options, pick the first matching one.
.click({ position: { x: 1, y: 1 } }) // Clicking the center of the sgid button launches the sgid contact form, put this here until we get rid of the link
.click()

await expectToast(page, /form authentication successfully updated/i)
await expectToast(page, /singpass authentication successfully enabled/i)

// Don't need to click if SGID is desired auth type
// since SGID is the default once Singpass is enabled
if (formSettings.authType !== FormAuthType.SGID) {
await page
.locator('label', {
has: page.locator(
`input[type='radio'][value='${formSettings.authType}']`,
),
})
.first() // Since 'Singpass' will match all radio options, pick the first matching one.
.click({ position: { x: 1, y: 1 } }) // Clicking the center of the sgid button launches the sgid contact form, put this here until we get rid of the link

await expectToast(page, /singpass authentication successfully updated/i)
}

switch (formSettings.authType) {
case FormAuthType.SP:
Expand Down
17 changes: 14 additions & 3 deletions frontend/src/features/admin-form/common/AdminViewFormService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import {
filterHiddenInputs,
} from '~features/public-form/utils'

import { PREVIEW_MOCK_UINFIN } from '../preview/constants'
import {
PREVIEW_MASKED_MOCK_UINFIN,
PREVIEW_MOCK_UINFIN,
} from '../preview/constants'

// endpoint exported for testing
export const ADMIN_FORM_ENDPOINT = '/admin/forms'
Expand Down Expand Up @@ -58,7 +61,11 @@ export const previewForm = async (
// Add default mock authenticated state if previewing an authenticatable form
// and if server has not already sent back a mock authenticated state.
if (data.form.authType !== FormAuthType.NIL && !data.spcpSession) {
data.spcpSession = { userName: PREVIEW_MOCK_UINFIN }
data.spcpSession = {
userName: data.form.isNricMaskEnabled
? PREVIEW_MASKED_MOCK_UINFIN
: PREVIEW_MOCK_UINFIN,
}
}

// Inject MyInfo preview values into form fields (if they are MyInfo fields).
Expand Down Expand Up @@ -87,7 +94,11 @@ export const viewFormTemplate = async (
// Add default mock authenticated state if previewing an authenticatable form
// and if server has not already sent back a mock authenticated state.
if (data.form.authType !== FormAuthType.NIL && !data.spcpSession) {
data.spcpSession = { userName: PREVIEW_MOCK_UINFIN }
data.spcpSession = {
userName: data.form.isNricMaskEnabled
? PREVIEW_MASKED_MOCK_UINFIN
: PREVIEW_MOCK_UINFIN,
}
}

// Inject MyInfo preview values into form fields (if they are MyInfo fields).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { FormAuthType, FormLogoState, FormStartPage } from '~shared/types'

import { useIsMobile } from '~hooks/useIsMobile'

import { PREVIEW_MOCK_UINFIN } from '~features/admin-form/preview/constants'
import {
PREVIEW_MASKED_MOCK_UINFIN as PREVIEW_MASKED_MOCK_UINFIN,
PREVIEW_MOCK_UINFIN,
} from '~features/admin-form/preview/constants'
import { useEnv } from '~features/env/queries'
import { FormInstructions } from '~features/public-form/components/FormInstructions/FormInstructions'
import {
Expand Down Expand Up @@ -197,7 +200,9 @@ export const StartPageView = () => {
showHeader
loggedInId={
form && form.authType !== FormAuthType.NIL
? PREVIEW_MOCK_UINFIN
? form.isNricMaskEnabled
? PREVIEW_MASKED_MOCK_UINFIN
: PREVIEW_MOCK_UINFIN
: undefined
}
{...formHeaderProps}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { PaymentsThankYouSvgr } from '~components/FormEndPage/PaymentsThankYouSv
import { ThankYouSvgr } from '~components/FormEndPage/ThankYouSvgr'

import { useAdminForm } from '~features/admin-form/common/queries'
import { PREVIEW_MOCK_UINFIN } from '~features/admin-form/preview/constants'
import {
PREVIEW_MASKED_MOCK_UINFIN,
PREVIEW_MOCK_UINFIN,
} from '~features/admin-form/preview/constants'
import { useEnv } from '~features/env/queries'
import {
FormBannerLogo,
Expand Down Expand Up @@ -90,7 +93,9 @@ export const EndPageContent = (): JSX.Element => {
onLogout={undefined}
loggedInId={
form && form.authType !== FormAuthType.NIL
? PREVIEW_MOCK_UINFIN
? form.isNricMaskEnabled
? PREVIEW_MASKED_MOCK_UINFIN
: PREVIEW_MOCK_UINFIN
: undefined
}
/>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/features/admin-form/preview/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const PREVIEW_MOCK_UINFIN = 'S1234567A'
export const PREVIEW_MASKED_MOCK_UINFIN = '*****567A'
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,42 @@ export default {
} as Meta

const Template: Story = () => <SettingsAuthPage />
export const PrivateEmailForm = Template.bind({})
PrivateEmailForm.parameters = {
export const PrivateEmailNilAuthForm = Template.bind({})
PrivateEmailNilAuthForm.parameters = {
msw: buildMswRoutes({ status: FormStatus.Private }),
}

export const PrivateStorageForm = Template.bind({})
PrivateStorageForm.parameters = {
export const PrivateStorageNilAuthForm = Template.bind({})
PrivateStorageNilAuthForm.parameters = {
msw: buildMswRoutes({
responseMode: FormResponseMode.Encrypt,
status: FormStatus.Private,
}),
}

export const PublicEmailSingpassForm = Template.bind({})
PublicEmailSingpassForm.parameters = {
export const PublicEmailNilAuthForm = Template.bind({})
PublicEmailNilAuthForm.parameters = {
msw: buildMswRoutes({
responseMode: FormResponseMode.Email,
status: FormStatus.Public,
authType: FormAuthType.SP,
esrvcId: 'STORYBOOK-TEST',
}),
}

export const PublicStorageNilAuthForm = Template.bind({})
PublicStorageNilAuthForm.parameters = {
msw: buildMswRoutes({
responseMode: FormResponseMode.Encrypt,
status: FormStatus.Public,
}),
}

// purpose: tests that isNricMaskEnabled should not affect setting options available
export const PublicStorageNilAuthFormNricMaskingEnabled = Template.bind({})
PublicStorageNilAuthFormNricMaskingEnabled.parameters = {
msw: buildMswRoutes({
responseMode: FormResponseMode.Encrypt,
status: FormStatus.Public,
isNricMaskEnabled: true,
}),
}

Expand All @@ -63,9 +80,31 @@ PrivateStorageCorppassForm.parameters = {
status: FormStatus.Private,
authType: FormAuthType.CP,
responseMode: FormResponseMode.Encrypt,
esrvcId: 'STORYBOOK-TEST',
}),
}

export const PublicEmailSingpassForm = Template.bind({})
PublicEmailSingpassForm.parameters = {
msw: buildMswRoutes({
status: FormStatus.Public,
authType: FormAuthType.SP,
esrvcId: 'STORYBOOK-TEST',
}),
}

export const PrivateEmailMyInfoWithoutMyInfoFieldsForm = Template.bind({})
PrivateEmailMyInfoWithoutMyInfoFieldsForm.parameters = {
msw: [
...buildMswRoutes({
status: FormStatus.Private,
authType: FormAuthType.MyInfo,
esrvcId: 'STORYBOOK-TEST',
}),
...createFormBuilderMocks({ form_fields: [] }),
],
}

export const PrivateEmailMyinfoForm = Template.bind({})
PrivateEmailMyinfoForm.parameters = {
msw: [
Expand All @@ -78,6 +117,39 @@ PrivateEmailMyinfoForm.parameters = {
],
}

export const PublicEmailMyInfoForm = Template.bind({})
PublicEmailMyInfoForm.parameters = {
msw: [
...buildMswRoutes({
status: FormStatus.Public,
authType: FormAuthType.MyInfo,
esrvcId: 'STORYBOOK-TEST',
}),
...createFormBuilderMocks({ form_fields: MOCK_FORM_FIELDS_WITH_MYINFO }),
],
}

export const PrivateEmailSingpassFormNricMaskingEnabled = Template.bind({})
PrivateEmailSingpassFormNricMaskingEnabled.parameters = {
msw: buildMswRoutes({
status: FormStatus.Private,
authType: FormAuthType.SGID,
isNricMaskEnabled: true,
}),
}
export const PrivateEmailMyInfoFormNricMaskingEnabled = Template.bind({})
PrivateEmailMyInfoFormNricMaskingEnabled.parameters = {
msw: [
...buildMswRoutes({
status: FormStatus.Private,
authType: FormAuthType.MyInfo,
esrvcId: 'STORYBOOK-TEST',
isNricMaskEnabled: true,
}),
...createFormBuilderMocks({ form_fields: MOCK_FORM_FIELDS_WITH_MYINFO }),
],
}

export const Loading = Template.bind({})
Loading.parameters = {
msw: [getAdminFormSettings({ delay: 'infinite' })],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const SettingsAuthPage = (): JSX.Element => {

return (
<>
<CategoryHeader>Enable Singpass authentication</CategoryHeader>
<CategoryHeader>Singpass</CategoryHeader>
<AuthSettingsSection />
</>
)
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/features/admin-form/settings/SettingsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ export const updateFormAuthType: UpdateFormFn<'authType'> = async (
return updateFormSettings(formId, { authType: newAuthType })
}

export const updateFormNricMask: UpdateFormFn<'isNricMaskEnabled'> = async (
formId,
newIsNricMaskEnabled,
) => {
return updateFormSettings(formId, {
isNricMaskEnabled: newIsNricMaskEnabled,
})
}

export const updateFormEsrvcId: UpdateFormFn<'esrvcId'> = async (
formId,
newEsrvcId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Text } from '@chakra-ui/react'

import { GUIDE_SPCP_ESRVCID } from '~constants/links'
import Link from '~components/Link'

export const AuthSettingsDescriptionText = () => {
return (
<Text textStyle="subhead-1" color="secondary.500" mb="2.5rem" mt="2.5rem">
Authenticate respondents by NRIC/FIN.{' '}
<Link
textStyle="subhead-1"
href={GUIDE_SPCP_ESRVCID}
isExternal
// Needed for link to open since there are nested onClicks
onClickCapture={(e) => e.stopPropagation()}
>
Learn more about Singpass authentication
</Link>
</Text>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Box } from '@chakra-ui/react'

import InlineMessage from '~components/InlineMessage'

interface AuthSettingsDisabledExplanationTextProps {
isFormPublic: boolean
containsMyInfoFields: boolean
}

const CONTAINS_MYINFO_FIELDS_DISABLED_EXPLANATION_TEXT =
'To change Singpass authentication settings, close your form to new responses and remove all existing Myinfo fields.'
const FORM_IS_PUBLIC_DISABLED_EXPLANATION_TEXT =
'To change Singpass authentication settings, close your form to new responses.'

export const AuthSettingsDisabledExplanationText = ({
isFormPublic,
containsMyInfoFields,
}: AuthSettingsDisabledExplanationTextProps) => {
const explanationText = containsMyInfoFields
? CONTAINS_MYINFO_FIELDS_DISABLED_EXPLANATION_TEXT
: isFormPublic
? FORM_IS_PUBLIC_DISABLED_EXPLANATION_TEXT
: null

if (!explanationText) {
return null
}
return (
<Box my="2.5rem">
<InlineMessage mb="1rem">{explanationText}</InlineMessage>
</Box>
)
}
Loading
Loading