├── .prettierrc ├── lib └── main.ts ├── src ├── main.ts ├── shims-vue.d.ts ├── types │ └── index.d.ts ├── logic │ ├── index.ts │ ├── checkStrength.ts │ ├── isCommonPassword.ts │ ├── nameScore.ts │ └── scorePassword.ts ├── data │ └── commonPasswords.ts ├── App.vue └── password-meter.vue ├── .editorconfig ├── vue.config.js ├── tests ├── password-examples.ts └── unit │ ├── logic.spec.ts │ └── main.spec.ts ├── .gitignore ├── tsconfig.json ├── index.html ├── licence.txt ├── vite.config.js ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "arrowParens": "always", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /lib/main.ts: -------------------------------------------------------------------------------- 1 | import PasswordMeter from '../src/password-meter.vue' 2 | 3 | export default PasswordMeter 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue' 3 | 4 | createApp( App ).mount('#app') 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from 'vue' 2 | 3 | declare module 'vue-simple-password-meter' { 4 | const PasswordMeter: Component 5 | export default PasswordMeter 6 | } 7 | 8 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configureWebpack: { 3 | optimization: { 4 | splitChunks: false 5 | } 6 | }, 7 | css: { 8 | extract: false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/logic/index.ts: -------------------------------------------------------------------------------- 1 | import scorePassword from './scorePassword' 2 | import nameScore from './nameScore' 3 | import checkStrength from './checkStrength' 4 | 5 | export { scorePassword, nameScore, checkStrength } 6 | -------------------------------------------------------------------------------- /tests/password-examples.ts: -------------------------------------------------------------------------------- 1 | export const risky = 'sdwasdwq' 2 | export const guessable = 'adsasdasd' 3 | export const weak = 'asdasd1212' 4 | export const safe = 'ASSAasd123' 5 | export const secure = '$p@RYr3y6' 6 | 7 | export const common = '123456789' 8 | -------------------------------------------------------------------------------- /src/logic/checkStrength.ts: -------------------------------------------------------------------------------- 1 | import scorePassword from './scorePassword' 2 | import nameScore from './nameScore' 3 | 4 | const checkStrength = (pass: string) => { 5 | const score = scorePassword(pass) 6 | return nameScore(score) 7 | } 8 | 9 | export default checkStrength 10 | -------------------------------------------------------------------------------- /src/logic/isCommonPassword.ts: -------------------------------------------------------------------------------- 1 | /* 2 | check to see if it is a common password 3 | */ 4 | 5 | import commonPasswords from '../data/commonPasswords'; 6 | 7 | export const isCommonPassword = (password: string): boolean => { 8 | return commonPasswords.includes(password); 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/logic/nameScore.ts: -------------------------------------------------------------------------------- 1 | const nameScore = (score: number): string => { 2 | switch (score) { 3 | case 0: 4 | return 'risky' 5 | case 1: 6 | return 'guessable' 7 | case 2: 8 | return 'weak' 9 | case 3: 10 | return 'safe' 11 | case 4: 12 | return 'secure' 13 | default: 14 | return '' 15 | } 16 | } 17 | 18 | export default nameScore 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "isolatedModules": true, 8 | "skipLibCheck": true, 9 | "noEmit": true, 10 | "paths": { 11 | "@/*": ["./src/*"], 12 | } 13 | }, 14 | "include": ["src/**/*.ts", "tests/**/*.ts"], 15 | "exclude": ["node_modules", ".vscode"] 16 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vue simple password meter 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/data/commonPasswords.ts: -------------------------------------------------------------------------------- 1 | // common passwords as an array of strings 2 | 3 | const commonPasswords = [ 4 | "123456", 5 | "qwerty", 6 | "password", 7 | "111111", 8 | "Abc123", 9 | "123456789", 10 | "12345678", 11 | "123123", 12 | "1234567890", 13 | "12345", 14 | "1234567", 15 | "qwertyuiop", 16 | "qwerty123", 17 | "1q2w3e", 18 | "password1", 19 | "123321", 20 | "Iloveyou", 21 | "12345" 22 | ] 23 | 24 | export default commonPasswords 25 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | 29 | 31 | -------------------------------------------------------------------------------- /licence.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Milad Dehghan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue'; 5 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' 6 | 7 | export default defineConfig({ 8 | plugins: [vue(),cssInjectedByJsPlugin()], 9 | resolve: { 10 | alias: { 11 | '@': resolve(__dirname, './src'), 12 | }, 13 | }, 14 | build: { 15 | lib: { 16 | // Could also be a dictionary or array of multiple entry points 17 | entry: resolve(__dirname, 'lib/main.ts'), 18 | name: 'vue-simple-password-meter', 19 | // the proper extensions will be added 20 | fileName: 'vue-simple-password-meter' 21 | }, 22 | rollupOptions: { 23 | // make sure to externalize deps that shouldn't be bundled 24 | // into your library 25 | external: ['vue'], 26 | output: { 27 | // Provide global variables to use in the UMD build 28 | // for externalized deps 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | }, 35 | test: { 36 | environment: 'happy-dom' 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/password-meter.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 31 | 32 | 66 | -------------------------------------------------------------------------------- /src/logic/scorePassword.ts: -------------------------------------------------------------------------------- 1 | import { isCommonPassword } from '@/logic/isCommonPassword' 2 | 3 | const scorePassword = (pass : string): number => { 4 | let score = 0 5 | let length = 0 6 | let specialChar = 0 7 | let caseMix = 0 8 | let numCharMix = 0 9 | 10 | const specialCharRegex = /[^A-Za-z0-9]/g 11 | const lowercaseRegex = /(.*[a-z].*)/g 12 | const uppercaseRegex = /(.*[A-Z].*)/g 13 | const numberRegex = /(.*[0-9].*)/g 14 | const repeatCharRegex = /(\w)(\1+\1+\1+\1+)/g 15 | 16 | const hasSpecialChar = specialCharRegex.test(pass) 17 | const hasLowerCase = lowercaseRegex.test(pass) 18 | const hasUpperCase = uppercaseRegex.test(pass) 19 | const hasNumber = numberRegex.test(pass) 20 | const hasRepeatChars = repeatCharRegex.test(pass) 21 | 22 | if (pass.length > 4) { 23 | if (isCommonPassword(pass)) { 24 | return 0 25 | } 26 | 27 | if ((hasLowerCase || hasUpperCase) && hasNumber) { 28 | numCharMix = 1 29 | } 30 | 31 | if (hasUpperCase && hasLowerCase) { 32 | caseMix = 1 33 | } 34 | 35 | if ((hasLowerCase || hasUpperCase || hasNumber) && hasSpecialChar) { 36 | specialChar = 1 37 | } 38 | 39 | if (pass.length > 8) { 40 | length = 1 41 | } 42 | 43 | if (pass.length > 12 && !hasRepeatChars) { 44 | length = 2 45 | } 46 | 47 | if (pass.length > 20 && !hasRepeatChars) { 48 | length = 3 49 | } 50 | 51 | score = length + specialChar + caseMix + numCharMix 52 | 53 | if (score > 4) { 54 | score = 4 55 | } 56 | } 57 | 58 | return score 59 | } 60 | 61 | export default scorePassword 62 | -------------------------------------------------------------------------------- /tests/unit/logic.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkStrength, scorePassword } from '@/logic' 2 | import { risky, guessable, weak, safe, secure, common } from '../password-examples' 3 | import {describe, it, expect} from 'vitest' 4 | 5 | describe('checkStrength', () => { 6 | it('return risky', () => { 7 | expect(checkStrength(risky)).toBe('risky') 8 | }) 9 | 10 | it('return guessable', () => { 11 | expect(checkStrength(guessable)).toBe('guessable') 12 | }) 13 | 14 | it('return weak', () => { 15 | expect(checkStrength(weak)).toBe('weak') 16 | }) 17 | 18 | it('return safe', () => { 19 | expect(checkStrength(safe)).toBe('safe') 20 | }) 21 | 22 | it('return secure', () => { 23 | expect(checkStrength(secure)).toBe('secure') 24 | }) 25 | 26 | it('return risky common', () => { 27 | expect(checkStrength(common)).toBe('risky') 28 | }) 29 | 30 | it('return risky empty', () => { 31 | expect(checkStrength('')).toBe('risky') 32 | }); 33 | 34 | }) 35 | 36 | describe('scorePassword', () => { 37 | it('return risky = 0', () => { 38 | expect(scorePassword(risky)).toBe(0) 39 | }) 40 | 41 | it('return guessable = 1', () => { 42 | expect(scorePassword(guessable)).toBe(1) 43 | }) 44 | 45 | it('return weak = 2', () => { 46 | expect(scorePassword(weak)).toBe(2) 47 | }) 48 | 49 | it('return safe = 3', () => { 50 | expect(scorePassword(safe)).toBe(3) 51 | }) 52 | 53 | it('return secure = 4', () => { 54 | expect(scorePassword(secure)).toBe(4) 55 | }) 56 | 57 | it('return risky common = 0', () => { 58 | expect(scorePassword(common)).toBe(0) 59 | }) 60 | 61 | it('return risky empty = 0', () => { 62 | expect(scorePassword('')).toBe(0) 63 | }); 64 | }) 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-simple-password-meter", 3 | "version": "1.3.4", 4 | "private": false, 5 | "description": "A really simple password strength meter. customizable with css", 6 | "author": { 7 | "name": "Milad Dehghan", 8 | "email": "miladd3@gmail.com", 9 | "url": "https://github.com/miladd3" 10 | }, 11 | "scripts": { 12 | "serve": "vite serve", 13 | "clean": "rimraf ./dist", 14 | "build": "npm run clean && vite build", 15 | "build:npm": "npm run clean && vite build", 16 | "gitvue2": "git checkout vue-2 && git pull && npm clean-install", 17 | "test": "vitest" 18 | }, 19 | "main": "dist/vue-simple-password-meter.umd.js", 20 | "unpkg": "dist/vue-simple-password-meter.umd.js", 21 | "typings": "src/types/index.d.ts", 22 | "types": "src/types/index.d.ts", 23 | "files": [ 24 | "dist", 25 | "src/types" 26 | ], 27 | "devDependencies": { 28 | "@typescript-eslint/eslint-plugin": "^2.33.0", 29 | "@typescript-eslint/parser": "^2.33.0", 30 | "@vitejs/plugin-vue": "^3.2.0", 31 | "@vue/test-utils": "^2.3.2", 32 | "happy-dom": "^9.1.9", 33 | "rimraf": "^4.4.1", 34 | "typescript": "^5.0.4", 35 | "vite": "^3.2.4", 36 | "vite-plugin-css-injected-by-js": "^2.1.1", 37 | "vitest": "^0.30.0", 38 | "vue": "^3.2", 39 | "vue-jest": "^5.0.0-0" 40 | }, 41 | "browserslist": [ 42 | "> 1%", 43 | "last 2 versions" 44 | ], 45 | "bugs": { 46 | "url": "https://github.com/miladd3/vue-simple-password-meter/issues", 47 | "email": "miladd3@gmail.com" 48 | }, 49 | "keywords": [ 50 | "vue", 51 | "password strength", 52 | "password meter", 53 | "password", 54 | "simple", 55 | "strength" 56 | ], 57 | "license": "MIT", 58 | "repository": { 59 | "url": "https://github.com/miladd3/vue-simple-password-meter" 60 | }, 61 | "homepage": "https://github.com/miladd3/vue-simple-password-meter#readme", 62 | "resolutions": { 63 | "yargs-parser": "15.0.1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/unit/main.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import PasswordMeter from '@/password-meter.vue' 3 | import { risky, guessable, weak, safe, secure, common } from '../password-examples' 4 | import {describe, it, expect} from 'vitest' 5 | 6 | describe('password-meter.vue', () => { 7 | /** 8 | * Test risky 9 | */ 10 | it('renders and detects: risky', () => { 11 | const wrapper = shallowMount(PasswordMeter, { 12 | props: { 13 | password: risky 14 | } 15 | }) 16 | 17 | expect(wrapper.classes()).toContain('risky') 18 | }) 19 | 20 | /** 21 | * Test guessable 22 | */ 23 | it('renders and detects : guessable', () => { 24 | const wrapper = shallowMount(PasswordMeter, { 25 | props: { 26 | password: guessable 27 | } 28 | }) 29 | 30 | expect(wrapper.classes()).toContain('guessable') 31 | }) 32 | 33 | /** 34 | * Test weak 35 | */ 36 | it('renders and detects : weak', () => { 37 | const wrapper = shallowMount(PasswordMeter, { 38 | props: { 39 | password: weak 40 | } 41 | }) 42 | 43 | expect(wrapper.classes()).toContain('weak') 44 | }) 45 | 46 | /** 47 | * Test safe 48 | */ 49 | it('renders and detects : safe', () => { 50 | const wrapper = shallowMount(PasswordMeter, { 51 | props: { 52 | password: safe 53 | } 54 | }) 55 | 56 | expect(wrapper.classes()).toContain('safe') 57 | }) 58 | 59 | /** 60 | * Test secure 61 | */ 62 | it('renders and detects : secure', () => { 63 | const wrapper = shallowMount(PasswordMeter, { 64 | props: { 65 | password: secure 66 | } 67 | }) 68 | 69 | expect(wrapper.classes()).toContain('secure') 70 | }) 71 | 72 | /** 73 | * Test common 74 | */ 75 | it('renders and detects : common', () => { 76 | const wrapper = shallowMount(PasswordMeter, { 77 | props: { 78 | password: common 79 | } 80 | }) 81 | 82 | expect(wrapper.classes()).toContain('risky') 83 | }); 84 | 85 | }) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-simple-password-meter 2 | 3 | **Vue Simple Password Meter** is a simple password checker written in vanilla js and extremely lightweight (** less than 1kb minified + Gzipped**) 4 | 5 | #### This is `Vue 3.*` compatible version. If you are using `Vue 2.*` [Click Here](https://github.com/miladd3/vue-simple-password-meter/tree/vue-2) 6 | 7 | ## Demo 8 | 9 | [Demo](https://vspm-next.herokuapp.com/) 10 | 11 | ## Install 12 | 13 | `npm install vue-simple-password-meter --save` 14 | 15 | ## Usage 16 | 17 | Simply use v-model and send it to the component using password prop 18 | 19 | ```vue 20 | 27 | 28 | 45 | ``` 46 | 47 | ### Customize using css 48 | 49 | If you want to customize the bar its really simple with some easy css you can customize it 50 | 51 | Overwrite these css styles globally and change each state color and style 52 | 53 | ```css 54 | .po-password-strength-bar { 55 | border-radius: 2px; 56 | transition: all 0.2s linear; 57 | height: 5px; 58 | margin-top: 8px; 59 | } 60 | 61 | .po-password-strength-bar.risky { 62 | background-color: #f95e68; 63 | width: 10%; 64 | } 65 | 66 | .po-password-strength-bar.guessable { 67 | background-color: #fb964d; 68 | width: 32.5%; 69 | } 70 | 71 | .po-password-strength-bar.weak { 72 | background-color: #fdd244; 73 | width: 55%; 74 | } 75 | 76 | .po-password-strength-bar.safe { 77 | background-color: #b0dc53; 78 | width: 77.5%; 79 | } 80 | 81 | .po-password-strength-bar.secure { 82 | background-color: #35cc62; 83 | width: 100%; 84 | } 85 | ``` 86 | 87 | 88 | ## Events 89 | 90 | You can use event `score` to use scored number between `0` to `4` that scores password from risky to secure with 4 is a secure password and 0 is risky and between. 91 | 92 | You can use this as a form verification tool 93 | 94 | See below example for more detail 95 | 96 | ```vue 97 | 105 | 106 | 133 | ``` 134 | 135 | ## Contributing 136 | 137 | If you want to contribute to this project simply fork it and clone it then run 138 | `npm i` 139 | in the root of the project, then run 140 | `npm run serve` 141 | to run development server. 142 | 143 | ## Motivation 144 | 145 | Since Every other components and libraries mostly were using [zxcvbn](https://github.com/dropbox/zxcvbn) was 799.5kb minified and 388.3kb minified and Gzipped, so I decided to make simpler approach and use regex instead of dictionary for validating. 146 | 147 | ### Licence and cast 148 | 149 | MIT Licence 150 | 151 | by [Milad Dehghan](http://dehghan.net) 152 | --------------------------------------------------------------------------------