Skip to content

Commit

Permalink
Totp enrollment frontend (#2657)
Browse files Browse the repository at this point in the history
* Totp enrollment box component

* totp enabled field in profile controller response

* Profile totp enrollment frontend procedure

* fix mispell

* Confirmation modal on totp disable

* Addressing review feedbacks
  • Loading branch information
CDimonaco authored Jun 3, 2024
1 parent 3ab6152 commit cbffa6d
Show file tree
Hide file tree
Showing 14 changed files with 801 additions and 14 deletions.
7 changes: 7 additions & 0 deletions assets/js/lib/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ export const deleteUser = (userID) => del(`/users/${userID}`);
export const getUserProfile = () => get('/profile');

export const editUserProfile = (payload) => patch('/profile', payload);

export const initiateTotpEnrolling = () => get('/profile/totp_enrollment');

export const resetTotpEnrolling = () => del('/profile/totp_enrollment');

export const confirmTotpEnrolling = (payload) =>
post('/profile/totp_enrollment', payload);
1 change: 1 addition & 0 deletions assets/js/lib/test-utils/factories/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const profileFactory = Factory.define(() => ({
email: faker.internet.email(),
abilities: abilityFactory.buildList(2),
password_change_requested: false,
totp_enabled: faker.datatype.boolean(),
created_at: formatISO(faker.date.past()),
updated_at: formatISO(faker.date.past()),
}));
Expand Down
77 changes: 77 additions & 0 deletions assets/js/pages/Profile/ProfileForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,37 @@ import Button from '@common/Button';
import Input from '@common/Input';
import Label from '@common/Label';
import Modal from '@common/Modal';
import Switch from '@common/Switch';
import MultiSelect from '@common/MultiSelect';
import ProfilePasswordChangeForm from '@pages/Profile/ProfilePasswordChangeForm';
import TotpEnrollementBox from '@pages/Profile/TotpEnrollmentBox';

import { REQUIRED_FIELD_TEXT, errorMessage, mapAbilities } from '@lib/forms';

function ProfileForm({
fullName = '',
emailAddress = '',
username = '',
totpEnabled = false,
totpSecret = '',
totpQrData = '',
abilities = [],
errors,
loading,
disableForm,
passwordModalOpen = false,
totpBoxOpen = false,
togglePasswordModal = noop,
onSave = noop,
onResetTotp = noop,
onVerifyTotp = noop,
onEnableTotp = noop,
}) {
const [fullNameState, setFullName] = useState(fullName);
const [fullNameErrorState, setFullNameError] = useState(null);
const [emailAddressState, setEmailAddress] = useState(emailAddress);
const [emailAddressErrorState, setEmailAddressError] = useState(null);
const [totpDisableModalOpen, setTotpDisableModalOpen] = useState(false);

const validateRequired = () => {
let error = false;
Expand Down Expand Up @@ -54,6 +65,14 @@ function ProfileForm({
onSave(user);
};

const toggleTotp = () => {
if (!totpEnabled) {
onEnableTotp();
return;
}
setTotpDisableModalOpen(true);
};

useEffect(() => {
setFullNameError(getError('fullname', errors));
setEmailAddressError(getError('email', errors));
Expand Down Expand Up @@ -105,6 +124,32 @@ function ProfileForm({
Change Password
</Button>
</div>
{totpBoxOpen ? (
<div className="col-start-1 col-span-5">
<h2 className="font-bold text-xl"> Configure TOTP </h2>
<TotpEnrollementBox
errors={errors}
qrData={totpQrData}
secret={totpSecret}
loading={loading}
verifyTotp={onVerifyTotp}
/>
</div>
) : (
<>
<Label
className="col-start-1 col-span-1"
info="Setup a multi factor TOTP authentication besides your password to increase security
for your account."
>
Authenticator App
</Label>
<div className="col-start-2 col-span-3">
<Switch selected={totpEnabled} onChange={toggleTotp} />
</div>
</>
)}

<Label className="col-start-1 col-span-1">Permissions</Label>
<div className="col-start-2 col-span-3">
<MultiSelect
Expand All @@ -126,6 +171,38 @@ function ProfileForm({
</Button>
</div>
</div>
<Modal
title="Disable TOTP"
className="!w-3/4 !max-w-3xl"
open={totpDisableModalOpen}
onClose={() => setTotpDisableModalOpen((opened) => !opened)}
>
<div className="flex flex-col my-2">
<span className="font-semibold">
Are you sure you want to disable TOTP?{' '}
</span>
<div className="w-1/6 h-4/5 flex mt-4">
<Button
type="danger-bold"
className="mr-2"
onClick={() => {
onResetTotp();
setTotpDisableModalOpen(false);
}}
disabled={loading}
>
Disable
</Button>
<Button
type="primary-white"
onClick={() => setTotpDisableModalOpen(false)}
className=""
>
Cancel
</Button>
</div>
</div>
</Modal>
<Modal
title="Change Password"
className="!w-3/4 !max-w-3xl"
Expand Down
39 changes: 35 additions & 4 deletions assets/js/pages/Profile/ProfileForm.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,30 @@ export default {
action: 'Save user',
description: 'Save user action',
},
},
args: {
username,
abilities,
totpEnabled: {
description: 'User TOTP enabled',
control: {
type: 'boolean',
},
},
totpSecret: {
description: 'User TOTP secret',
control: {
type: 'text',
},
},
totpQrData: {
description: 'User TOTP secret encoded as qr',
control: {
type: 'text',
},
},
totpBoxOpen: {
description: 'Show TOTP enrollment box',
control: {
type: 'text',
},
},
},
render: (args) => (
<ContainerWrapper>
Expand All @@ -66,6 +86,9 @@ export const Default = {
args: {
username,
abilities,
totpSecret: 'HKJDFHJKHDIU379847HJKDJKH',
totpQrData:
'otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example',
},
};

Expand All @@ -81,6 +104,14 @@ export const Loading = {
},
};

export const WithTotpEnrollmentBoxEnabled = {
args: {
...Default.args,
totpEnabled: true,
totpBoxOpen: true,
},
};

export const WithErrors = {
args: {
...Default.args,
Expand Down
Loading

0 comments on commit cbffa6d

Please sign in to comment.