From 375530d876fb3f5d861decb7129d5065c1ccf09f Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Sun, 24 Nov 2024 21:00:08 +0200 Subject: [PATCH 1/6] fix: return matches as array --- src/matchers/lowercaseMatcher.ts | 6 ++++-- src/matchers/numberMatcher.ts | 6 ++++-- src/matchers/specialMatcher.ts | 6 ++++-- src/matchers/uppercaseMatcher.ts | 6 ++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/matchers/lowercaseMatcher.ts b/src/matchers/lowercaseMatcher.ts index c807271..c0078e8 100644 --- a/src/matchers/lowercaseMatcher.ts +++ b/src/matchers/lowercaseMatcher.ts @@ -3,12 +3,14 @@ import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; export const lowercaseMatcher: Matcher = { Matching: class LowercaseMatcher { match({ password }: { password: string }): Match[] { + const matches: Match[] = []; for (const char of password) { if (char >= 'a' && char <= 'z') { - return []; + return matches; } } - return [{ pattern: 'lowercase', token: password, i: 0, j: password.length - 1 }]; + matches.push({ pattern: 'lowercase', token: password, i: 0, j: password.length - 1 }); + return matches; } }, feedback(_match) { diff --git a/src/matchers/numberMatcher.ts b/src/matchers/numberMatcher.ts index 54de55c..2f652d6 100644 --- a/src/matchers/numberMatcher.ts +++ b/src/matchers/numberMatcher.ts @@ -3,12 +3,14 @@ import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; export const numberMatcher: Matcher = { Matching: class NumberMatcher { match({ password }: { password: string }): Match[] { + const matches: Match[] = []; for (const char of password) { if (char >= '0' && char <= '9') { - return []; + return matches; } } - return [{ pattern: 'number', token: password, i: 0, j: password.length - 1 }]; + matches.push({ pattern: 'number', token: password, i: 0, j: password.length - 1 }); + return matches; } }, feedback(_match) { diff --git a/src/matchers/specialMatcher.ts b/src/matchers/specialMatcher.ts index 6356697..834ed98 100644 --- a/src/matchers/specialMatcher.ts +++ b/src/matchers/specialMatcher.ts @@ -3,13 +3,15 @@ import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; export const specialMatcher: Matcher = { Matching: class SpecialMatcher { match({ password }: { password: string }): Match[] { + const matches: Match[] = []; const specialChars = "!@#$%^&*()_+[]{}|;:',.<>?/"; for (const char of password) { if (specialChars.includes(char)) { - return []; + return matches; } } - return [{ pattern: 'special', token: password, i: 0, j: password.length - 1 }]; + matches.push({ pattern: 'special', token: password, i: 0, j: password.length - 1 }); + return matches; } }, feedback(_match) { diff --git a/src/matchers/uppercaseMatcher.ts b/src/matchers/uppercaseMatcher.ts index 830fc75..9ed1518 100644 --- a/src/matchers/uppercaseMatcher.ts +++ b/src/matchers/uppercaseMatcher.ts @@ -3,12 +3,14 @@ import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; export const uppercaseMatcher: Matcher = { Matching: class UppercaseMatcher { match({ password }: { password: string }): Match[] { + const matches: Match[] = []; for (const char of password) { if (char >= 'A' && char <= 'Z') { - return []; + return matches; } } - return [{ pattern: 'uppercase', token: password, i: 0, j: password.length - 1 }]; + matches.push({ pattern: 'uppercase', token: password, i: 0, j: password.length - 1 }); + return matches; } }, feedback(_match) { From eb2b91b72d1c15ce4bc9dd0f68afa18b887b7bac Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Sun, 24 Nov 2024 21:00:24 +0200 Subject: [PATCH 2/6] fix: add min length matcher --- src/index.ts | 1 + src/matchers/minLengthMatcher.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/matchers/minLengthMatcher.ts diff --git a/src/index.ts b/src/index.ts index 5c067cf..02cde8e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export { uppercaseMatcher } from './matchers/uppercaseMatcher'; +export { minLengthMatcher } from './matchers/minLengthMatcher'; export { lowercaseMatcher } from './matchers/lowercaseMatcher'; export { numberMatcher } from './matchers/numberMatcher'; export { specialMatcher } from './matchers/specialMatcher'; diff --git a/src/matchers/minLengthMatcher.ts b/src/matchers/minLengthMatcher.ts new file mode 100644 index 0000000..50e05bb --- /dev/null +++ b/src/matchers/minLengthMatcher.ts @@ -0,0 +1,19 @@ +import { Matcher, Match } from '@zxcvbn-ts/core/dist/types'; + +export const minLengthMatcher = (minLength: number): Matcher => ({ + Matching: class MinLengthMatcher { + match({ password }: { password: string }): Match[] { + const matches: Match[] = []; + if (password.length < minLength) { + matches.push({ pattern: 'minLength', token: password, i: 0, j: password.length - 1 }); + } + return matches; + } + }, + feedback(_match) { + return { warning: `Password must be at least ${minLength} characters long.`, suggestions: [] }; + }, + scoring(_match) { + return -100; + }, +}); From 3fa9137abcb1c3aa2cff52044994f4913d77dbfd Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Sun, 24 Nov 2024 21:04:43 +0200 Subject: [PATCH 3/6] tests: min length tests + new tests suites order --- tests/unit/matchers.test.ts | 121 +++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 43 deletions(-) diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index a87e9f1..fff411b 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -1,85 +1,120 @@ import 'mocha'; import { expect } from 'chai'; -import { uppercaseMatcher, lowercaseMatcher, numberMatcher, specialMatcher } from '../../src'; +import { uppercaseMatcher, lowercaseMatcher, numberMatcher, specialMatcher, minLengthMatcher } from '../../src'; -describe('character requirements matchers and feedback', () => { +describe('uppercaseMatcher', () => { it('should return a match for missing uppercase letters', () => { const result = uppercaseMatcher.Matching.prototype.match({ password: 'password123!' }); expect(result).to.deep.include({ pattern: 'uppercase', token: 'password123!', i: 0, j: 11 }); }); - it('should return a match for missing lowercase letters', () => { - const result = lowercaseMatcher.Matching.prototype.match({ password: 'PASSWORD123!' }); - expect(result).to.deep.include({ pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11 }); - }); - - it('should return a match for missing numbers', () => { - const result = numberMatcher.Matching.prototype.match({ password: 'Password!' }); - expect(result).to.deep.include({ pattern: 'number', token: 'Password!', i: 0, j: 8 }); - }); - - it('should return a match for missing special characters', () => { - const result = specialMatcher.Matching.prototype.match({ password: 'Password123' }); - expect(result).to.deep.include({ pattern: 'special', token: 'Password123', i: 0, j: 10 }); - }); - - it('should return no matches for a password meeting all requirements', () => { - const resultUppercase = uppercaseMatcher.Matching.prototype.match({ password: 'Password123!' }); - const resultLowercase = lowercaseMatcher.Matching.prototype.match({ password: 'Password123!' }); - const resultNumber = numberMatcher.Matching.prototype.match({ password: 'Password123!' }); - const resultSpecial = specialMatcher.Matching.prototype.match({ password: 'Password123!' }); - - expect(resultUppercase).to.be.empty; - expect(resultLowercase).to.be.empty; - expect(resultNumber).to.be.empty; - expect(resultSpecial).to.be.empty; - }); - it('should provide correct feedback for missing uppercase letters', () => { const match = { pattern: 'uppercase', token: 'password123!', i: 0, j: 11, guesses: 1, guessesLog10: 0 }; const feedback = uppercaseMatcher.feedback(match); expect(feedback.warning).to.equal('Include at least one uppercase letter.'); }); + it('should return a score of -100 for missing uppercase letters', () => { + const match = { pattern: 'uppercase', token: 'password123!', i: 0, j: 11 }; + const score = uppercaseMatcher.scoring(match); + expect(score).to.equal(-100); + }); +}); + +describe('lowercaseMatcher', () => { + it('should return a match for missing lowercase letters', () => { + const result = lowercaseMatcher.Matching.prototype.match({ password: 'PASSWORD123!' }); + expect(result).to.deep.include({ pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11 }); + }); + it('should provide correct feedback for missing lowercase letters', () => { const match = { pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11, guesses: 1, guessesLog10: 0 }; const feedback = lowercaseMatcher.feedback(match); expect(feedback.warning).to.equal('Include at least one lowercase letter.'); }); + it('should return a score of -100 for missing lowercase letters', () => { + const match = { pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11 }; + const score = lowercaseMatcher.scoring(match); + expect(score).to.equal(-100); + }); +}); + +describe('numberMatcher', () => { + it('should return a match for missing numbers', () => { + const result = numberMatcher.Matching.prototype.match({ password: 'Password!' }); + expect(result).to.deep.include({ pattern: 'number', token: 'Password!', i: 0, j: 8 }); + }); + it('should provide correct feedback for missing numbers', () => { const match = { pattern: 'number', token: 'Password!', i: 0, j: 8, guesses: 1, guessesLog10: 0 }; const feedback = numberMatcher.feedback(match); expect(feedback.warning).to.equal('Include at least one number.'); }); + it('should return a score of -100 for missing numbers', () => { + const match = { pattern: 'number', token: 'Password!', i: 0, j: 8 }; + const score = numberMatcher.scoring(match); + expect(score).to.equal(-100); + }); +}); + +describe('specialMatcher', () => { + it('should return a match for missing special characters', () => { + const result = specialMatcher.Matching.prototype.match({ password: 'Password123' }); + expect(result).to.deep.include({ pattern: 'special', token: 'Password123', i: 0, j: 10 }); + }); + it('should provide correct feedback for missing special characters', () => { const match = { pattern: 'special', token: 'Password123', i: 0, j: 10, guesses: 1, guessesLog10: 0 }; const feedback = specialMatcher.feedback(match); expect(feedback.warning).to.equal('Include at least one special character.'); }); - it('should return a score of -100 for missing uppercase letters', () => { - const match = { pattern: 'uppercase', token: 'password123!', i: 0, j: 11 }; - const score = uppercaseMatcher.scoring(match); + it('should return a score of -100 for missing special characters', () => { + const match = { pattern: 'special', token: 'Password123', i: 0, j: 10 }; + const score = specialMatcher.scoring(match); expect(score).to.equal(-100); }); +}); - it('should return a score of -100 for missing lowercase letters', () => { - const match = { pattern: 'lowercase', token: 'PASSWORD123!', i: 0, j: 11 }; - const score = lowercaseMatcher.scoring(match); - expect(score).to.equal(-100); +describe('minLengthMatcher', () => { + const minLength = 10; + const matcher = minLengthMatcher(minLength); + + it('should return a match for passwords shorter than the minimum length', () => { + const result = matcher.Matching.prototype.match({ password: 'short' }); + expect(result).to.deep.include({ pattern: 'minLength', token: 'short', i: 0, j: 4 }); }); - it('should return a score of -100 for missing numbers', () => { - const match = { pattern: 'number', token: 'Password!', i: 0, j: 8 }; - const score = numberMatcher.scoring(match); - expect(score).to.equal(-100); + it('should return no matches for passwords meeting the minimum length', () => { + const result = matcher.Matching.prototype.match({ password: 'longenoughpassword' }); + expect(result).to.be.empty; }); - it('should return a score of -100 for missing special characters', () => { - const match = { pattern: 'special', token: 'Password123', i: 0, j: 10 }; - const score = specialMatcher.scoring(match); + it('should provide correct feedback for passwords shorter than the minimum length', () => { + const match = { pattern: 'minLength', token: 'short', i: 0, j: 4, guesses: 1, guessesLog10: 0 }; + const feedback = matcher.feedback(match); + expect(feedback.warning).to.equal(`Password must be at least ${minLength} characters long.`); + }); + + it('should return a score of -100 for passwords shorter than the minimum length', () => { + const match = { pattern: 'minLength', token: 'short', i: 0, j: 4 }; + const score = matcher.scoring(match); expect(score).to.equal(-100); }); }); + +describe('multiple matchers', () => { + it('should return no matches for a password meeting all requirements', () => { + const resultUppercase = uppercaseMatcher.Matching.prototype.match({ password: 'Password123!' }); + const resultLowercase = lowercaseMatcher.Matching.prototype.match({ password: 'Password123!' }); + const resultNumber = numberMatcher.Matching.prototype.match({ password: 'Password123!' }); + const resultSpecial = specialMatcher.Matching.prototype.match({ password: 'Password123!' }); + + expect(resultUppercase).to.be.empty; + expect(resultLowercase).to.be.empty; + expect(resultNumber).to.be.empty; + expect(resultSpecial).to.be.empty; + }); +}); From d755c6d92e3dee8fb43f47da7a0fa3f2ee14e527 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Sun, 24 Nov 2024 21:13:29 +0200 Subject: [PATCH 4/6] feat: update min length feedback warning --- src/matchers/minLengthMatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matchers/minLengthMatcher.ts b/src/matchers/minLengthMatcher.ts index 50e05bb..c406809 100644 --- a/src/matchers/minLengthMatcher.ts +++ b/src/matchers/minLengthMatcher.ts @@ -11,7 +11,7 @@ export const minLengthMatcher = (minLength: number): Matcher => ({ } }, feedback(_match) { - return { warning: `Password must be at least ${minLength} characters long.`, suggestions: [] }; + return { warning: `Password may not be shorter than ${minLength} characters.`, suggestions: [] }; }, scoring(_match) { return -100; From 98ec3961f2a9c823bb0959d11fac64463b139838 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Sun, 24 Nov 2024 21:15:01 +0200 Subject: [PATCH 5/6] tests: fix min length feedback string test --- tests/unit/matchers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/matchers.test.ts b/tests/unit/matchers.test.ts index fff411b..6f0e95e 100644 --- a/tests/unit/matchers.test.ts +++ b/tests/unit/matchers.test.ts @@ -95,7 +95,7 @@ describe('minLengthMatcher', () => { it('should provide correct feedback for passwords shorter than the minimum length', () => { const match = { pattern: 'minLength', token: 'short', i: 0, j: 4, guesses: 1, guessesLog10: 0 }; const feedback = matcher.feedback(match); - expect(feedback.warning).to.equal(`Password must be at least ${minLength} characters long.`); + expect(feedback.warning).to.equal(`Password may not be shorter than ${minLength} characters.`); }); it('should return a score of -100 for passwords shorter than the minimum length', () => { From 5e621fad12403edf0f9323dd6221f784f114eb09 Mon Sep 17 00:00:00 2001 From: Ilan Kushnir Date: Mon, 25 Nov 2024 13:50:02 +0200 Subject: [PATCH 6/6] chore: update usage instructions --- README.md | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d8422e7..e9588c8 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,20 @@ npm install zxcvbn-custom-matchers ### Usage ```ts import { zxcvbnOptions } from '@zxcvbn-ts/core'; -import { +import { lowercaseMatcher, numberMatcher, specialMatcher, - uppercaseMatcher + uppercaseMatcher, + minLengthMatcher } from 'zxcvbn-custom-matchers'; // Add the matchers -zxcvbnOptions.addMatcher('lowercaseMatcher', lowercaseMatcher); -zxcvbnOptions.addMatcher('numberMatcher', numberMatcher); -zxcvbnOptions.addMatcher('specialMatcher', specialMatcher); -zxcvbnOptions.addMatcher('uppercaseMatcher', uppercaseMatcher); +zxcvbnOptions.addMatcher('lowercase', lowercaseMatcher); +zxcvbnOptions.addMatcher('number', numberMatcher); +zxcvbnOptions.addMatcher('special', specialMatcher); +zxcvbnOptions.addMatcher('uppercase', uppercaseMatcher); +zxcvbnOptions.addMatcher('minLength', minLengthMatcher(10)); // Use zxcvbn as usual import { zxcvbn } from '@zxcvbn-ts/core'; @@ -29,30 +31,43 @@ const result = zxcvbn('password123'); console.log(result); ``` +>Note: When adding a custom matcher with addMatcher, the first parameter (a string) should be the same as the matcher's pattern. + ## Matchers Description This project includes several matchers that enforce specific character requirements in passwords. Each matcher checks for a particular type of character and provides feedback and scoring. ### Uppercase Matcher +- **Pattern**: `uppercase` - **Purpose**: Ensures the password contains at least one uppercase letter. - **Feedback**: Suggests including at least one uppercase letter if missing. -- **Scoring**: Returns a score of `-100` if missing uppercase letters. +- **Scoring**: Returns a score of `1` if missing uppercase letters. ### Lowercase Matcher +- **Pattern**: `lowercase` - **Purpose**: Ensures the password contains at least one lowercase letter. - **Feedback**: Suggests including at least one lowercase letter if missing. -- **Scoring**: Returns a score of `-100` if missing lowercase letters. +- **Scoring**: Returns a score of `1` if missing lowercase letters. ### Number Matcher +- **Pattern**: `number` - **Purpose**: Ensures the password contains at least one number. - **Feedback**: Suggests including at least one number if missing. -- **Scoring**: Returns a score of `-100` if missing numbers. +- **Scoring**: Returns a score of `1` if missing numbers. ### Special Character Matcher +- **Pattern**: `special` - **Purpose**: Ensures the password contains at least one special character (e.g., !, @, #, $, etc.). - **Feedback**: Suggests including at least one special character if missing. -- **Scoring**: Returns a score of `-100` if missing special characters. +- **Scoring**: Returns a score of `1` if missing special characters. + +### Minimum Length Matcher + +- **Pattern**: `minLength` +- **Purpose**: Ensures the password meets the minimum length requirement. +- **Feedback**: Suggests the password must be at least the specified length if shorter. +- **Scoring**: Returns a score of `1` if the password is shorter than the specified length.