diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index f912626c833e..42438be4ab7a 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -15,6 +15,7 @@ import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; import { validateEmailDomain } from '../lib'; +import { generatePassword } from '../lib/generatePassword'; import { passwordPolicy } from '../lib/passwordPolicy'; import { checkEmailAvailability } from './checkEmailAvailability'; import { checkUsernameAvailability } from './checkUsernameAvailability'; @@ -344,7 +345,7 @@ export const saveUser = async function (userId, userData) { if (userData.hasOwnProperty('setRandomPassword')) { if (userData.setRandomPassword) { - userData.password = passwordPolicy.generatePassword(); + userData.password = generatePassword(); userData.requirePasswordChange = true; sendPassword = true; } diff --git a/apps/meteor/app/lib/server/lib/generatePassword.ts b/apps/meteor/app/lib/server/lib/generatePassword.ts new file mode 100644 index 000000000000..bf8d2474b7a7 --- /dev/null +++ b/apps/meteor/app/lib/server/lib/generatePassword.ts @@ -0,0 +1,31 @@ +import generator from 'generate-password'; + +import { passwordPolicy } from './passwordPolicy'; + +export const generatePassword = (): string => { + const policies = passwordPolicy.getPasswordPolicy(); + + const maxLength: number = (policies.policy.find(([key]) => key === 'get-password-policy-maxLength')?.[1]?.maxLength as number) || -1; + const minLength: number = (policies.policy.find(([key]) => key === 'get-password-policy-minLength')?.[1]?.minLength as number) || -1; + + const length = Math.min(Math.max(minLength, 12), maxLength > 0 ? maxLength : Number.MAX_SAFE_INTEGER); + + if (policies.enabled) { + for (let i = 0; i < 10; i++) { + const password = generator.generate({ + length, + ...(policies.policy && { numbers: true }), + ...(policies.policy.some(([key]) => key === 'get-password-policy-mustContainAtLeastOneSpecialCharacter') && { symbols: true }), + ...(policies.policy.some(([key]) => key === 'get-password-policy-mustContainAtLeastOneLowercase') && { lowercase: true }), + ...(policies.policy.some(([key]) => key === 'get-password-policy-mustContainAtLeastOneUppercase') && { uppercase: true }), + strict: true, + }); + + if (passwordPolicy.validate(password)) { + return password; + } + } + } + + return generator.generate({ length: 17 }); +}; diff --git a/apps/meteor/app/lib/server/lib/passwordPolicy.js b/apps/meteor/app/lib/server/lib/passwordPolicy.js deleted file mode 100644 index 57490d1712c1..000000000000 --- a/apps/meteor/app/lib/server/lib/passwordPolicy.js +++ /dev/null @@ -1,32 +0,0 @@ -import { settings } from '../../../settings/server'; -import PasswordPolicy from './PasswordPolicyClass'; - -export const passwordPolicy = new PasswordPolicy(); - -settings.watch('Accounts_Password_Policy_Enabled', (value) => { - passwordPolicy.enabled = value; -}); -settings.watch('Accounts_Password_Policy_MinLength', (value) => { - passwordPolicy.minLength = value; -}); -settings.watch('Accounts_Password_Policy_MaxLength', (value) => { - passwordPolicy.maxLength = value; -}); -settings.watch('Accounts_Password_Policy_ForbidRepeatingCharacters', (value) => { - passwordPolicy.forbidRepeatingCharacters = value; -}); -settings.watch('Accounts_Password_Policy_ForbidRepeatingCharactersCount', (value) => { - passwordPolicy.forbidRepeatingCharactersCount = value; -}); -settings.watch('Accounts_Password_Policy_AtLeastOneLowercase', (value) => { - passwordPolicy.mustContainAtLeastOneLowercase = value; -}); -settings.watch('Accounts_Password_Policy_AtLeastOneUppercase', (value) => { - passwordPolicy.mustContainAtLeastOneUppercase = value; -}); -settings.watch('Accounts_Password_Policy_AtLeastOneNumber', (value) => { - passwordPolicy.mustContainAtLeastOneNumber = value; -}); -settings.watch('Accounts_Password_Policy_AtLeastOneSpecialCharacter', (value) => { - passwordPolicy.mustContainAtLeastOneSpecialCharacter = value; -}); diff --git a/apps/meteor/app/lib/server/lib/passwordPolicy.ts b/apps/meteor/app/lib/server/lib/passwordPolicy.ts new file mode 100644 index 000000000000..b40447ca56ab --- /dev/null +++ b/apps/meteor/app/lib/server/lib/passwordPolicy.ts @@ -0,0 +1,64 @@ +import { PasswordPolicy } from '@rocket.chat/password-policies'; + +import { settings } from '../../../settings/server'; + +const enabled = false; +const minLength = -1; +const maxLength = -1; +const forbidRepeatingCharacters = false; +const forbidRepeatingCharactersCount = 3; +const mustContainAtLeastOneLowercase = false; +const mustContainAtLeastOneUppercase = false; +const mustContainAtLeastOneNumber = false; +const mustContainAtLeastOneSpecialCharacter = false; + +export let passwordPolicy = new PasswordPolicy({ + enabled, + minLength, + maxLength, + forbidRepeatingCharacters, + forbidRepeatingCharactersCount, + mustContainAtLeastOneLowercase, + mustContainAtLeastOneUppercase, + mustContainAtLeastOneNumber, + mustContainAtLeastOneSpecialCharacter, + throwError: true, +}); + +settings.watchMultiple( + [ + 'Accounts_Password_Policy_Enabled', + 'Accounts_Password_Policy_MinLength', + 'Accounts_Password_Policy_MaxLength', + 'Accounts_Password_Policy_ForbidRepeatingCharacters', + 'Accounts_Password_Policy_ForbidRepeatingCharactersCount', + 'Accounts_Password_Policy_AtLeastOneLowercase', + 'Accounts_Password_Policy_AtLeastOneUppercase', + 'Accounts_Password_Policy_AtLeastOneNumber', + 'Accounts_Password_Policy_AtLeastOneSpecialCharacter', + ], + ([ + enabled, + minLength, + maxLength, + forbidRepeatingCharacters, + forbidRepeatingCharactersCount, + mustContainAtLeastOneLowercase, + mustContainAtLeastOneUppercase, + mustContainAtLeastOneNumber, + mustContainAtLeastOneSpecialCharacter, + ]) => { + passwordPolicy = new PasswordPolicy({ + enabled: Boolean(enabled), + minLength: Number(minLength), + maxLength: Number(maxLength), + forbidRepeatingCharacters: Boolean(forbidRepeatingCharacters), + forbidRepeatingCharactersCount: Number(forbidRepeatingCharactersCount), + mustContainAtLeastOneLowercase: Boolean(mustContainAtLeastOneLowercase), + mustContainAtLeastOneUppercase: Boolean(mustContainAtLeastOneUppercase), + mustContainAtLeastOneNumber: Boolean(mustContainAtLeastOneNumber), + mustContainAtLeastOneSpecialCharacter: Boolean(mustContainAtLeastOneSpecialCharacter), + throwError: true, + }); + }, +); diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.tsx index f1f3dde6aec0..7a95e7aae018 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.tsx @@ -65,7 +65,8 @@ const DeploymentCard = ({ info, statistics, instances }: DeploymentCardProps): R {t('Commit_details')} {t('github_HEAD')}: ({commit.hash ? commit.hash.slice(0, 9) : ''})
- {t('Branch')}: {commit.branch} + {t('Branch')}: {commit.branch}
+ {commit.subject}
{t('PID')} diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 4b4d1275656e..1e1d7e03ce02 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -257,6 +257,7 @@ "@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:^", diff --git a/apps/meteor/server/methods/getPasswordPolicy.ts b/apps/meteor/server/methods/getPasswordPolicy.ts index 7201999ff677..cc35f1cfb514 100644 --- a/apps/meteor/server/methods/getPasswordPolicy.ts +++ b/apps/meteor/server/methods/getPasswordPolicy.ts @@ -28,6 +28,9 @@ Meteor.methods({ method: 'getPasswordPolicy', }); } - return passwordPolicy.getPasswordPolicy(); + return passwordPolicy.getPasswordPolicy() as { + enabled: boolean; + policy: [name: TranslationKey, options?: Record][]; + }; }, }); diff --git a/apps/meteor/server/settings/accounts.ts b/apps/meteor/server/settings/accounts.ts index a7592829578d..ccc87b0ffd24 100644 --- a/apps/meteor/server/settings/accounts.ts +++ b/apps/meteor/server/settings/accounts.ts @@ -745,50 +745,60 @@ export const createAccountSettings = () => await this.section('Password_Policy', async function () { await this.add('Accounts_Password_Policy_Enabled', false, { type: 'boolean', + public: true, }); const enableQuery = { _id: 'Accounts_Password_Policy_Enabled', value: true, + public: true, }; await this.add('Accounts_Password_Policy_MinLength', 7, { type: 'int', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_MaxLength', -1, { type: 'int', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_ForbidRepeatingCharacters', true, { type: 'boolean', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_ForbidRepeatingCharactersCount', 3, { type: 'int', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_AtLeastOneLowercase', true, { type: 'boolean', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_AtLeastOneUppercase', true, { type: 'boolean', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_AtLeastOneNumber', true, { type: 'boolean', + public: true, enableQuery, }); await this.add('Accounts_Password_Policy_AtLeastOneSpecialCharacter', true, { type: 'boolean', + public: true, enableQuery, }); }); diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.js index 032d25018066..899eb0db7d5f 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.js @@ -5,7 +5,7 @@ import { getCredentials, request, methodCall, api, credentials } from '../../dat import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom } from '../../data/rooms.helper'; -import { createUser, deleteUser } from '../../data/users.helper.js'; +import { createUser, deleteUser, login } from '../../data/users.helper.js'; describe('Meteor.methods', function () { this.retries(0); @@ -2397,4 +2397,237 @@ describe('Meteor.methods', function () { }); }); }); + + describe('[@muteUserInRoom & @unmuteUserInRoom]', () => { + let rid = null; + let channelName = null; + let testUser = null; + let testUserCredentials = {}; + + before('create test user', async () => { + const username = `user.test.${Date.now()}`; + const email = `${username}@rocket.chat`; + + testUser = await createUser({ email, name: username, username, password: username, roles: ['user'] }); + }); + + before('create channel', async () => { + channelName = `methods-test-channel-${Date.now()}`; + rid = (await createRoom({ type: 'c', name: channelName, members: [testUser.username] })).body.channel._id; + }); + + before('login testUser', async () => { + testUserCredentials = await login(testUser.username, testUser.username); + }); + + describe('-> standard room', () => { + describe('- when muting a user in a standard room', () => { + it('should mute an user in a standard room', async () => { + await request + .post(methodCall('muteUserInRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'muteUserInRoom', + params: [{ rid, username: testUser.username }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data).not.to.have.a.property('error'); + }); + }); + + it('muted user should not be able to send message', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').that.is.a('string'); + expect(res.body.error).to.equal('You_have_been_muted'); + }); + }); + }); + + describe('- when unmuting a user in a standard room', () => { + it('should unmute an user in a standard room', async () => { + await request + .post(methodCall('unmuteUserInRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'unmuteUserInRoom', + params: [{ rid, username: testUser.username }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data).not.to.have.a.property('error'); + }); + }); + + it('unmuted user should be able to send message', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }); + }); + }); + + describe('-> read-only room', () => { + before('set room to read-only', async () => { + await request + .post(api('channels.setReadOnly')) + .set(credentials) + .send({ + roomId: rid, + readOnly: true, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it('should not allow an user to send messages', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').that.is.a('string'); + expect(res.body.error).to.equal(`You can't send messages because the room is readonly.`); + }); + }); + + describe('- when unmuting a user in a read-only room', () => { + it('should unmute an user in a read-only room', async () => { + await request + .post(methodCall('unmuteUserInRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'unmuteUserInRoom', + params: [{ rid, username: testUser.username }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data).not.to.have.a.property('error'); + }); + }); + + it('unmuted user in read-only room should be able to send message', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }); + }); + + describe('- when muting a user in a read-only room', () => { + it('should mute an user in a read-only room', async () => { + await request + .post(methodCall('muteUserInRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'muteUserInRoom', + params: [{ rid, username: testUser.username }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data).not.to.have.a.property('error'); + }); + }); + + it('muted user in read-only room should not be able to send message', async () => { + await request + .post(api('chat.sendMessage')) + .set(testUserCredentials) + .send({ + message: { + msg: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').that.is.a('string'); + }); + }); + }); + }); + }); }); diff --git a/apps/meteor/tests/unit/app/lib/server.tests.js b/apps/meteor/tests/unit/app/lib/server.tests.js deleted file mode 100644 index f77b5a4d1cb8..000000000000 --- a/apps/meteor/tests/unit/app/lib/server.tests.js +++ /dev/null @@ -1,277 +0,0 @@ -import { expect } from 'chai'; -import proxyquire from 'proxyquire'; - -const { default: PasswordPolicyClass } = proxyquire.noCallThru().load('../../../../app/lib/server/lib/PasswordPolicyClass', { - 'meteor/meteor': { - Meteor: { - absoluteUrl() { - return 'http://localhost:3000/'; - }, - }, - }, - '@rocket.chat/random': { - Random: { - id: () => 1, - }, - }, -}); - -describe('PasswordPolicyClass', () => { - describe('Default options', () => { - const passwordPolice = new PasswordPolicyClass(); - it('should be disabled', () => { - expect(passwordPolice.enabled).to.be.equal(false); - }); - it('should have minLength = -1', () => { - expect(passwordPolice.minLength).to.be.equal(-1); - }); - it('should have maxLength = -1', () => { - expect(passwordPolice.maxLength).to.be.equal(-1); - }); - it('should have forbidRepeatingCharacters = false', () => { - expect(passwordPolice.forbidRepeatingCharacters).to.be.false; - }); - it('should have forbidRepeatingCharactersCount = 3', () => { - expect(passwordPolice.forbidRepeatingCharactersCount).to.be.equal(3); - }); - it('should have mustContainAtLeastOneLowercase = false', () => { - expect(passwordPolice.mustContainAtLeastOneLowercase).to.be.false; - }); - it('should have mustContainAtLeastOneUppercase = false', () => { - expect(passwordPolice.mustContainAtLeastOneUppercase).to.be.false; - }); - it('should have mustContainAtLeastOneNumber = false', () => { - expect(passwordPolice.mustContainAtLeastOneNumber).to.be.false; - }); - it('should have mustContainAtLeastOneSpecialCharacter = false', () => { - expect(passwordPolice.mustContainAtLeastOneSpecialCharacter).to.be.false; - }); - - describe('Password tests with default options', () => { - it('should allow all passwords', () => { - const passwordPolice = new PasswordPolicyClass({ throwError: false }); - expect(passwordPolice.validate()).to.be.equal(false); - expect(passwordPolice.validate('')).to.be.equal(false); - expect(passwordPolice.validate(' ')).to.be.equal(false); - expect(passwordPolice.validate('a')).to.be.equal(true); - expect(passwordPolice.validate('aaaaaaaaa')).to.be.equal(true); - }); - }); - }); - - describe('Password tests with options', () => { - it('should not allow non string or empty', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - throwError: false, - }); - expect(passwordPolice.validate()).to.be.false; - expect(passwordPolice.validate(1)).to.be.false; - expect(passwordPolice.validate(true)).to.be.false; - expect(passwordPolice.validate(new Date())).to.be.false; - expect(passwordPolice.validate(new Function())).to.be.false; - expect(passwordPolice.validate('')).to.be.false; - }); - - it('should restrict by minLength', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - minLength: 5, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.false; - expect(passwordPolice.validate('1234')).to.be.false; - expect(passwordPolice.validate('12345')).to.be.true; - expect(passwordPolice.validate(' ')).to.be.false; - }); - - it('should restrict by maxLength', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - maxLength: 5, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.true; - expect(passwordPolice.validate('12345')).to.be.true; - expect(passwordPolice.validate('123456')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - }); - - it('should allow repeated characters', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - forbidRepeatingCharacters: false, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.true; - expect(passwordPolice.validate('12345')).to.be.true; - expect(passwordPolice.validate('123456')).to.be.true; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('11111111111111')).to.be.true; - }); - - it('should restrict repeated characters', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - forbidRepeatingCharacters: true, - forbidRepeatingCharactersCount: 3, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.true; - expect(passwordPolice.validate('11')).to.be.true; - expect(passwordPolice.validate('111')).to.be.true; - expect(passwordPolice.validate('1111')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.true; - }); - - it('should restrict repeated characters customized', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - forbidRepeatingCharacters: true, - forbidRepeatingCharactersCount: 5, - throwError: false, - }); - - expect(passwordPolice.validate('1')).to.be.true; - expect(passwordPolice.validate('11')).to.be.true; - expect(passwordPolice.validate('111')).to.be.true; - expect(passwordPolice.validate('1111')).to.be.true; - expect(passwordPolice.validate('11111')).to.be.true; - expect(passwordPolice.validate('111111')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.true; - }); - - it('should contain one lowercase', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - mustContainAtLeastOneLowercase: true, - throwError: false, - }); - - expect(passwordPolice.validate('a')).to.be.true; - expect(passwordPolice.validate('aa')).to.be.true; - expect(passwordPolice.validate('A')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.false; - expect(passwordPolice.validate('AAAAA')).to.be.false; - expect(passwordPolice.validate('AAAaAAA')).to.be.true; - }); - - it('should contain one uppercase', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - mustContainAtLeastOneUppercase: true, - throwError: false, - }); - - expect(passwordPolice.validate('a')).to.be.false; - expect(passwordPolice.validate('aa')).to.be.false; - expect(passwordPolice.validate('A')).to.be.true; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.false; - expect(passwordPolice.validate('AAAAA')).to.be.true; - expect(passwordPolice.validate('AAAaAAA')).to.be.true; - }); - - it('should contain one number', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - mustContainAtLeastOneNumber: true, - throwError: false, - }); - - expect(passwordPolice.validate('a')).to.be.false; - expect(passwordPolice.validate('aa')).to.be.false; - expect(passwordPolice.validate('A')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.true; - expect(passwordPolice.validate('AAAAA')).to.be.false; - expect(passwordPolice.validate('AAAaAAA')).to.be.false; - expect(passwordPolice.validate('AAAa1AAA')).to.be.true; - }); - - it('should contain one special character', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - mustContainAtLeastOneSpecialCharacter: true, - throwError: false, - }); - - expect(passwordPolice.validate('a')).to.be.false; - expect(passwordPolice.validate('aa')).to.be.false; - expect(passwordPolice.validate('A')).to.be.false; - expect(passwordPolice.validate(' ')).to.be.false; - expect(passwordPolice.validate('123456')).to.be.false; - expect(passwordPolice.validate('AAAAA')).to.be.false; - expect(passwordPolice.validate('AAAaAAA')).to.be.false; - expect(passwordPolice.validate('AAAa1AAA')).to.be.false; - expect(passwordPolice.validate('AAAa@AAA')).to.be.true; - }); - }); - - describe('Password generator', () => { - it('should return a random password', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - throwError: false, - }); - - expect(passwordPolice.generatePassword()).to.not.be.undefined; - }); - }); - - describe('Password Policy', () => { - it('should return a correct password policy', () => { - const passwordPolice = new PasswordPolicyClass({ - enabled: true, - throwError: false, - minLength: 10, - maxLength: 20, - forbidRepeatingCharacters: true, - forbidRepeatingCharactersCount: 4, - mustContainAtLeastOneLowercase: true, - mustContainAtLeastOneUppercase: true, - mustContainAtLeastOneNumber: true, - mustContainAtLeastOneSpecialCharacter: true, - }); - - const policy = passwordPolice.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 passwordPolice = new PasswordPolicyClass({ - enabled: false, - }); - - const policy = passwordPolice.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 passwordPolice = new PasswordPolicyClass({ - enabled: true, - }); - - const policy = passwordPolice.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/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 83f1eb282c87..9a3f9ef9c582 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -16,6 +16,9 @@ COPY ./packages/core-typings/dist packages/core-typings/dist COPY ./packages/rest-typings/package.json packages/rest-typings/package.json COPY ./packages/rest-typings/dist packages/rest-typings/dist +COPY ./packages/password-policies/package.json packages/password-policies/package.json +COPY ./packages/password-policies/dist packages/password-policies/dist + COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json COPY ./packages/ui-contexts/dist packages/ui-contexts/dist diff --git a/packages/livechat/src/components/FilesDropTarget/index.js b/packages/livechat/src/components/FilesDropTarget/index.js deleted file mode 100644 index 589cdd8ecde9..000000000000 --- a/packages/livechat/src/components/FilesDropTarget/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import { Component } from 'preact'; - -import { createClassName } from '../../helpers/createClassName'; -import styles from './styles.scss'; - -const escapeForRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - -export class FilesDropTarget extends Component { - state = { - dragLevel: 0, - }; - - handleInputRef = (ref) => { - this.input = ref; - }; - - handleDragOver = (event) => { - event.preventDefault(); - }; - - handleDragEnter = (event) => { - event.preventDefault(); - this.setState({ dragLevel: this.state.dragLevel + 1 }); - }; - - handleDragLeave = (event) => { - event.preventDefault(); - this.setState({ dragLevel: this.state.dragLevel - 1 }); - }; - - handleDrop = (event) => { - event.preventDefault(); - - let { dragLevel } = this.state; - if (dragLevel === 0) { - return; - } - - dragLevel = 0; - this.setState({ dragLevel }); - - this.handleUpload(event.dataTransfer.files); - }; - - handleInputChange = (event) => { - this.handleUpload(event.currentTarget.files); - }; - - handleUpload = (files) => { - const { accept, multiple, onUpload } = this.props; - - if (!onUpload) { - return; - } - - let filteredFiles = Array.from(files); - - if (accept) { - const acceptMatchers = accept.split(',').map((acceptString) => { - if (acceptString.charAt(0) === '.') { - return ({ name }) => new RegExp(`${escapeForRegExp(acceptString)}$`, 'i').test(name); - } - - const matchTypeOnly = /^(.+)\/\*$/i.exec(acceptString); - if (matchTypeOnly) { - return ({ type }) => new RegExp(`^${escapeForRegExp(matchTypeOnly[1])}/.*$`, 'i').test(type); - } - - return ({ type }) => 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); - }; - - browse = () => { - this.input.click(); - }; - - render = ({ overlayed, overlayText, accept, multiple, className, style = {}, children }, { dragLevel }) => ( -
0 }, [className])} - style={style} - > - - {children} -
- ); -} diff --git a/packages/livechat/src/components/FilesDropTarget/index.tsx b/packages/livechat/src/components/FilesDropTarget/index.tsx new file mode 100644 index 000000000000..3e9935c37565 --- /dev/null +++ b/packages/livechat/src/components/FilesDropTarget/index.tsx @@ -0,0 +1,122 @@ +import type { ComponentChildren, Ref } from 'preact'; +import type { TargetedEvent } from 'preact/compat'; +import { useState } from 'preact/hooks'; +import type { JSXInternal } from 'preact/src/jsx'; + +import { createClassName } from '../../helpers/createClassName'; +import styles from './styles.scss'; + +const escapeForRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +type FilesDropTargetProps = { + overlayed?: boolean; + overlayText?: string; + accept?: string; + multiple?: boolean; + className?: string; + style?: JSXInternal.CSSProperties; + children?: ComponentChildren; + inputRef?: Ref; + onUpload?: (files: File[]) => void; +}; + +export const FilesDropTarget = ({ + overlayed, + overlayText, + accept, + multiple, + className, + style = {}, + children, + inputRef, + onUpload, +}: FilesDropTargetProps) => { + const [dragLevel, setDragLevel] = useState(0); + + const handleDragOver = (event: DragEvent) => { + event.preventDefault(); + }; + + const handleDragEnter = (event: DragEvent) => { + event.preventDefault(); + setDragLevel(dragLevel + 1); + }; + + const handleDragLeave = (event: DragEvent) => { + event.preventDefault(); + setDragLevel(dragLevel - 1); + }; + + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + + if (dragLevel === 0 || !event?.dataTransfer?.files?.length) { + return; + } + + setDragLevel(0); + + handleUpload(event?.dataTransfer?.files); + }; + + const handleInputChange = (event: TargetedEvent) => { + 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