) => {
+ if (!event?.currentTarget?.files?.length) {
+ return;
+ }
+
+ handleUpload(event.currentTarget.files);
+ };
+
+ const handleUpload = (files: FileList) => {
+ if (!onUpload) {
+ return;
+ }
+
+ let filteredFiles = Array.from(files);
+
+ if (accept) {
+ const acceptMatchers = accept.split(',').map((acceptString) => {
+ if (acceptString.charAt(0) === '.') {
+ return ({ name }: { name: string }) => new RegExp(`${escapeForRegExp(acceptString)}$`, 'i').test(name);
+ }
+
+ const matchTypeOnly = /^(.+)\/\*$/i.exec(acceptString);
+ if (matchTypeOnly) {
+ return ({ type }: { type: string }) => new RegExp(`^${escapeForRegExp(matchTypeOnly[1])}/.*$`, 'i').test(type);
+ }
+
+ return ({ type }: { type: string }) => new RegExp(`^s${escapeForRegExp(acceptString)}$`, 'i').test(type);
+ });
+
+ filteredFiles = filteredFiles.filter((file) => acceptMatchers.some((acceptMatcher) => acceptMatcher(file)));
+ }
+
+ if (!multiple) {
+ filteredFiles = filteredFiles.slice(0, 1);
+ }
+
+ filteredFiles.length && onUpload(filteredFiles);
+ };
+
+ return (
+ 0 }, [className])}
+ style={style}
+ >
+
+ {children}
+
+ );
+};
diff --git a/packages/livechat/src/components/FilesDropTarget/stories.tsx b/packages/livechat/src/components/FilesDropTarget/stories.tsx
index e1a01cbaea09..d687fbaad492 100644
--- a/packages/livechat/src/components/FilesDropTarget/stories.tsx
+++ b/packages/livechat/src/components/FilesDropTarget/stories.tsx
@@ -71,7 +71,7 @@ AcceptingMultipleFiles.args = {
export const TriggeringBrowseAction = Template.bind({});
TriggeringBrowseAction.storyName = 'triggering browse action';
-const ref = createRef();
+const inputRef = createRef();
TriggeringBrowseAction.args = {
children: (
-
+
),
- ref,
+ inputRef,
};
diff --git a/packages/livechat/src/routes/Chat/component.js b/packages/livechat/src/routes/Chat/component.js
index 98991ab72b53..8bd9ac468c6e 100644
--- a/packages/livechat/src/routes/Chat/component.js
+++ b/packages/livechat/src/routes/Chat/component.js
@@ -1,4 +1,4 @@
-import { Component } from 'preact';
+import { Component, createRef } from 'preact';
import { Suspense, lazy } from 'preact/compat';
import { withTranslation } from 'react-i18next';
@@ -35,6 +35,8 @@ class Chat extends Component {
emojiPickerActive: false,
};
+ inputRef = createRef(null);
+
handleFilesDropTargetRef = (ref) => {
this.filesDropTarget = ref;
};
@@ -61,7 +63,7 @@ class Chat extends Component {
handleUploadClick = (event) => {
event.preventDefault();
- this.filesDropTarget.browse();
+ this.inputRef?.current?.click();
};
handleSendClick = (event) => {
@@ -151,7 +153,7 @@ class Chat extends Component {
handleEmojiClick={this.handleEmojiClick}
{...props}
>
-
+
{incomingCallAlert && !!incomingCallAlert.show && }
{incomingCallAlert?.show && ongoingCall && ongoingCall.callStatus === CallStatus.IN_PROGRESS_SAME_TAB ? (
diff --git a/packages/password-policies/.eslintrc.json b/packages/password-policies/.eslintrc.json
new file mode 100644
index 000000000000..15f2cd4817e1
--- /dev/null
+++ b/packages/password-policies/.eslintrc.json
@@ -0,0 +1,8 @@
+{
+ "extends": ["@rocket.chat/eslint-config"],
+ "plugins": ["jest"],
+ "env": {
+ "jest/globals": true
+ },
+ "ignorePatterns": ["**/dist"]
+}
diff --git a/packages/password-policies/jest.config.ts b/packages/password-policies/jest.config.ts
new file mode 100644
index 000000000000..959a31a7c6bf
--- /dev/null
+++ b/packages/password-policies/jest.config.ts
@@ -0,0 +1,3 @@
+export default {
+ preset: 'ts-jest',
+};
diff --git a/packages/password-policies/package.json b/packages/password-policies/package.json
new file mode 100644
index 000000000000..52fa766671db
--- /dev/null
+++ b/packages/password-policies/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@rocket.chat/password-policies",
+ "version": "0.0.1",
+ "private": true,
+ "devDependencies": {
+ "@types/chai": "^4.3.5",
+ "@types/jest": "~29.5.3",
+ "chai": "^4.3.7",
+ "eslint": "~8.45.0",
+ "jest": "~29.6.1",
+ "ts-jest": "~29.0.5",
+ "typescript": "~5.2.2"
+ },
+ "scripts": {
+ "lint": "eslint --ext .js,.jsx,.ts,.tsx .",
+ "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix",
+ "testunit": "jest",
+ "build": "rm -rf dist && tsc -p tsconfig.json",
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
+ },
+ "main": "./dist/index.js",
+ "typings": "./dist/index.d.ts",
+ "files": [
+ "/dist"
+ ]
+}
diff --git a/apps/meteor/app/lib/server/lib/PasswordPolicyClass.js b/packages/password-policies/src/PasswordPolicyClass.ts
similarity index 61%
rename from apps/meteor/app/lib/server/lib/PasswordPolicyClass.js
rename to packages/password-policies/src/PasswordPolicyClass.ts
index 99dc0d6fabf5..8212df18002c 100644
--- a/apps/meteor/app/lib/server/lib/PasswordPolicyClass.js
+++ b/packages/password-policies/src/PasswordPolicyClass.ts
@@ -1,8 +1,45 @@
-import { Random } from '@rocket.chat/random';
-import generator from 'generate-password';
-import { Meteor } from 'meteor/meteor';
+import { PasswordPolicyError } from './PasswordPolicyError';
+
+type PasswordPolicyType = {
+ enabled: boolean;
+ policy: [name: string, options?: Record][];
+};
+
+type ValidationMessageType = {
+ name: string;
+ isValid: boolean;
+ limit?: number;
+};
+
+export class PasswordPolicy {
+ private regex: {
+ forbiddingRepeatingCharacters: RegExp;
+ mustContainAtLeastOneLowercase: RegExp;
+ mustContainAtLeastOneUppercase: RegExp;
+ mustContainAtLeastOneNumber: RegExp;
+ mustContainAtLeastOneSpecialCharacter: RegExp;
+ };
+
+ private enabled: boolean;
+
+ private minLength: number;
+
+ private maxLength: number;
+
+ private forbidRepeatingCharacters: boolean;
+
+ private mustContainAtLeastOneLowercase: boolean;
+
+ private mustContainAtLeastOneUppercase: boolean;
+
+ private mustContainAtLeastOneNumber: boolean;
+
+ private mustContainAtLeastOneSpecialCharacter: boolean;
+
+ private throwError: boolean;
+
+ private forbidRepeatingCharactersCount: number;
-class PasswordPolicy {
constructor({
enabled = false,
minLength = -1,
@@ -14,14 +51,7 @@ class PasswordPolicy {
mustContainAtLeastOneNumber = false,
mustContainAtLeastOneSpecialCharacter = false,
throwError = true,
- } = {}) {
- this.regex = {
- mustContainAtLeastOneLowercase: new RegExp('[a-z]'),
- mustContainAtLeastOneUppercase: new RegExp('[A-Z]'),
- mustContainAtLeastOneNumber: new RegExp('[0-9]'),
- mustContainAtLeastOneSpecialCharacter: new RegExp('[^A-Za-z0-9 ]'),
- };
-
+ }) {
this.enabled = enabled;
this.minLength = minLength;
this.maxLength = maxLength;
@@ -32,27 +62,103 @@ class PasswordPolicy {
this.mustContainAtLeastOneNumber = mustContainAtLeastOneNumber;
this.mustContainAtLeastOneSpecialCharacter = mustContainAtLeastOneSpecialCharacter;
this.throwError = throwError;
- }
- set forbidRepeatingCharactersCount(value) {
- this._forbidRepeatingCharactersCount = value;
- this.regex.forbiddingRepeatingCharacters = new RegExp(`(.)\\1{${this.forbidRepeatingCharactersCount},}`);
- }
-
- get forbidRepeatingCharactersCount() {
- return this._forbidRepeatingCharactersCount;
+ this.regex = {
+ forbiddingRepeatingCharacters: new RegExp(`(.)\\1{${forbidRepeatingCharactersCount},}`),
+ mustContainAtLeastOneLowercase: new RegExp('[a-z]'),
+ mustContainAtLeastOneUppercase: new RegExp('[A-Z]'),
+ mustContainAtLeastOneNumber: new RegExp('[0-9]'),
+ mustContainAtLeastOneSpecialCharacter: new RegExp('[^A-Za-z0-9 ]'),
+ };
}
- error(error, message, reasons) {
+ error(
+ error: string,
+ message: string,
+ reasons?: {
+ error: string;
+ message: string;
+ }[],
+ ) {
if (this.throwError) {
- throw new Meteor.Error(error, message, reasons);
+ throw new PasswordPolicyError(message, error, reasons);
}
return false;
}
- validate(password) {
- const reasons = [];
+ sendValidationMessage(password: string): {
+ name: string;
+ isValid: boolean;
+ limit?: number;
+ }[] {
+ const validationReturn: ValidationMessageType[] = [];
+
+ if (!this.enabled) {
+ return [];
+ }
+
+ if (this.minLength >= 1) {
+ validationReturn.push({
+ name: 'get-password-policy-minLength',
+ isValid: !(password.length < this.minLength),
+ limit: this.minLength,
+ });
+ }
+
+ if (this.maxLength >= 1) {
+ validationReturn.push({
+ name: 'get-password-policy-maxLength',
+ isValid: !(password.length > this.maxLength),
+ limit: this.maxLength,
+ });
+ }
+
+ if (this.forbidRepeatingCharacters) {
+ validationReturn.push({
+ name: 'get-password-policy-forbidRepeatingCharactersCount',
+ isValid: !this.regex.forbiddingRepeatingCharacters.test(password),
+ limit: this.forbidRepeatingCharactersCount,
+ });
+ }
+
+ if (this.mustContainAtLeastOneLowercase) {
+ validationReturn.push({
+ name: 'get-password-policy-mustContainAtLeastOneLowercase',
+ isValid: this.regex.mustContainAtLeastOneLowercase.test(password),
+ });
+ }
+
+ if (this.mustContainAtLeastOneUppercase) {
+ validationReturn.push({
+ name: 'get-password-policy-mustContainAtLeastOneUppercase',
+ isValid: this.regex.mustContainAtLeastOneUppercase.test(password),
+ });
+ }
+
+ if (this.mustContainAtLeastOneNumber) {
+ validationReturn.push({
+ name: 'get-password-policy-mustContainAtLeastOneNumber',
+ isValid: this.regex.mustContainAtLeastOneNumber.test(password),
+ });
+ }
+
+ if (this.mustContainAtLeastOneSpecialCharacter) {
+ validationReturn.push({
+ name: 'get-password-policy-mustContainAtLeastOneSpecialCharacter',
+ isValid: this.regex.mustContainAtLeastOneSpecialCharacter.test(password),
+ });
+ }
+
+ return validationReturn;
+ }
+
+ validate(password: string) {
+ const reasons: {
+ error: string;
+ message: string;
+ }[] = [];
+
if (typeof password !== 'string' || !password.trim().length) {
return this.error('error-password-policy-not-met', "The password provided does not meet the server's password policy.");
}
@@ -117,11 +223,12 @@ class PasswordPolicy {
return true;
}
- getPasswordPolicy() {
- const data = {
+ getPasswordPolicy(): PasswordPolicyType {
+ const data: PasswordPolicyType = {
enabled: false,
policy: [],
};
+
if (this.enabled) {
data.enabled = true;
if (this.minLength >= 1) {
@@ -154,31 +261,4 @@ class PasswordPolicy {
}
return data;
}
-
- generatePassword() {
- if (this.enabled) {
- for (let i = 0; i < 10; i++) {
- const password = this._generatePassword();
- if (this.validate(password)) {
- return password;
- }
- }
- }
-
- return Random.id();
- }
-
- _generatePassword() {
- const length = Math.min(Math.max(this.minLength, 12), this.maxLength > 0 ? this.maxLength : Number.MAX_SAFE_INTEGER);
- return generator.generate({
- length,
- ...(this.mustContainAtLeastOneNumber && { numbers: true }),
- ...(this.mustContainAtLeastOneSpecialCharacter && { symbols: true }),
- ...(this.mustContainAtLeastOneLowercase && { lowercase: true }),
- ...(this.mustContainAtLeastOneUppercase && { uppercase: true }),
- strict: true,
- });
- }
}
-
-export default PasswordPolicy;
diff --git a/packages/password-policies/src/PasswordPolicyError.ts b/packages/password-policies/src/PasswordPolicyError.ts
new file mode 100644
index 000000000000..ea58d0e83b07
--- /dev/null
+++ b/packages/password-policies/src/PasswordPolicyError.ts
@@ -0,0 +1,11 @@
+export class PasswordPolicyError extends Error {
+ public error: string;
+
+ public details?: { error: string; message: string }[] | undefined;
+
+ constructor(message: string, error: string, details?: { error: string; message: string }[]) {
+ super(message);
+ this.error = error;
+ this.details = details;
+ }
+}
diff --git a/packages/password-policies/src/index.ts b/packages/password-policies/src/index.ts
new file mode 100644
index 000000000000..ce94042e029a
--- /dev/null
+++ b/packages/password-policies/src/index.ts
@@ -0,0 +1 @@
+export { PasswordPolicy } from './PasswordPolicyClass';
diff --git a/packages/password-policies/tests/passwordPolicyClass.test.ts b/packages/password-policies/tests/passwordPolicyClass.test.ts
new file mode 100644
index 000000000000..4cda16e3dd27
--- /dev/null
+++ b/packages/password-policies/tests/passwordPolicyClass.test.ts
@@ -0,0 +1,223 @@
+import { expect } from 'chai';
+
+import { PasswordPolicy } from '../src/PasswordPolicyClass';
+
+describe('PasswordPolicy', () => {
+ describe('Password tests with default options', () => {
+ it('should allow all passwords', () => {
+ const passwordPolicy = new PasswordPolicy({ throwError: false });
+ expect(passwordPolicy.validate(null as any)).to.be.equal(false);
+ expect(passwordPolicy.validate(undefined as any)).to.be.equal(false);
+ expect(passwordPolicy.validate('')).to.be.equal(false);
+ expect(passwordPolicy.validate(' ')).to.be.equal(false);
+ expect(passwordPolicy.validate('a')).to.be.equal(true);
+ expect(passwordPolicy.validate('aaaaaaaaa')).to.be.equal(true);
+ });
+ });
+
+ describe('Password tests with options', () => {
+ it('should not allow non string or empty', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ throwError: false,
+ });
+ expect(passwordPolicy.validate(null as any)).to.be.equal(false);
+ expect(passwordPolicy.validate(undefined as any)).to.be.false;
+ expect(passwordPolicy.validate(1 as any)).to.be.false;
+ expect(passwordPolicy.validate(true as any)).to.be.false;
+ expect(passwordPolicy.validate(new Date() as any)).to.be.false;
+ expect(passwordPolicy.validate(new Function() as any)).to.be.false;
+ expect(passwordPolicy.validate('')).to.be.false;
+ });
+
+ it('should restrict by minLength', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ minLength: 5,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('1')).to.be.false;
+ expect(passwordPolicy.validate('1234')).to.be.false;
+ expect(passwordPolicy.validate('12345')).to.be.true;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ });
+
+ it('should restrict by maxLength', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ maxLength: 5,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('1')).to.be.true;
+ expect(passwordPolicy.validate('12345')).to.be.true;
+ expect(passwordPolicy.validate('123456')).to.be.false;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ });
+
+ it('should allow repeated characters', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ forbidRepeatingCharacters: false,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('1')).to.be.true;
+ expect(passwordPolicy.validate('12345')).to.be.true;
+ expect(passwordPolicy.validate('123456')).to.be.true;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ expect(passwordPolicy.validate('11111111111111')).to.be.true;
+ });
+
+ it('should restrict repeated characters', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ forbidRepeatingCharacters: true,
+ forbidRepeatingCharactersCount: 3,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('1')).to.be.true;
+ expect(passwordPolicy.validate('11')).to.be.true;
+ expect(passwordPolicy.validate('111')).to.be.true;
+ expect(passwordPolicy.validate('1111')).to.be.false;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ expect(passwordPolicy.validate('123456')).to.be.true;
+ });
+
+ it('should restrict repeated characters customized', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ forbidRepeatingCharacters: true,
+ forbidRepeatingCharactersCount: 5,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('1')).to.be.true;
+ expect(passwordPolicy.validate('11')).to.be.true;
+ expect(passwordPolicy.validate('111')).to.be.true;
+ expect(passwordPolicy.validate('1111')).to.be.true;
+ expect(passwordPolicy.validate('11111')).to.be.true;
+ expect(passwordPolicy.validate('111111')).to.be.false;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ expect(passwordPolicy.validate('123456')).to.be.true;
+ });
+
+ it('should contain one lowercase', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ mustContainAtLeastOneLowercase: true,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('a')).to.be.true;
+ expect(passwordPolicy.validate('aa')).to.be.true;
+ expect(passwordPolicy.validate('A')).to.be.false;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ expect(passwordPolicy.validate('123456')).to.be.false;
+ expect(passwordPolicy.validate('AAAAA')).to.be.false;
+ expect(passwordPolicy.validate('AAAaAAA')).to.be.true;
+ });
+
+ it('should contain one uppercase', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ mustContainAtLeastOneUppercase: true,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('a')).to.be.false;
+ expect(passwordPolicy.validate('aa')).to.be.false;
+ expect(passwordPolicy.validate('A')).to.be.true;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ expect(passwordPolicy.validate('123456')).to.be.false;
+ expect(passwordPolicy.validate('AAAAA')).to.be.true;
+ expect(passwordPolicy.validate('AAAaAAA')).to.be.true;
+ });
+
+ it('should contain one number', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ mustContainAtLeastOneNumber: true,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('a')).to.be.false;
+ expect(passwordPolicy.validate('aa')).to.be.false;
+ expect(passwordPolicy.validate('A')).to.be.false;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ expect(passwordPolicy.validate('123456')).to.be.true;
+ expect(passwordPolicy.validate('AAAAA')).to.be.false;
+ expect(passwordPolicy.validate('AAAaAAA')).to.be.false;
+ expect(passwordPolicy.validate('AAAa1AAA')).to.be.true;
+ });
+
+ it('should contain one special character', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ mustContainAtLeastOneSpecialCharacter: true,
+ throwError: false,
+ });
+
+ expect(passwordPolicy.validate('a')).to.be.false;
+ expect(passwordPolicy.validate('aa')).to.be.false;
+ expect(passwordPolicy.validate('A')).to.be.false;
+ expect(passwordPolicy.validate(' ')).to.be.false;
+ expect(passwordPolicy.validate('123456')).to.be.false;
+ expect(passwordPolicy.validate('AAAAA')).to.be.false;
+ expect(passwordPolicy.validate('AAAaAAA')).to.be.false;
+ expect(passwordPolicy.validate('AAAa1AAA')).to.be.false;
+ expect(passwordPolicy.validate('AAAa@AAA')).to.be.true;
+ });
+ });
+
+ describe('Password Policy', () => {
+ it('should return a correct password policy', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ throwError: false,
+ minLength: 10,
+ maxLength: 20,
+ forbidRepeatingCharacters: true,
+ forbidRepeatingCharactersCount: 4,
+ mustContainAtLeastOneLowercase: true,
+ mustContainAtLeastOneUppercase: true,
+ mustContainAtLeastOneNumber: true,
+ mustContainAtLeastOneSpecialCharacter: true,
+ });
+
+ const policy = passwordPolicy.getPasswordPolicy();
+
+ expect(policy).to.not.be.undefined;
+ expect(policy.enabled).to.be.true;
+ expect(policy.policy.length).to.be.equal(8);
+ expect(policy.policy[0][0]).to.be.equal('get-password-policy-minLength');
+ expect(policy.policy[0][1]?.minLength).to.be.equal(10);
+ });
+
+ it('should return correct values if policy is disabled', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: false,
+ });
+
+ const policy = passwordPolicy.getPasswordPolicy();
+
+ expect(policy.enabled).to.be.false;
+ expect(policy.policy.length).to.be.equal(0);
+ });
+
+ it('should return correct values if policy is enabled but no specifiers exists', () => {
+ const passwordPolicy = new PasswordPolicy({
+ enabled: true,
+ });
+
+ const policy = passwordPolicy.getPasswordPolicy();
+
+ expect(policy.enabled).to.be.true;
+ // even when no policy is specified, forbidRepeatingCharactersCount is still configured
+ // since its default value is 3
+ expect(policy.policy.length).to.be.equal(1);
+ });
+ });
+});
diff --git a/packages/password-policies/tsconfig.json b/packages/password-policies/tsconfig.json
new file mode 100644
index 000000000000..e2be47cf5499
--- /dev/null
+++ b/packages/password-policies/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.client.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./dist"
+ },
+ "include": ["./src/**/*"]
+}
diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.stories.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.stories.tsx
index fe3be924ab18..412305be13e9 100644
--- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.stories.tsx
+++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.stories.tsx
@@ -3,46 +3,42 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { PasswordVerifier } from './PasswordVerifier';
-type Response = {
- enabled: boolean;
- policy: [
- name: string,
- value?:
- | {
- [x: string]: number;
- }
- | undefined,
- ][];
-};
-
export default {
title: 'Components/PasswordVerifier',
component: PasswordVerifier,
-} as ComponentMeta;
-
-const response: Response = {
- enabled: true,
- policy: [
- ['get-password-policy-minLength', { minLength: 10 }],
- ['get-password-policy-forbidRepeatingCharactersCount', { maxRepeatingChars: 3 }],
- ['get-password-policy-mustContainAtLeastOneLowercase'],
- ['get-password-policy-mustContainAtLeastOneUppercase'],
- ['get-password-policy-mustContainAtLeastOneNumber'],
- ['get-password-policy-mustContainAtLeastOneSpecialCharacter'],
+ decorators: [
+ mockAppRoot()
+ .withSetting('Accounts_Password_Policy_Enabled', 'true')
+ .withSetting('Accounts_Password_Policy_MinLength', '12')
+ .withSetting('Accounts_Password_Policy_MaxLength', '24')
+ .withSetting('Accounts_Password_Policy_ForbidRepeatingCharacters', 'true')
+ .withSetting('Accounts_Password_Policy_ForbidRepeatingCharactersCount', '3')
+ .withSetting('Accounts_Password_Policy_AtLeastOneLowercase', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneUppercase', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneNumber', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneSpecialCharacter', 'true')
+ .withSetting('Language', 'en')
+ .withTranslations('en', 'core', { Password_must_have: 'Password must have:' })
+ .withTranslations('en', 'core', { 'get-password-policy-minLength-label': 'At least {{limit}} characters' })
+ .withTranslations('en', 'core', { 'get-password-policy-maxLength-label': 'At most {{limit}} characters' })
+ .withTranslations('en', 'core', {
+ 'get-password-policy-forbidRepeatingCharactersCount-label': 'Max. {{limit}} repeating characters',
+ })
+ .withTranslations('en', 'core', {
+ 'get-password-policy-mustContainAtLeastOneLowercase-label': 'At least one lowercase letter',
+ })
+ .withTranslations('en', 'core', {
+ 'get-password-policy-mustContainAtLeastOneUppercase-label': 'At least one uppercase letter',
+ })
+ .withTranslations('en', 'core', { 'get-password-policy-mustContainAtLeastOneNumber-label': 'At least one number' })
+ .withTranslations('en', 'core', {
+ 'get-password-policy-mustContainAtLeastOneSpecialCharacter-label': 'At least one symbol',
+ })
+ .buildStoryDecorator(),
],
-};
-
-const Wrapper = mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
- .build();
-
-export const Default: ComponentStory = (args) => (
-
-
-
-);
+ args: {
+ password: '123',
+ },
+} as ComponentMeta;
-Default.storyName = 'PasswordVerifier';
-Default.args = {
- password: 'asd',
-};
+export const Default: ComponentStory = (args) => ;
diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx
index 9599f43c6cbb..bf53360a2351 100644
--- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx
+++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx
@@ -1,4 +1,4 @@
-import { Box, Skeleton } from '@rocket.chat/fuselage';
+import { Box } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useVerifyPassword } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';
@@ -10,15 +10,17 @@ type PasswordVerifierProps = {
id?: string;
};
+type PasswordVerificationProps = {
+ name: string;
+ isValid: boolean;
+ limit?: number;
+}[];
+
export const PasswordVerifier = ({ password, id }: PasswordVerifierProps) => {
const { t } = useTranslation();
const uniqueId = useUniqueId();
- const { data: passwordVerifications, isLoading } = useVerifyPassword(password || '');
-
- if (isLoading) {
- return ;
- }
+ const passwordVerifications: PasswordVerificationProps = useVerifyPassword(password || '');
if (!passwordVerifications?.length) {
return null;
diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx
index cd2a5175e59f..cb2efaca19f5 100644
--- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx
+++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx
@@ -1,51 +1,24 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
-import { passwordVerificationsTemplate } from '@rocket.chat/ui-contexts/dist/hooks/useVerifyPassword';
import { render, waitFor } from '@testing-library/react';
import { PasswordVerifier } from './PasswordVerifier';
-type Response = {
- enabled: boolean;
- policy: [
- name: string,
- value?:
- | {
- [x: string]: number;
- }
- | undefined,
- ][];
-};
-
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
it('should render no policy if its disabled ', () => {
- const response: Response = {
- enabled: false,
- policy: [],
- };
-
const { queryByRole } = render(, {
- wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
- .build(),
+ wrapper: mockAppRoot().withSetting('Accounts_Password_Policy_Enabled', 'true').build(),
});
expect(queryByRole('list')).toBeNull();
});
it('should render no policy if its enabled but empty', async () => {
- const response: Response = {
- enabled: true,
- policy: [],
- };
-
const { queryByRole, queryByTestId } = render(, {
- wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
- .build(),
+ wrapper: mockAppRoot().build(),
});
await waitFor(() => {
@@ -55,14 +28,10 @@ it('should render no policy if its enabled but empty', async () => {
});
it('should render policy list if its enabled and not empty', async () => {
- const response: Response = {
- enabled: true,
- policy: [['get-password-policy-minLength', { minLength: 10 }]],
- };
-
const { queryByRole, queryByTestId } = render(, {
wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
+ .withSetting('Accounts_Password_Policy_Enabled', 'true')
+ .withSetting('Accounts_Password_Policy_MinLength', '6')
.build(),
});
@@ -75,14 +44,17 @@ it('should render policy list if its enabled and not empty', async () => {
});
it('should render all the policies when all policies are enabled', async () => {
- const response: Response = {
- enabled: true,
- policy: Object.keys(passwordVerificationsTemplate).map((item) => [item]),
- };
-
const { queryByTestId, queryAllByRole } = render(, {
wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
+ .withSetting('Accounts_Password_Policy_Enabled', 'true')
+ .withSetting('Accounts_Password_Policy_MinLength', '6')
+ .withSetting('Accounts_Password_Policy_MaxLength', '24')
+ .withSetting('Accounts_Password_Policy_ForbidRepeatingCharacters', 'true')
+ .withSetting('Accounts_Password_Policy_ForbidRepeatingCharactersCount', '3')
+ .withSetting('Accounts_Password_Policy_AtLeastOneLowercase', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneUppercase', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneNumber', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneSpecialCharacter', 'true')
.build(),
});
@@ -90,18 +62,14 @@ it('should render all the policies when all policies are enabled', async () => {
expect(queryByTestId('password-verifier-skeleton')).toBeNull();
});
- expect(queryAllByRole('listitem').length).toEqual(response.policy.length);
+ expect(queryAllByRole('listitem').length).toEqual(7);
});
it("should render policy as invalid if password doesn't match the requirements", async () => {
- const response: Response = {
- enabled: true,
- policy: [['get-password-policy-minLength', { minLength: 10 }]],
- };
-
const { queryByTestId, getByRole } = render(, {
wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
+ .withSetting('Accounts_Password_Policy_Enabled', 'true')
+ .withSetting('Accounts_Password_Policy_MinLength', '10')
.build(),
});
@@ -113,14 +81,10 @@ it("should render policy as invalid if password doesn't match the requirements",
});
it('should render policy as valid if password matches the requirements', async () => {
- const response: Response = {
- enabled: true,
- policy: [['get-password-policy-minLength', { minLength: 2 }]],
- };
-
const { queryByTestId, getByRole } = render(, {
wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
+ .withSetting('Accounts_Password_Policy_Enabled', 'true')
+ .withSetting('Accounts_Password_Policy_MinLength', '2')
.build(),
});
diff --git a/packages/ui-client/src/hooks/useValidatePassword.spec.ts b/packages/ui-client/src/hooks/useValidatePassword.spec.ts
index 5d1c5a635c52..275e4ab8d6f5 100644
--- a/packages/ui-client/src/hooks/useValidatePassword.spec.ts
+++ b/packages/ui-client/src/hooks/useValidatePassword.spec.ts
@@ -3,64 +3,32 @@ import { renderHook } from '@testing-library/react-hooks';
import { useValidatePassword } from './useValidatePassword';
-type Response = {
- enabled: boolean;
- policy: [
- name: string,
- value?:
- | {
- [x: string]: number;
- }
- | undefined,
- ][];
-};
+const settingsMockWrapper = mockAppRoot()
+ .withSetting('Accounts_Password_Policy_Enabled', 'true')
+ .withSetting('Accounts_Password_Policy_MinLength', '6')
+ .withSetting('Accounts_Password_Policy_MaxLength', '24')
+ .withSetting('Accounts_Password_Policy_ForbidRepeatingCharacters', 'true')
+ .withSetting('Accounts_Password_Policy_ForbidRepeatingCharactersCount', '3')
+ .withSetting('Accounts_Password_Policy_AtLeastOneLowercase', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneUppercase', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneNumber', 'true')
+ .withSetting('Accounts_Password_Policy_AtLeastOneSpecialCharacter', 'true')
+ .build();
it("should return `false` if password doesn't match all the requirements", async () => {
- const response: Response = {
- enabled: true,
- policy: [['get-password-policy-minLength', { minLength: 8 }]],
- };
-
- const { result, waitForValueToChange } = renderHook(async () => useValidatePassword('secret'), {
- wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
- .build(),
+ const { result } = renderHook(async () => useValidatePassword('secret'), {
+ wrapper: settingsMockWrapper,
});
- await waitForValueToChange(() => result.current);
const res = await result.current;
expect(res).toBeFalsy();
});
it('should return `true` if password matches all the requirements', async () => {
- const response: Response = {
- enabled: true,
- policy: [['get-password-policy-minLength', { minLength: 8 }]],
- };
-
- const { result, waitForValueToChange } = renderHook(async () => useValidatePassword('secret-password'), {
- wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
- .build(),
+ const { result } = renderHook(async () => useValidatePassword('5kgnGPq^&t4DSYW!SH#4N'), {
+ wrapper: settingsMockWrapper,
});
- await waitForValueToChange(() => result.current);
const res = await result.current;
expect(res).toBeTruthy();
});
-
-it('should return `undefined` if password validation is still loading', async () => {
- const response: Response = {
- enabled: true,
- policy: [['get-password-policy-minLength', { minLength: 8 }]],
- };
-
- const { result } = renderHook(async () => useValidatePassword('secret-password'), {
- wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/pw.getPolicy', () => response)
- .build(),
- });
-
- const res = await result.current;
- expect(res).toBeUndefined();
-});
diff --git a/packages/ui-client/src/hooks/useValidatePassword.ts b/packages/ui-client/src/hooks/useValidatePassword.ts
index 3402bbaf8435..9098d13009e1 100644
--- a/packages/ui-client/src/hooks/useValidatePassword.ts
+++ b/packages/ui-client/src/hooks/useValidatePassword.ts
@@ -1,8 +1,14 @@
import { useVerifyPassword } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';
-export const useValidatePassword = (password: string) => {
- const { data: passwordVerifications, isLoading } = useVerifyPassword(password);
+type passwordVerificationsType = {
+ name: string;
+ isValid: boolean;
+ limit?: number;
+}[];
- return useMemo(() => (isLoading ? undefined : passwordVerifications.every(({ isValid }) => isValid)), [isLoading, passwordVerifications]);
+export const useValidatePassword = (password: string): boolean => {
+ const passwordVerifications: passwordVerificationsType = useVerifyPassword(password);
+
+ return useMemo(() => passwordVerifications.every(({ isValid }) => isValid), [passwordVerifications]);
};
diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json
index d25295fe2d97..fd262b4a66fc 100644
--- a/packages/ui-contexts/package.json
+++ b/packages/ui-contexts/package.json
@@ -29,6 +29,9 @@
"react": "~17.0.2",
"use-sync-external-store": "^1.2.0"
},
+ "dependencies": {
+ "@rocket.chat/password-policies": "workspace:^"
+ },
"volta": {
"extends": "../../package.json"
},
diff --git a/packages/ui-contexts/src/hooks/usePasswordPolicy.ts b/packages/ui-contexts/src/hooks/usePasswordPolicy.ts
deleted file mode 100644
index ca259a215586..000000000000
--- a/packages/ui-contexts/src/hooks/usePasswordPolicy.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-
-import { useEndpoint } from './useEndpoint';
-
-export const usePasswordPolicy = () => {
- const getPasswordPolicy = useEndpoint('GET', '/v1/pw.getPolicy');
-
- return useQuery(['login', 'password-policy'], async () => getPasswordPolicy());
-};
diff --git a/packages/ui-contexts/src/hooks/useVerifyPassword.ts b/packages/ui-contexts/src/hooks/useVerifyPassword.ts
index 71985e44887f..f1244b2552c6 100644
--- a/packages/ui-contexts/src/hooks/useVerifyPassword.ts
+++ b/packages/ui-contexts/src/hooks/useVerifyPassword.ts
@@ -1,69 +1,47 @@
-import { useCallback, useMemo } from 'react';
+import { PasswordPolicy } from '@rocket.chat/password-policies';
+import { useMemo } from 'react';
-import { usePasswordPolicy } from './usePasswordPolicy';
-
-export const passwordVerificationsTemplate: Record boolean> = {
- 'get-password-policy-minLength': (password: string, minLength?: number) => Boolean(minLength && password.length >= minLength),
- 'get-password-policy-maxLength': (password: string, maxLength?: number) => Boolean(maxLength && password.length <= maxLength),
- 'get-password-policy-forbidRepeatingCharactersCount': (password: string, maxRepeatingChars?: number) => {
- const repeatingCharsHash = {} as Record;
-
- for (let i = 0; i < password.length; i++) {
- const currentChar = password[i];
-
- if (repeatingCharsHash[currentChar]) {
- repeatingCharsHash[currentChar]++;
- if (repeatingCharsHash[currentChar] === maxRepeatingChars) return false;
- } else {
- repeatingCharsHash[currentChar] = 1;
- }
- }
-
- return true;
- },
- 'get-password-policy-mustContainAtLeastOneLowercase': (password: string) => /[a-z]/.test(password),
- 'get-password-policy-mustContainAtLeastOneUppercase': (password: string) => /[A-Z]/.test(password),
- 'get-password-policy-mustContainAtLeastOneNumber': (password: string) => /[0-9]/.test(password),
- 'get-password-policy-mustContainAtLeastOneSpecialCharacter': (password: string) => /[^A-Za-z0-9\s]/.test(password),
-};
+import { useSetting } from './useSetting';
type PasswordVerifications = { isValid: boolean; limit?: number; name: string }[];
-type PasswordPolicies = [key: string, value?: Record][];
-
-export const useVerifyPasswordByPolices = (policies?: PasswordPolicies) => {
- return useCallback(
- (password: string): PasswordVerifications => {
- if (!policies) {
- return [];
- }
- return policies
- .map(([name, rules]) => {
- if (name === 'get-password-policy-forbidRepeatingCharacters') return;
- const limit = rules && Object.values(rules)[0];
-
- return {
- name,
- isValid: password.length !== 0 && passwordVerificationsTemplate[name](password, limit),
- ...(limit && { limit }),
- };
- })
- .filter(Boolean) as PasswordVerifications;
- },
- [policies],
+export const useVerifyPassword = (password: string): PasswordVerifications => {
+ const enabled = Boolean(useSetting('Accounts_Password_Policy_Enabled'));
+ const minLength = Number(useSetting('Accounts_Password_Policy_MinLength'));
+ const maxLength = Number(useSetting('Accounts_Password_Policy_MaxLength'));
+ const forbidRepeatingCharacters = Boolean(useSetting('Accounts_Password_Policy_ForbidRepeatingCharacters'));
+ const forbidRepeatingCharactersCount = Number(useSetting('Accounts_Password_Policy_ForbidRepeatingCharactersCount'));
+ const mustContainAtLeastOneLowercase = Boolean(useSetting('Accounts_Password_Policy_AtLeastOneLowercase'));
+ const mustContainAtLeastOneUppercase = Boolean(useSetting('Accounts_Password_Policy_AtLeastOneUppercase'));
+ const mustContainAtLeastOneNumber = Boolean(useSetting('Accounts_Password_Policy_AtLeastOneNumber'));
+ const mustContainAtLeastOneSpecialCharacter = Boolean(useSetting('Accounts_Password_Policy_AtLeastOneSpecialCharacter'));
+
+ const validator = useMemo(
+ () =>
+ new PasswordPolicy({
+ enabled,
+ minLength,
+ maxLength,
+ forbidRepeatingCharacters,
+ forbidRepeatingCharactersCount,
+ mustContainAtLeastOneLowercase,
+ mustContainAtLeastOneUppercase,
+ mustContainAtLeastOneNumber,
+ mustContainAtLeastOneSpecialCharacter,
+ throwError: true,
+ }),
+ [
+ enabled,
+ minLength,
+ maxLength,
+ forbidRepeatingCharacters,
+ forbidRepeatingCharactersCount,
+ mustContainAtLeastOneLowercase,
+ mustContainAtLeastOneUppercase,
+ mustContainAtLeastOneNumber,
+ mustContainAtLeastOneSpecialCharacter,
+ ],
);
-};
-
-export const useVerifyPassword = (password: string): { data: PasswordVerifications; isLoading: boolean } => {
- const { data, isLoading } = usePasswordPolicy();
- const validator = useVerifyPasswordByPolices((data?.enabled && data?.policy) || undefined);
-
- return useMemo(
- () => ({
- data: validator(password),
- isLoading,
- }),
- [password, validator, isLoading],
- );
+ return useMemo(() => validator.sendValidationMessage(password || ''), [password, validator]);
};
diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts
index fca1dc0e4edb..d404dc921579 100644
--- a/packages/ui-contexts/src/index.ts
+++ b/packages/ui-contexts/src/index.ts
@@ -80,7 +80,6 @@ export { useUserRoom } from './hooks/useUserRoom';
export { useUserSubscription } from './hooks/useUserSubscription';
export { useUserSubscriptionByName } from './hooks/useUserSubscriptionByName';
export { useUserSubscriptions } from './hooks/useUserSubscriptions';
-export { usePasswordPolicy } from './hooks/usePasswordPolicy';
export { useVerifyPassword } from './hooks/useVerifyPassword';
export { useSelectedDevices } from './hooks/useSelectedDevices';
export { useDeviceConstraints } from './hooks/useDeviceConstraints';
diff --git a/yarn.lock b/yarn.lock
index c598977509bf..cde6d55626f8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8591,6 +8591,7 @@ __metadata:
"@rocket.chat/mp3-encoder": 0.24.0
"@rocket.chat/omnichannel-services": "workspace:^"
"@rocket.chat/onboarding-ui": next
+ "@rocket.chat/password-policies": "workspace:^"
"@rocket.chat/pdf-worker": "workspace:^"
"@rocket.chat/poplib": "workspace:^"
"@rocket.chat/presence": "workspace:^"
@@ -9053,6 +9054,20 @@ __metadata:
languageName: node
linkType: hard
+"@rocket.chat/password-policies@workspace:^, @rocket.chat/password-policies@workspace:packages/password-policies":
+ version: 0.0.0-use.local
+ resolution: "@rocket.chat/password-policies@workspace:packages/password-policies"
+ dependencies:
+ "@types/chai": ^4.3.5
+ "@types/jest": ~29.5.3
+ chai: ^4.3.7
+ eslint: ~8.45.0
+ jest: ~29.6.1
+ ts-jest: ~29.0.5
+ typescript: ~5.2.2
+ languageName: unknown
+ linkType: soft
+
"@rocket.chat/pdf-worker@workspace:^, @rocket.chat/pdf-worker@workspace:ee/packages/pdf-worker":
version: 0.0.0-use.local
resolution: "@rocket.chat/pdf-worker@workspace:ee/packages/pdf-worker"
@@ -9483,6 +9498,7 @@ __metadata:
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/emitter": next
"@rocket.chat/fuselage-hooks": next
+ "@rocket.chat/password-policies": "workspace:^"
"@rocket.chat/rest-typings": "workspace:^"
"@types/jest": ~29.5.3
"@types/react": ~17.0.62