snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
37
packages/password-policies/CHANGELOG.md
Normal file
37
packages/password-policies/CHANGELOG.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# @rocket.chat/password-policies
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
|
||||
|
||||
## 0.1.1-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ([#37327](https://github.com/RocketChat/Rocket.Chat/pull/37327)) Adds complexity requirements to end-to-end encryption passphrase
|
||||
|
||||
## 0.1.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ([#37327](https://github.com/RocketChat/Rocket.Chat/pull/37327)) Adds complexity requirements to end-to-end encryption passphrase
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#31138](https://github.com/RocketChat/Rocket.Chat/pull/31138)) feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo
|
||||
|
||||
## 0.0.2-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b223cbde14: feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo
|
||||
6
packages/password-policies/jest.config.ts
Normal file
6
packages/password-policies/jest.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import server from '@rocket.chat/jest-presets/server';
|
||||
import type { Config } from 'jest';
|
||||
|
||||
export default {
|
||||
preset: server.preset,
|
||||
} satisfies Config;
|
||||
30
packages/password-policies/package.json
Normal file
30
packages/password-policies/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@rocket.chat/password-policies",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc",
|
||||
"dev": "tsc --watch --preserveWatchOutput",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"test": "jest",
|
||||
"testunit": "jest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rocket.chat/jest-presets": "workspace:~",
|
||||
"@rocket.chat/tsconfig": "workspace:*",
|
||||
"@types/jest": "~30.0.0",
|
||||
"eslint": "~9.39.5",
|
||||
"jest": "~30.2.0",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
219
packages/password-policies/src/PasswordPolicy.spec.ts
Normal file
219
packages/password-policies/src/PasswordPolicy.spec.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { PasswordPolicy } from './PasswordPolicy';
|
||||
|
||||
describe('Password tests with default options', () => {
|
||||
it('should allow all passwords', () => {
|
||||
const passwordPolicy = new PasswordPolicy({ throwError: false });
|
||||
expect(passwordPolicy.validate(null as any)).toBe(false);
|
||||
expect(passwordPolicy.validate(undefined as any)).toBe(false);
|
||||
expect(passwordPolicy.validate('')).toBe(false);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
expect(passwordPolicy.validate('a')).toBe(true);
|
||||
expect(passwordPolicy.validate('aaaaaaaaa')).toBe(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)).toBe(false);
|
||||
expect(passwordPolicy.validate(undefined as any)).toBe(false);
|
||||
expect(passwordPolicy.validate(1 as any)).toBe(false);
|
||||
expect(passwordPolicy.validate(true as any)).toBe(false);
|
||||
expect(passwordPolicy.validate(new Date() as any)).toBe(false);
|
||||
expect(passwordPolicy.validate(new Function() as any)).toBe(false);
|
||||
expect(passwordPolicy.validate('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should restrict by minLength', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
minLength: 5,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('1')).toBe(false);
|
||||
expect(passwordPolicy.validate('1234')).toBe(false);
|
||||
expect(passwordPolicy.validate('12345')).toBe(true);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
});
|
||||
|
||||
it('should restrict by maxLength', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
maxLength: 5,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('1')).toBe(true);
|
||||
expect(passwordPolicy.validate('12345')).toBe(true);
|
||||
expect(passwordPolicy.validate('123456')).toBe(false);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow repeated characters', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
forbidRepeatingCharacters: false,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('1')).toBe(true);
|
||||
expect(passwordPolicy.validate('12345')).toBe(true);
|
||||
expect(passwordPolicy.validate('123456')).toBe(true);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
expect(passwordPolicy.validate('11111111111111')).toBe(true);
|
||||
});
|
||||
|
||||
it('should restrict repeated characters', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
forbidRepeatingCharacters: true,
|
||||
forbidRepeatingCharactersCount: 3,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('1')).toBe(true);
|
||||
expect(passwordPolicy.validate('11')).toBe(true);
|
||||
expect(passwordPolicy.validate('111')).toBe(true);
|
||||
expect(passwordPolicy.validate('1111')).toBe(false);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
expect(passwordPolicy.validate('123456')).toBe(true);
|
||||
});
|
||||
|
||||
it('should restrict repeated characters customized', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
forbidRepeatingCharacters: true,
|
||||
forbidRepeatingCharactersCount: 5,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('1')).toBe(true);
|
||||
expect(passwordPolicy.validate('11')).toBe(true);
|
||||
expect(passwordPolicy.validate('111')).toBe(true);
|
||||
expect(passwordPolicy.validate('1111')).toBe(true);
|
||||
expect(passwordPolicy.validate('11111')).toBe(true);
|
||||
expect(passwordPolicy.validate('111111')).toBe(false);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
expect(passwordPolicy.validate('123456')).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain one lowercase', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
mustContainAtLeastOneLowercase: true,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('a')).toBe(true);
|
||||
expect(passwordPolicy.validate('aa')).toBe(true);
|
||||
expect(passwordPolicy.validate('A')).toBe(false);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
expect(passwordPolicy.validate('123456')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAAA')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAaAAA')).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain one uppercase', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
mustContainAtLeastOneUppercase: true,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('a')).toBe(false);
|
||||
expect(passwordPolicy.validate('aa')).toBe(false);
|
||||
expect(passwordPolicy.validate('A')).toBe(true);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
expect(passwordPolicy.validate('123456')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAAA')).toBe(true);
|
||||
expect(passwordPolicy.validate('AAAaAAA')).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain one number', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
mustContainAtLeastOneNumber: true,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('a')).toBe(false);
|
||||
expect(passwordPolicy.validate('aa')).toBe(false);
|
||||
expect(passwordPolicy.validate('A')).toBe(false);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
expect(passwordPolicy.validate('123456')).toBe(true);
|
||||
expect(passwordPolicy.validate('AAAAA')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAaAAA')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAa1AAA')).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain one special character', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: true,
|
||||
mustContainAtLeastOneSpecialCharacter: true,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(passwordPolicy.validate('a')).toBe(false);
|
||||
expect(passwordPolicy.validate('aa')).toBe(false);
|
||||
expect(passwordPolicy.validate('A')).toBe(false);
|
||||
expect(passwordPolicy.validate(' ')).toBe(false);
|
||||
expect(passwordPolicy.validate('123456')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAAA')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAaAAA')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAa1AAA')).toBe(false);
|
||||
expect(passwordPolicy.validate('AAAa@AAA')).toBe(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).not.toBe(undefined);
|
||||
expect(policy.enabled).toBe(true);
|
||||
expect(policy.policy.length).toBe(8);
|
||||
expect(policy.policy[0][0]).toBe('get-password-policy-minLength');
|
||||
expect(policy.policy[0][1]?.minLength).toBe(10);
|
||||
});
|
||||
|
||||
it('should return correct values if policy is disabled', () => {
|
||||
const passwordPolicy = new PasswordPolicy({
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const policy = passwordPolicy.getPasswordPolicy();
|
||||
|
||||
expect(policy.enabled).toBe(false);
|
||||
expect(policy.policy.length).toBe(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).toBe(true);
|
||||
// even when no policy is specified, forbidRepeatingCharactersCount is still configured
|
||||
// since its default value is 3
|
||||
expect(policy.policy.length).toBe(1);
|
||||
});
|
||||
});
|
||||
287
packages/password-policies/src/PasswordPolicy.ts
Normal file
287
packages/password-policies/src/PasswordPolicy.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { PasswordPolicyError } from './PasswordPolicyError';
|
||||
|
||||
type PasswordPolicyMap = {
|
||||
minLength: number;
|
||||
maxLength: number;
|
||||
forbidRepeatingCharacters: boolean;
|
||||
forbidRepeatingCharactersCount: number;
|
||||
mustContainAtLeastOneLowercase: boolean;
|
||||
mustContainAtLeastOneUppercase: boolean;
|
||||
mustContainAtLeastOneNumber: boolean;
|
||||
mustContainAtLeastOneSpecialCharacter: boolean;
|
||||
};
|
||||
|
||||
type PasswordPolicyKey = keyof PasswordPolicyMap;
|
||||
type PasswordPolicyName<K extends PasswordPolicyKey> = `get-password-policy-${K}`;
|
||||
|
||||
type PasswordPolicyParametersEntry = {
|
||||
[K in PasswordPolicyKey]: PasswordPolicyMap[K] extends number
|
||||
? [PasswordPolicyName<K>, Record<K, PasswordPolicyMap[K]>]
|
||||
: [PasswordPolicyName<K>];
|
||||
}[PasswordPolicyKey];
|
||||
|
||||
type PasswordPolicyType<Entry = PasswordPolicyParametersEntry> = {
|
||||
enabled: boolean;
|
||||
policy: Entry[];
|
||||
};
|
||||
|
||||
export type PasswordPolicyOptions = Partial<
|
||||
PasswordPolicyMap & {
|
||||
enabled: boolean;
|
||||
throwError: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export type PasswordPolicyValidation = {
|
||||
[K in PasswordPolicyKey]: PasswordPolicyMap[K] extends number
|
||||
? { name: PasswordPolicyName<K>; limit: number }
|
||||
: { name: PasswordPolicyName<K> };
|
||||
}[PasswordPolicyKey] & { isValid: boolean };
|
||||
|
||||
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;
|
||||
|
||||
constructor({
|
||||
enabled = false,
|
||||
minLength = -1,
|
||||
maxLength = -1,
|
||||
forbidRepeatingCharacters = false,
|
||||
forbidRepeatingCharactersCount = 3, // the regex is this number minus one
|
||||
mustContainAtLeastOneLowercase = false, // /[A-Z]{3,}/ could do this instead of at least one
|
||||
mustContainAtLeastOneUppercase = false,
|
||||
mustContainAtLeastOneNumber = false,
|
||||
mustContainAtLeastOneSpecialCharacter = false,
|
||||
throwError = true,
|
||||
}: PasswordPolicyOptions) {
|
||||
this.enabled = enabled;
|
||||
this.minLength = minLength;
|
||||
this.maxLength = maxLength;
|
||||
this.forbidRepeatingCharacters = forbidRepeatingCharacters;
|
||||
this.forbidRepeatingCharactersCount = forbidRepeatingCharactersCount;
|
||||
this.mustContainAtLeastOneLowercase = mustContainAtLeastOneLowercase;
|
||||
this.mustContainAtLeastOneUppercase = mustContainAtLeastOneUppercase;
|
||||
this.mustContainAtLeastOneNumber = mustContainAtLeastOneNumber;
|
||||
this.mustContainAtLeastOneSpecialCharacter = mustContainAtLeastOneSpecialCharacter;
|
||||
this.throwError = throwError;
|
||||
|
||||
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: string,
|
||||
message: string,
|
||||
reasons?: {
|
||||
error: string;
|
||||
message: string;
|
||||
}[],
|
||||
) {
|
||||
if (this.throwError) {
|
||||
throw new PasswordPolicyError(message, error, reasons);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
sendValidationMessage(password: string): PasswordPolicyValidation[] {
|
||||
const validationReturn: PasswordPolicyValidation[] = [];
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
if (!this.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.minLength >= 1 && password.length < this.minLength) {
|
||||
reasons.push({
|
||||
error: 'error-password-policy-not-met-minLength',
|
||||
message: 'The password does not meet the minimum length password policy.',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.maxLength >= 1 && password.length > this.maxLength) {
|
||||
reasons.push({
|
||||
error: 'error-password-policy-not-met-maxLength',
|
||||
message: 'The password does not meet the maximum length password policy.',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.forbidRepeatingCharacters && this.regex.forbiddingRepeatingCharacters.test(password)) {
|
||||
reasons.push({
|
||||
error: 'error-password-policy-not-met-repeatingCharacters',
|
||||
message: 'The password contains repeating characters which is against the password policy.',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mustContainAtLeastOneLowercase && !this.regex.mustContainAtLeastOneLowercase.test(password)) {
|
||||
reasons.push({
|
||||
error: 'error-password-policy-not-met-oneLowercase',
|
||||
message: 'The password does not contain at least one lowercase character which is against the password policy.',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mustContainAtLeastOneUppercase && !this.regex.mustContainAtLeastOneUppercase.test(password)) {
|
||||
reasons.push({
|
||||
error: 'error-password-policy-not-met-oneUppercase',
|
||||
message: 'The password does not contain at least one uppercase character which is against the password policy.',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mustContainAtLeastOneNumber && !this.regex.mustContainAtLeastOneNumber.test(password)) {
|
||||
reasons.push({
|
||||
error: 'error-password-policy-not-met-oneNumber',
|
||||
message: 'The password does not contain at least one numerical character which is against the password policy.',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mustContainAtLeastOneSpecialCharacter && !this.regex.mustContainAtLeastOneSpecialCharacter.test(password)) {
|
||||
reasons.push({
|
||||
error: 'error-password-policy-not-met-oneSpecial',
|
||||
message: 'The password does not contain at least one special character which is against the password policy.',
|
||||
});
|
||||
}
|
||||
|
||||
if (reasons.length) {
|
||||
return this.error('error-password-policy-not-met', `The password provided does not meet the server's password policy.`, reasons);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getPasswordPolicy(): PasswordPolicyType<[name: string, params?: Record<string, number | boolean>]> {
|
||||
const data: PasswordPolicyType = {
|
||||
enabled: false,
|
||||
policy: [],
|
||||
};
|
||||
|
||||
if (this.enabled) {
|
||||
data.enabled = true;
|
||||
if (this.minLength >= 1) {
|
||||
data.policy.push(['get-password-policy-minLength', { minLength: this.minLength }]);
|
||||
}
|
||||
if (this.maxLength >= 1) {
|
||||
data.policy.push(['get-password-policy-maxLength', { maxLength: this.maxLength }]);
|
||||
}
|
||||
if (this.forbidRepeatingCharacters) {
|
||||
data.policy.push(['get-password-policy-forbidRepeatingCharacters']);
|
||||
}
|
||||
if (this.forbidRepeatingCharactersCount) {
|
||||
data.policy.push([
|
||||
'get-password-policy-forbidRepeatingCharactersCount',
|
||||
{ forbidRepeatingCharactersCount: this.forbidRepeatingCharactersCount },
|
||||
]);
|
||||
}
|
||||
if (this.mustContainAtLeastOneLowercase) {
|
||||
data.policy.push(['get-password-policy-mustContainAtLeastOneLowercase']);
|
||||
}
|
||||
if (this.mustContainAtLeastOneUppercase) {
|
||||
data.policy.push(['get-password-policy-mustContainAtLeastOneUppercase']);
|
||||
}
|
||||
if (this.mustContainAtLeastOneNumber) {
|
||||
data.policy.push(['get-password-policy-mustContainAtLeastOneNumber']);
|
||||
}
|
||||
if (this.mustContainAtLeastOneSpecialCharacter) {
|
||||
data.policy.push(['get-password-policy-mustContainAtLeastOneSpecialCharacter']);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
11
packages/password-policies/src/PasswordPolicyError.ts
Normal file
11
packages/password-policies/src/PasswordPolicyError.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
1
packages/password-policies/src/index.ts
Normal file
1
packages/password-policies/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { PasswordPolicy, type PasswordPolicyOptions, type PasswordPolicyValidation } from './PasswordPolicy';
|
||||
8
packages/password-policies/tsconfig.json
Normal file
8
packages/password-policies/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@rocket.chat/tsconfig/client.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user