From 5ecf0ec706292100d2f7fd3218c7ae32d7451241 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 24 Sep 2022 17:56:15 -0800 Subject: [PATCH] implement password score UI and functions --- frontend/components/global/PasswordScore.vue | 40 +++++++++++++++++ frontend/composables/use-password-score.ts | 37 ++++++++++++++++ frontend/lib/passwords/index.test.ts | 30 +++++++++++++ frontend/lib/passwords/index.ts | 45 ++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 frontend/components/global/PasswordScore.vue create mode 100644 frontend/composables/use-password-score.ts create mode 100644 frontend/lib/passwords/index.test.ts create mode 100644 frontend/lib/passwords/index.ts diff --git a/frontend/components/global/PasswordScore.vue b/frontend/components/global/PasswordScore.vue new file mode 100644 index 0000000..1394237 --- /dev/null +++ b/frontend/components/global/PasswordScore.vue @@ -0,0 +1,40 @@ + + + Password Strength: {{ message }} + + + + + + + diff --git a/frontend/composables/use-password-score.ts b/frontend/composables/use-password-score.ts new file mode 100644 index 0000000..fcb7621 --- /dev/null +++ b/frontend/composables/use-password-score.ts @@ -0,0 +1,37 @@ +import type { ComputedRef, Ref } from "vue"; +import { scorePassword } from "~~/lib/passwords"; + +export interface PasswordScore { + score: ComputedRef; + message: ComputedRef; + isValid: ComputedRef; +} + +export function usePasswordScore(pw: Ref, min = 30): PasswordScore { + const score = computed(() => { + return scorePassword(pw.value) || 0; + }); + + const message = computed(() => { + if (score.value < 20) { + return "Very weak"; + } else if (score.value < 40) { + return "Weak"; + } else if (score.value < 60) { + return "Good"; + } else if (score.value < 80) { + return "Strong"; + } + return "Very strong"; + }); + + const isValid = computed(() => { + return score.value >= min; + }); + + return { + score, + isValid, + message, + }; +} diff --git a/frontend/lib/passwords/index.test.ts b/frontend/lib/passwords/index.test.ts new file mode 100644 index 0000000..50c1b52 --- /dev/null +++ b/frontend/lib/passwords/index.test.ts @@ -0,0 +1,30 @@ +import { describe, test, expect } from "vitest"; +import { scorePassword } from "."; + +describe("scorePassword tests", () => { + test("flagged words should return negative number", () => { + const flaggedWords = ["password", "homebox", "admin", "qwerty", "login"]; + + for (const word of flaggedWords) { + expect(scorePassword(word)).toBe(0); + } + }); + + test("should return 0 for empty string", () => { + expect(scorePassword("")).toBe(0); + }); + + test("should return 0 for strings less than 6", () => { + expect(scorePassword("12345")).toBe(0); + }); + + test("should return positive number for long string", () => { + const result = expect(scorePassword("123456")); + result.toBeGreaterThan(0); + result.toBeLessThan(31); + }); + + test("should return max number for long string with all variations", () => { + expect(scorePassword("3bYWcfYOwqxljqeOmQXTLlBwkrH6HV")).toBe(100); + }); +}); diff --git a/frontend/lib/passwords/index.ts b/frontend/lib/passwords/index.ts new file mode 100644 index 0000000..27a3c45 --- /dev/null +++ b/frontend/lib/passwords/index.ts @@ -0,0 +1,45 @@ +const flaggedWords = ["password", "homebox", "admin", "qwerty", "login"]; + +/** + * scorePassword returns a score for a given password between 0 and 100. + * if a password contains a flagged word, it returns 0. + * @param pass + * @returns + */ +export function scorePassword(pass: string): number { + let score = 0; + if (!pass) return score; + + if (pass.length < 6) return score; + + // Check for flagged words + for (const word of flaggedWords) { + if (pass.toLowerCase().includes(word)) { + return 0; + } + } + + // award every unique letter until 5 repetitions + const letters: { [key: string]: number } = {}; + + for (let i = 0; i < pass.length; i++) { + letters[pass[i]] = (letters[pass[i]] || 0) + 1; + score += 5.0 / letters[pass[i]]; + } + + // bonus points for mixing it up + const variations: { [key: string]: boolean } = { + digits: /\d/.test(pass), + lower: /[a-z]/.test(pass), + upper: /[A-Z]/.test(pass), + nonWords: /\W/.test(pass), + }; + + let variationCount = 0; + for (const check in variations) { + variationCount += variations[check] === true ? 1 : 0; + } + score += (variationCount - 1) * 10; + + return Math.max(Math.min(score, 100), 0); +}
Password Strength: {{ message }}