Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Box, FieldGroup, TextInput, Field, FieldLabel, FieldRow, FieldError, Button } from '@rocket.chat/fuselage';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { Box, Button } from '@rocket.chat/fuselage';
import { FieldGroup, TextInput, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage-forms';
import { GenericModal } from '@rocket.chat/ui-client';
import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
import type { ReactElement, ChangeEvent, SyntheticEvent } from 'react';
import { useId, useState } from 'react';
import type { ReactElement } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import type { OnConfirm } from './TwoFactorModal';
Expand All @@ -13,14 +13,25 @@ type TwoFactorEmailModalProps = {
onConfirm: OnConfirm;
onClose: () => void;
emailOrUsername: string;
invalidAttempt?: boolean;
};

const TwoFactorEmailModal = ({ onConfirm, onClose, emailOrUsername, invalidAttempt }: TwoFactorEmailModalProps): ReactElement => {
type TwoFactorEmailFormData = {
code: string;
};

const TwoFactorEmailModal = ({ onConfirm, onClose, emailOrUsername }: TwoFactorEmailModalProps): ReactElement => {
const dispatchToastMessage = useToastMessageDispatch();
const { t } = useTranslation();
const [code, setCode] = useState<string>('');
const ref = useAutoFocus<HTMLInputElement>();

const {
control,
handleSubmit,
setError,
setValue,
formState: { errors, isSubmitting },
} = useForm<TwoFactorEmailFormData>({
defaultValues: { code: '' },
});

const sendEmailCode = useEndpoint('POST', '/v1/users.2fa.sendEmailCode');

Expand All @@ -36,46 +47,51 @@ const TwoFactorEmailModal = ({ onConfirm, onClose, emailOrUsername, invalidAttem
}
};

const onConfirmEmailCode = (e: SyntheticEvent): void => {
e.preventDefault();
onConfirm(code, Method.EMAIL);
};

const onChange = ({ currentTarget }: ChangeEvent<HTMLInputElement>): void => {
setCode(currentTarget.value);
};

const id = useId();
const onSubmit = handleSubmit(async ({ code }) => {
try {
await onConfirm(code, Method.EMAIL);
} catch (error) {
setError('code', {
type: 'manual',
message: t('Invalid_two_factor_code'),
});
setValue('code', '');
}
});

return (
<GenericModal
wrapperFunction={(props) => <Box is='form' onSubmit={onConfirmEmailCode} {...props} />}
wrapperFunction={(props) => <Box is='form' onSubmit={onSubmit} {...props} />}
onCancel={onClose}
confirmText={t('Verify')}
title={t('Enter_authentication_code')}
onClose={onClose}
variant='warning'
confirmDisabled={!code}
confirmDisabled={isSubmitting}
tagline={t('Email_two-factor_authentication')}
icon={null}
>
<FieldGroup>
<Field>
<FieldLabel alignSelf='stretch' htmlFor={id}>
{t('Enter_the_code_we_just_emailed_you')}
</FieldLabel>
<FieldLabel alignSelf='stretch'>{t('Enter_the_code_we_just_emailed_you')}</FieldLabel>
<FieldRow>
<TextInput
id={id}
ref={ref}
value={code}
onChange={onChange}
placeholder={t('Enter_code_here')}
autoComplete='one-time-code'
inputMode='numeric'
<Controller
name='code'
control={control}
rules={{ required: t('Required_field', { field: t('Code') }) }}
render={({ field }) => (
<TextInput
{...field}
placeholder={t('Enter_code_here')}
autoComplete='one-time-code'
inputMode='numeric'
disabled={isSubmitting}
error={errors.code?.message}
/>
)}
/>
</FieldRow>
{invalidAttempt && <FieldError>{t('Invalid_password')}</FieldError>}
{errors.code && <FieldError>{errors.code.message}</FieldError>}
</Field>
</FieldGroup>
<Button display='flex' justifyContent='end' onClick={onClickResendCode} small mbs={24}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ export enum Method {
PASSWORD = 'password',
}

export type OnConfirm = (code: string, method: Method) => void;
export type OnConfirm = (code: string, method: Method) => void | Promise<void>;

type TwoFactorModalProps = {
onConfirm: OnConfirm;
onClose: () => void;
invalidAttempt?: boolean;
} & (
| {
method: 'totp' | 'password';
Expand All @@ -26,19 +25,19 @@ type TwoFactorModalProps = {
}
);

const TwoFactorModal = ({ onConfirm, onClose, invalidAttempt, ...props }: TwoFactorModalProps): ReactElement => {
const TwoFactorModal = ({ onConfirm, onClose, ...props }: TwoFactorModalProps): ReactElement => {
if (props.method === Method.TOTP) {
return <TwoFactorTotp onConfirm={onConfirm} onClose={onClose} invalidAttempt={invalidAttempt} />;
return <TwoFactorTotp onConfirm={onConfirm} onClose={onClose} />;
}

if (props.method === Method.EMAIL) {
const { emailOrUsername } = props;

return <TwoFactorEmail onConfirm={onConfirm} onClose={onClose} emailOrUsername={emailOrUsername} invalidAttempt={invalidAttempt} />;
return <TwoFactorEmail onConfirm={onConfirm} onClose={onClose} emailOrUsername={emailOrUsername} />;
}

if (props.method === Method.PASSWORD) {
return <TwoFactorPassword onConfirm={onConfirm} onClose={onClose} invalidAttempt={invalidAttempt} />;
return <TwoFactorPassword onConfirm={onConfirm} onClose={onClose} />;
}

throw new Error('Invalid Two Factor method');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Box, PasswordInput, FieldGroup, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { Box } from '@rocket.chat/fuselage';
import { PasswordInput, FieldGroup, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage-forms';
import { GenericModal } from '@rocket.chat/ui-client';
import type { ReactElement, ChangeEvent, Ref, SyntheticEvent } from 'react';
import { useId, useState } from 'react';
import type { ReactElement } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import type { OnConfirm } from './TwoFactorModal';
Expand All @@ -11,51 +11,62 @@ import { Method } from './TwoFactorModal';
type TwoFactorPasswordModalProps = {
onConfirm: OnConfirm;
onClose: () => void;
invalidAttempt?: boolean;
};

const TwoFactorPasswordModal = ({ onConfirm, onClose, invalidAttempt }: TwoFactorPasswordModalProps): ReactElement => {
const { t } = useTranslation();
const [code, setCode] = useState<string>('');
const ref = useAutoFocus();
type TwoFactorPasswordFormData = {
password: string;
};

const onConfirmTotpCode = (e: SyntheticEvent): void => {
e.preventDefault();
onConfirm(code, Method.PASSWORD);
};
const TwoFactorPasswordModal = ({ onConfirm, onClose }: TwoFactorPasswordModalProps): ReactElement => {
const { t } = useTranslation();

const onChange = ({ currentTarget }: ChangeEvent<HTMLInputElement>): void => {
setCode(currentTarget.value);
};
const {
control,
handleSubmit,
setError,
setValue,
formState: { errors, isSubmitting },
} = useForm<TwoFactorPasswordFormData>({
defaultValues: { password: '' },
});

const id = useId();
const onSubmit = handleSubmit(async ({ password }) => {
try {
await onConfirm(password, Method.PASSWORD);
} catch (error) {
setError('password', {
type: 'manual',
message: t('Invalid_password'),
});
setValue('password', '');
}
});

return (
<GenericModal
wrapperFunction={(props) => <Box is='form' onSubmit={onConfirmTotpCode} {...props} />}
wrapperFunction={(props) => <Box is='form' onSubmit={onSubmit} {...props} />}
onCancel={onClose}
confirmText={t('Verify')}
title={t('Please_enter_your_password')}
onClose={onClose}
variant='warning'
icon='info'
confirmDisabled={!code}
confirmDisabled={isSubmitting}
>
<FieldGroup>
<Field>
<FieldLabel alignSelf='stretch' htmlFor={id}>
{t('For_your_security_you_must_enter_your_current_password_to_continue')}
</FieldLabel>
<FieldLabel alignSelf='stretch'>{t('For_your_security_you_must_enter_your_current_password_to_continue')}</FieldLabel>
<FieldRow>
<PasswordInput
id={id}
ref={ref as Ref<HTMLInputElement>}
value={code}
onChange={onChange}
placeholder={t('Password')}
></PasswordInput>
<Controller
name='password'
control={control}
rules={{ required: t('Required_field', { field: t('Password') }) }}
render={({ field }) => (
<PasswordInput {...field} placeholder={t('Password')} disabled={isSubmitting} error={errors.password?.message} />
)}
/>
</FieldRow>
{invalidAttempt && <FieldError>{t('Invalid_password')}</FieldError>}
{errors.password && <FieldError>{errors.password.message}</FieldError>}
</Field>
</FieldGroup>
</GenericModal>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Box, TextInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { Box } from '@rocket.chat/fuselage';
import { FieldGroup, TextInput, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage-forms';
import { GenericModal } from '@rocket.chat/ui-client';
import type { ReactElement, ChangeEvent, SyntheticEvent } from 'react';
import { useId, useState } from 'react';
import type { ReactElement } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import type { OnConfirm } from './TwoFactorModal';
Expand All @@ -12,54 +12,71 @@ type TwoFactorTotpModalProps = {
onConfirm: OnConfirm;
onClose: () => void;
onDismiss?: () => void;
invalidAttempt?: boolean;
};

const TwoFactorTotpModal = ({ onConfirm, onClose, onDismiss, invalidAttempt }: TwoFactorTotpModalProps): ReactElement => {
type TwoFactorTotpFormData = {
code: string;
};

const TwoFactorTotpModal = ({ onConfirm, onClose, onDismiss }: TwoFactorTotpModalProps): ReactElement => {
const { t } = useTranslation();
const [code, setCode] = useState<string>('');
const ref = useAutoFocus<HTMLInputElement>();

const onConfirmTotpCode = (e: SyntheticEvent): void => {
e.preventDefault();
onConfirm(code, Method.TOTP);
};
const {
control,
handleSubmit,
setError,
setValue,
formState: { errors, isSubmitting },
} = useForm<TwoFactorTotpFormData>({
defaultValues: { code: '' },
});

const onChange = ({ currentTarget }: ChangeEvent<HTMLInputElement>): void => {
setCode(currentTarget.value);
};
const onSubmit = handleSubmit(async ({ code }) => {
try {
await onConfirm(code, Method.TOTP);
} catch (error) {
setError('code', {
type: 'manual',
message: t('Invalid_two_factor_code'),
});
setValue('code', '');
}
});

const id = useId();
return (
<GenericModal
wrapperFunction={(props) => <Box is='form' onSubmit={onConfirmTotpCode} {...props} />}
wrapperFunction={(props) => <Box is='form' onSubmit={onSubmit} {...props} />}
onCancel={onClose}
confirmText={t('Verify')}
title={t('Enter_TOTP_password')}
onClose={onClose}
onDismiss={onDismiss}
variant='warning'
confirmDisabled={!code}
confirmDisabled={isSubmitting}
tagline={t('Two-factor_authentication')}
icon={null}
>
<FieldGroup>
<Field>
<FieldLabel alignSelf='stretch' htmlFor={id}>
{t('Enter_the_code_provided_by_your_authentication_app_to_continue')}
</FieldLabel>
<FieldLabel alignSelf='stretch'>{t('Enter_the_code_provided_by_your_authentication_app_to_continue')}</FieldLabel>
<FieldRow>
<TextInput
id={id}
ref={ref}
value={code}
onChange={onChange}
placeholder={t('Enter_code_here')}
autoComplete='one-time-code'
inputMode='numeric'
<Controller
name='code'
control={control}
rules={{ required: t('Required_field', { field: t('Code') }) }}
render={({ field }) => (
<TextInput
{...field}
placeholder={t('Enter_code_here')}
autoComplete='one-time-code'
inputMode='numeric'
disabled={isSubmitting}
error={errors.code?.message}
/>
)}
/>
</FieldRow>
{invalidAttempt && <FieldError>{t('Invalid_password')}</FieldError>}
{errors.code && <FieldError>{errors.code.message}</FieldError>}
</Field>
</FieldGroup>
</GenericModal>
Expand Down
Loading
Loading