Skip to content

Commit 2d9ff2d

Browse files
authored
[CA-1988] Add second factor removal (#121)
1 parent 24a91b2 commit 2d9ff2d

File tree

3 files changed

+278
-140
lines changed

3 files changed

+278
-140
lines changed

src/widgets/mfa/MfaCredentialsWidget.jsx

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,25 @@ import {Info, Intro, Separator} from '../../components/miscComponent';
77
import {createForm} from '../../components/form/formComponent';
88
import {deepDefaults} from '../../helpers/deepDefaults';
99
import phoneNumberField from '../../components/form/fields/phoneNumberField';
10+
import {withTheme} from '../../components/widget/widgetContext';
11+
import styled from 'styled-components';
1012

1113

1214
const EmailRegisteringCredentialForm = createForm({
1315
prefix: 'r5-mfa-credentials-email-',
1416
submitLabel: 'mfa.register.email'
1517
});
1618

19+
const EmailCredentialRemovalForm = createForm({
20+
prefix: 'r5-mfa-credentials-email-removal-',
21+
submitLabel: 'mfa.remove.email'
22+
})
23+
24+
const PhoneNumberCredentialRemovalForm = createForm({
25+
prefix: 'r5-mfa-credentials-phone-number-removal-',
26+
submitLabel: 'mfa.remove.phoneNumber'
27+
})
28+
1729
const VerificationCodeForm = createForm({
1830
prefix: 'r5-mfa-credentials-verification-code-',
1931
fields: [
@@ -33,6 +45,11 @@ const PhoneNumberRegisteringCredentialForm = config => createForm({
3345
submitLabel: 'mfa.register.phoneNumber'
3446
})
3547

48+
const DivCredentialBlock = withTheme(styled.div`
49+
margin-left: ${props => props.theme.get('_blockInnerHeight')}px;
50+
margin-bottom: 5em;
51+
`);
52+
3653
class MainView extends React.Component {
3754
onEmailRegistering = _ => {
3855
return this.props.apiClient.startMfaEmailRegistration({
@@ -48,21 +65,64 @@ class MainView extends React.Component {
4865
})
4966
}
5067

68+
onEmailRemoval = _ => {
69+
return this.props.apiClient.removeMfaEmail({
70+
accessToken: this.props.accessToken
71+
})
72+
}
73+
74+
onPhoneNumberRemoval = data => {
75+
return this.props.apiClient.removeMfaPhoneNumber({
76+
accessToken: this.props.accessToken,
77+
phoneNumber: data.phoneNumber
78+
})
79+
}
80+
5181
render() {
52-
const { i18n, showIntro, config } = this.props
82+
const { i18n, showIntro, config, showRemoveMfaCredentials, credentials } = this.props
5383
const PhoneNumberInputForm = PhoneNumberRegisteringCredentialForm(config);
84+
const phoneNumberCredentialRegistered = credentials.find(credential => credential.type === 'sms')
85+
const isEmailCredentialRegistered = credentials.some(credential => credential.type === 'email')
5486
return (
5587
<div>
56-
<div>
57-
{config.mfaEmailEnabled && showIntro && <Intro>{i18n('mfa.email.explain')}</Intro>}
58-
{config.mfaEmailEnabled && <EmailRegisteringCredentialForm handler={this.onEmailRegistering} onSuccess={data => this.props.goTo('verification-code', {...data, registrationType: 'email'})}/>}
59-
</div>
60-
{config.mfaEmailEnabled && config.mfaSmsEnabled && <Separator/>}
61-
<div>
62-
{config.mfaSmsEnabled && <Intro>{showIntro && <Intro>{i18n('mfa.phoneNumber.explain')}</Intro>}</Intro>}
63-
{config.mfaSmsEnabled && <PhoneNumberInputForm
64-
handler={this.onPhoneNumberRegistering} onSuccess={data => this.props.goTo('verification-code', {...data, registrationType: 'sms'})}/> }
65-
</div>
88+
<DivCredentialBlock>
89+
{config.mfaEmailEnabled &&
90+
<div>
91+
{showIntro && <Intro>{i18n('mfa.email.explain')}</Intro>}
92+
<EmailRegisteringCredentialForm handler={this.onEmailRegistering} onSuccess={data => this.props.goTo('verification-code', {...data, registrationType: 'email'})}/>
93+
</div>
94+
}
95+
96+
{config.mfaEmailEnabled && config.mfaSmsEnabled && <Separator/>}
97+
{config.mfaSmsEnabled &&
98+
<div>
99+
{showIntro && <Intro>{i18n('mfa.phoneNumber.explain')}</Intro>}
100+
<PhoneNumberInputForm
101+
handler={this.onPhoneNumberRegistering} onSuccess={data => this.props.goTo('verification-code', {...data, registrationType: 'sms'})}/>
102+
</div>
103+
}
104+
</DivCredentialBlock>
105+
<DivCredentialBlock>
106+
{showRemoveMfaCredentials &&
107+
config.mfaEmailEnabled &&
108+
isEmailCredentialRegistered &&
109+
<div>
110+
{showIntro && <Intro>{i18n('mfa.email.remove.explain')}</Intro>}
111+
<EmailCredentialRemovalForm handler={this.onEmailRemoval}
112+
onSuccess={_ => this.props.goTo('credential-removed', {credentialType: 'email'})}/>
113+
</div>
114+
}
115+
{showRemoveMfaCredentials && config.mfaEmailEnabled && config.mfaSmsEnabled && phoneNumberCredentialRegistered && isEmailCredentialRegistered && <Separator/>}
116+
{showRemoveMfaCredentials &&
117+
config.mfaSmsEnabled &&
118+
phoneNumberCredentialRegistered &&
119+
<div>
120+
{showIntro && <Intro>{i18n('mfa.phoneNumber.remove.explain')}</Intro>}
121+
<PhoneNumberCredentialRemovalForm handler={data => this.onPhoneNumberRemoval({...data, ...phoneNumberCredentialRegistered})}
122+
onSuccess={_ => this.props.goTo('credential-removed', {credentialType: 'sms'})}/>
123+
</div>
124+
}
125+
</DivCredentialBlock>
66126
</div>
67127
)
68128
}
@@ -102,17 +162,27 @@ const CredentialRegisteredView = ({ i18n, registrationType}) => <div>
102162
{registrationType === 'sms' && <Info>{i18n('mfa.phoneNumber.registered')}</Info>}
103163
</div>
104164

165+
const CredentialRemovedView = ({ i18n, credentialType}) => <div>
166+
{credentialType === 'email' && <Info>{i18n('mfa.email.removed')}</Info>}
167+
{credentialType === 'sms' && <Info>{i18n('mfa.phoneNumber.removed')}</Info>}
168+
</div>
169+
105170
export default createMultiViewWidget({
106171
initialView: 'main',
107172
views: {
108173
'main': MainView,
109174
'credential-registered': CredentialRegisteredView,
110-
'verification-code': VerificationCodeView
175+
'verification-code': VerificationCodeView,
176+
'credential-removed': CredentialRemovedView
111177
},
112-
prepare: (options) => {
113-
return deepDefaults({
114-
showIntro: true,
115-
...options
116-
}
117-
)}
178+
prepare: (options, { apiClient }) => {
179+
return apiClient.listMfaCredentials(options.accessToken).then(credentials => {
180+
return deepDefaults({
181+
showIntro: true,
182+
showRemoveMfaCredentials: true,
183+
...options,
184+
...credentials
185+
})
186+
})
187+
}
118188
})

tests/widgets/mfa/MfaCredentialsWidget.test.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,42 @@ const defaultConfig = { domain: 'local.reach5.net', mfaEmailEnabled: true, mfaSm
1010
const textFilter = expected => (i, el) => $(el).text() === expected;
1111

1212
describe('Snapshot', () => {
13-
const generateSnapshot = (options, config = defaultConfig) => () => {
14-
const tree = MfaCredentialsWidget(options, { config, apiClient: {} })
13+
const generateSnapshot = ({options = {showIntro: true}, config = defaultConfig, credentials }) => () => {
14+
const apiClient = {
15+
listMfaCredentials: jest.fn().mockReturnValueOnce(Promise.resolve({ credentials }))
16+
}
17+
const tree = MfaCredentialsWidget(options, {config, apiClient} )
1518
.then(result => renderer.create(result).toJSON());
1619

1720
expect(tree).resolves.toMatchSnapshot();
1821
};
1922

2023
describe('mfaCredentials', () => {
21-
test('default', generateSnapshot());
24+
test('default', generateSnapshot({ credentials: []}));
2225

23-
test('no intro', generateSnapshot({ showIntro: false }));
26+
test('no intro', generateSnapshot({ options: {showIntro: false}, credentials: [
27+
{ type: 'sms', phoneNumber: '33612345678', friendlyName: 'identifier', createdAt: '2022-09-21' },
28+
{ type: 'email', email: '[email protected]', friendlyName: 'identifier', createdAt: '2022-09-21' }
29+
]}));
2430
});
2531
});
2632
describe('DOM testing', () => {
27-
const generateComponent = async (options, config = defaultConfig) => {
28-
const result = await MfaCredentialsWidget(options, { config, apiClient: {} });
33+
const generateComponent = async (options, config = defaultConfig, credentials) => {
34+
35+
const apiClient = {
36+
listMfaCredentials: jest.fn().mockReturnValueOnce(Promise.resolve({ credentials }))
37+
}
38+
const result = await MfaCredentialsWidget(options, { config, apiClient });
2939

3040
return render(result);
3141
};
3242

3343
describe('mfaCredentials', () => {
3444
test('default', async () => {
35-
const instance = await generateComponent({});
45+
const instance = await generateComponent({showIntro: true, showRemoveMfaCredentials: true}, defaultConfig, [
46+
{ type: 'sms', phoneNumber: '33612345678', friendlyName: 'identifier', createdAt: '2022-09-21' },
47+
{ type: 'email', email: '[email protected]', friendlyName: 'identifier', createdAt: '2022-09-21' }
48+
]);
3649
// Intro
3750
expect(
3851
instance.find('div').filter(textFilter('mfa.email.explain'))
@@ -43,10 +56,20 @@ describe('DOM testing', () => {
4356
instance.find('button').filter(textFilter('mfa.register.phoneNumber'))
4457
).toHaveLength(1);
4558

46-
// Form button email
59+
// // Form button email
4760
expect(
4861
instance.find('button').filter(textFilter('mfa.register.email'))
4962
).toHaveLength(1);
63+
64+
// // Form button remove email
65+
expect(
66+
instance.find('button').filter(textFilter('mfa.remove.email'))
67+
)
68+
69+
// // Form button remove phone number
70+
expect(
71+
instance.find('button').filter(textFilter('mfa.remove.phoneNumber'))
72+
)
5073
});
5174
});
5275
});

0 commit comments

Comments
 (0)