Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions .changeset/nervous-clouds-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/ui-client': minor
'@rocket.chat/meteor': minor
---

Enables the password policy by default to ensure security by default and alters SetupWizard to handle errors
4 changes: 2 additions & 2 deletions apps/meteor/server/settings/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ export const createAccountSettings = () =>
});

await this.section('Password_Policy', async function () {
await this.add('Accounts_Password_Policy_Enabled', false, {
await this.add('Accounts_Password_Policy_Enabled', true, {
type: 'boolean',
public: true,
});
Expand All @@ -820,7 +820,7 @@ export const createAccountSettings = () =>
public: true,
};

await this.add('Accounts_Password_Policy_MinLength', 7, {
await this.add('Accounts_Password_Policy_MinLength', 14, {
type: 'int',
public: true,
enableQuery,
Expand Down
60 changes: 57 additions & 3 deletions packages/ui-client/src/views/setupWizard/steps/AdminInfoStep.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AdminInfoPage } from '@rocket.chat/onboarding-ui';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useSetting, useVerifyPassword, usePasswordPolicy } from '@rocket.chat/ui-contexts';
import type { ReactElement, ComponentProps } from 'react';
import { useMemo } from 'react';
import { I18nextProvider, useTranslation } from 'react-i18next';

import { useSetupWizardContext } from '../contexts/SetupWizardContext';
Expand All @@ -18,6 +19,46 @@ const AdminInfoStep = (): ReactElement => {

const { currentStep, validateEmail, registerAdminUser, maxSteps } = useSetupWizardContext();

const enabled = useSetting('Accounts_Password_Policy_Enabled', false);
const minLength = useSetting('Accounts_Password_Policy_MinLength', 14);
const maxLength = useSetting('Accounts_Password_Policy_MaxLength', -1);
const forbidRepeatingCharacters = useSetting('Accounts_Password_Policy_ForbidRepeatingCharacters', true);
const forbidRepeatingCharactersCount = useSetting('Accounts_Password_Policy_ForbidRepeatingCharactersCount', 3);
const mustContainAtLeastOneLowercase = useSetting('Accounts_Password_Policy_AtLeastOneLowercase', true);
const mustContainAtLeastOneUppercase = useSetting('Accounts_Password_Policy_AtLeastOneUppercase', true);
const mustContainAtLeastOneNumber = useSetting('Accounts_Password_Policy_AtLeastOneNumber', true);
const mustContainAtLeastOneSpecialCharacter = useSetting('Accounts_Password_Policy_AtLeastOneSpecialCharacter', true);

const validatePasswordPolicy = usePasswordPolicy({
enabled,
minLength,
maxLength,
forbidRepeatingCharacters,
forbidRepeatingCharactersCount,
mustContainAtLeastOneLowercase,
mustContainAtLeastOneUppercase,
mustContainAtLeastOneNumber,
mustContainAtLeastOneSpecialCharacter,
throwError: false,
});

const passwordPolicyValidations = useVerifyPassword('');
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
const passwordRulesHint = useMemo(() => {
if (!passwordPolicyValidations.validations || passwordPolicyValidations.validations.length === 0) {
return '';
}

return passwordPolicyValidations.validations
.map((validation) => {
const labelKey = `${validation.name}-label` as const;
if ('limit' in validation) {
return t(labelKey, { limit: validation.limit });
}
return t(labelKey);
})
.join(', ');
}, [passwordPolicyValidations.validations, t]);

// TODO: check if username exists
const validateUsername = (username: string): boolean | string => {
if (!usernameRegExp.test(username) || hasBlockedName(username)) {
Expand All @@ -27,15 +68,28 @@ const AdminInfoStep = (): ReactElement => {
return true;
};

const validatePassword = (password: string): boolean | string => {
if (!password || password.length === 0) {
return t('Required_field', { field: t('Password') });
}

const passwordValidation = validatePasswordPolicy(password);
if (!passwordValidation.valid) {
return t('Password_must_meet_the_complexity_requirements');
}

return true;
};

const handleSubmit: ComponentProps<typeof AdminInfoPage>['onSubmit'] = async (data) => {
registerAdminUser(data);
};

return (
<I18nextProvider i18n={i18n} defaultNS='onboarding'>
<AdminInfoPage
validatePassword={(password) => password.length > 0}
passwordRulesHint=''
validatePassword={validatePassword}
passwordRulesHint={passwordRulesHint}
validateUsername={validateUsername}
validateEmail={validateEmail}
currentStep={currentStep}
Expand Down
Loading