Skip to content
6 changes: 6 additions & 0 deletions .changeset/new-doors-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/i18n': minor
'@rocket.chat/meteor': minor
---

Prevents creation of unnamed Personal Access Tokens by requiring the form's `name` field fullfilment.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { SelectOption } from '@rocket.chat/fuselage';
import { Box, TextInput, Button, Margins, Select } from '@rocket.chat/fuselage';
import { Box, TextInput, Button, Margins, Select, FieldError, FieldGroup, Field, FieldRow } from '@rocket.chat/fuselage';
import { useSetModal, useToastMessageDispatch, useUserId, useMethod } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import { useCallback, useMemo, useEffect } from 'react';
import { useCallback, useId, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

Expand All @@ -27,11 +27,10 @@ const AddToken = ({ reload }: AddTokenProps) => {
const initialValues = useMemo(() => ({ name: '', bypassTwoFactor: 'require' }), []);

const {
register,
resetField,
handleSubmit,
control,
formState: { isSubmitted, submitCount },
reset,
formState: { errors },
} = useForm<AddTokenFormData>({ defaultValues: initialValues });

const twoFactorAuthOptions: SelectOption[] = useMemo(
Expand All @@ -47,8 +46,14 @@ const AddToken = ({ reload }: AddTokenProps) => {
try {
const token = await createTokenFn({ tokenName, bypassTwoFactor: bypassTwoFactor === 'bypass' });

const handleDismissModal = () => {
setModal(null);
reload();
reset();
};

setModal(
<GenericModal title={t('API_Personal_Access_Token_Generated')} onConfirm={() => setModal(null)} onClose={() => setModal(null)}>
<GenericModal title={t('API_Personal_Access_Token_Generated')} onConfirm={handleDismissModal} onClose={handleDismissModal}>
<Box
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
Expand All @@ -65,32 +70,48 @@ const AddToken = ({ reload }: AddTokenProps) => {
dispatchToastMessage({ type: 'error', message: error });
}
},
[createTokenFn, dispatchToastMessage, setModal, t, userId],
[createTokenFn, dispatchToastMessage, reload, reset, setModal, t, userId],
);

useEffect(() => {
resetField('name');
reload();
}, [isSubmitted, submitCount, reload, resetField]);
const nameErrorId = useId();

return (
<Box display='flex' is='form' onSubmit={handleSubmit(handleAddToken)} mb={8}>
<Box display='flex' width='100%'>
<Margins inlineEnd={4}>
<TextInput data-qa='PersonalTokenField' {...register('name')} placeholder={t('API_Add_Personal_Access_Token')} />
<Box>
<FieldGroup is='form' onSubmit={handleSubmit(handleAddToken)} mb={8}>
<Field>
<FieldRow>
<Margins inlineEnd={4}>
<Controller
name='bypassTwoFactor'
name='name'
control={control}
render={({ field }) => <Select {...field} options={twoFactorAuthOptions} />}
rules={{ validate: (value) => (value.trim() ? undefined : t('Please_provide_a_name_for_your_token')) }}
render={({ field }) => (
<TextInput
aria-describedby={nameErrorId}
data-qa='PersonalTokenField'
{...field}
placeholder={t('API_Add_Personal_Access_Token')}
/>
)}
/>
</Box>
</Margins>
</Box>
<Button primary type='submit'>
{t('Add')}
</Button>
</Box>
<Box>
<Controller
name='bypassTwoFactor'
control={control}
render={({ field }) => <Select {...field} options={twoFactorAuthOptions} />}
/>
</Box>
</Margins>
<Button primary type='submit'>
{t('Add')}
</Button>
</FieldRow>
{errors?.name && (
<FieldError id={nameErrorId} role='alert'>
{errors.name.message}
</FieldError>
)}
</Field>
</FieldGroup>
);
};

Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/tests/e2e/account-profile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ test.describe.serial('settings-account-profile', () => {
await poAccountProfile.btnTokenAddedOk.click();
});

await test.step('should not allow add new personal with no name', async () => {
await poAccountProfile.btnTokensAdd.click();
await expect(page.getByRole('alert').filter({ hasText: 'Please provide a name for your token' })).toBeVisible();
});

await test.step('should not allow add new personal token with same name', async () => {
await poAccountProfile.inputToken.fill(token);
await poAccountProfile.btnTokensAdd.click();
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -3976,6 +3976,7 @@
"Premium_capability": "Premium capability",
"Premium_omnichannel_capabilities": "Premium omnichannel capabilities",
"Premium_only": "Premium only",
"Please_provide_a_name_for_your_token": "Please provide a name for your token",
"Preparing_data_for_import_process": "Preparing data for import process",
"Preparing_list_of_channels": "Preparing list of channels",
"Preparing_list_of_messages": "Preparing list of messages",
Expand Down
Loading