- add password strength meter for creating or editing user passwords
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m0s

- add public opensearch api host
This commit is contained in:
Kaimbacher 2024-08-07 14:22:36 +02:00
parent f4854d70b9
commit 010bead723
13 changed files with 392 additions and 23 deletions

View file

@ -0,0 +1,26 @@
// common passwords as an array of strings
const commonPasswords = [
'123456',
'qwerty',
'password',
'111111',
'Abc123',
'123456789',
'12345678',
'123123',
'1234567890',
'12345',
'1234567',
'qwertyuiop',
'qwerty123',
'1q2w3e',
'password1',
'123321',
'Iloveyou',
'12345',
'test',
'test007'
];
export default commonPasswords;

View file

@ -0,0 +1,9 @@
export default class TrieNode {
children: { [key: string]: TrieNode };
isEndOfWord: boolean;
constructor() {
this.children = {};
this.isEndOfWord = false;
}
}

View file

@ -0,0 +1,30 @@
import TrieNode from './TieNode';
export default class Trie {
private root: TrieNode;
constructor() {
this.root = new TrieNode();
}
insert(word: string) {
let node: TrieNode = this.root;
for (let char of word) {
if (!node.children[char]) {
node.children[char] = new TrieNode();
}
node = node.children[char];
}
node.isEndOfWord = true;
}
search(word: string) {
let node = this.root;
for (let char of word) {
if (!node.children[char]) {
return false;
}
node = node.children[char];
}
return node.isEndOfWord;
}
}

View file

@ -0,0 +1,93 @@
import commonPasswords from '../data/commonPasswords';
import Trie from './Trie';
const checkStrength = (pass: string) => {
const score = scorePassword(pass);
const scoreLabel = mapScoreToLabel(score);
return {
score,
scoreLabel
}
};
export default checkStrength;
// Function to score the password based on different criteria
const scorePassword = (password: string): number => {
if (password.length <= 6) return 0;
if (isCommonPassword(password)) return 0;
let score = 0;
score += getLengthScore(password);
score += getSpecialCharScore(password);
score += getCaseMixScore(password);
score += getNumberMixScore(password);
return Math.min(score, 4); // Maximum score is 4
};
// Initialize the Trie with common passwords
const trie = new Trie();
commonPasswords.forEach(password => trie.insert(password));
const isCommonPassword = (password: string): boolean => {
// return commonPasswords.includes(password);
return trie.search(password);
};
// Function to get the score based on password length
const getLengthScore = (password: string): number => {
if (password.length > 20 && !hasRepeatChars(password)) return 3;
if (password.length > 12 && !hasRepeatChars(password)) return 2;
if (password.length > 8) return 1;
return 0;
};
// Function to check if the password contains repeated characters
const hasRepeatChars = (password: string): boolean => {
const repeatCharRegex = /(\w)(\1+\1+\1+\1+)/g;
return repeatCharRegex.test(password);
};
// Function to get the score based on the presence of special characters
const getSpecialCharScore = (password: string): number => {
const specialCharRegex = /[^A-Za-z0-9]/g;
return specialCharRegex.test(password) ? 1 : 0;
};
// Function to get the score based on the mix of uppercase and lowercase letters
const getCaseMixScore = (password: string): number => {
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
return hasUpperCase && hasLowerCase ? 1 : 0;
};
// Function to get the score based on the mix of letters and numbers
const getNumberMixScore = (password: string): number => {
const hasLetter = /[A-Za-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
return hasLetter && hasNumber ? 1 : 0;
};
// Function to map the score to a corresponding label
const mapScoreToLabel = (score: number): string => {
const labels = ['risky', 'guessable', 'weak', 'safe', 'secure'];
return labels[score] || '';
};
// const nameScore = (score: number): string => {
// switch (score) {
// case 0:
// return 'risky';
// case 1:
// return 'guessable';
// case 2:
// return 'weak';
// case 3:
// return 'safe';
// case 4:
// return 'secure';
// default:
// return '';
// }
// };

View file

@ -0,0 +1,5 @@
// import scorePassword from './scorePassword'
// import nameScore from './nameScore'
import checkStrength from './checkStrength'
export { checkStrength }

View file

@ -0,0 +1,66 @@
// import { isCommonPassword } from "./isCommonPassword"
import commonPasswords from '../data/commonPasswords';
const scorePassword = (pass: string): number => {
let score = 0;
let length = 0;
let specialChar = 0;
let caseMix = 0;
let numCharMix = 0;
const specialCharRegex = /[^A-Za-z0-9]/g;
const lowercaseRegex = /(.*[a-z].*)/g;
const uppercaseRegex = /(.*[A-Z].*)/g;
const numberRegex = /(.*[0-9].*)/g;
const repeatCharRegex = /(\w)(\1+\1+\1+\1+)/g;
const hasSpecialChar = specialCharRegex.test(pass);
const hasLowerCase = lowercaseRegex.test(pass);
const hasUpperCase = uppercaseRegex.test(pass);
const hasNumber = numberRegex.test(pass);
const hasRepeatChars = repeatCharRegex.test(pass);
if (pass.length > 4) {
if (isCommonPassword(pass)) {
return 0;
}
if ((hasLowerCase || hasUpperCase) && hasNumber) {
numCharMix = 1;
}
if (hasUpperCase && hasLowerCase) {
caseMix = 1;
}
if ((hasLowerCase || hasUpperCase || hasNumber) && hasSpecialChar) {
specialChar = 1;
}
if (pass.length > 8) {
length = 1;
}
if (pass.length > 12 && !hasRepeatChars) {
length = 2;
}
if (pass.length > 20 && !hasRepeatChars) {
length = 3;
}
score = length + specialChar + caseMix + numCharMix;
if (score > 4) {
score = 4;
}
}
return score;
};
export default scorePassword;
const isCommonPassword = (password: string): boolean => {
return commonPasswords.includes(password);
};

View file

@ -0,0 +1,95 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { checkStrength } from './logic/index';
// Define props
const props = defineProps<{
password: string;
}>();
// Define emits
// const emit = defineEmits<{
// (event: 'score', payload: { score: number; strength: string }): void;
// }>();
const emit = defineEmits(['score']);
// const score = (event) => {
// emit('score', event, payload: { score; strength: string });
// };
// Computed property for password class
const passwordClass = computed(() => {
if (!props.password) {
return null;
}
// const scoreLabel = checkStrength(props.password);
// const score = scorePassword(props.password);
const { score, scoreLabel } = checkStrength(props.password);
emit('score', score);
return {
[scoreLabel]: true,
// scored: true,
};
});
// export default {
// name: 'PasswordMeter',
// props: {
// password: String,
// },
// emits: ['score'],
// computed: {
// passwordClass(): object | null {
// if (!this.password) {
// return null;
// }
// const strength = checkStrength(this.password);
// const score = scorePassword(this.password);
// this.$emit('score', { score, strength });
// return {
// [strength]: true,
// scored: true,
// };
// },
// },
// };
</script>
<template>
<div class="po-password-strength-bar" :class="passwordClass" />
</template>
<style lang="css" scoped>
.po-password-strength-bar {
border-radius: 2px;
transition: all 0.2s linear;
height: 10px;
margin-top: 8px;
}
.po-password-strength-bar.risky {
background-color: #f95e68;
width: 10%;
}
.po-password-strength-bar.guessable {
background-color: #fb964d;
width: 32.5%;
}
.po-password-strength-bar.weak {
background-color: #fdd244;
width: 55%;
}
.po-password-strength-bar.safe {
background-color: #b0dc53;
width: 77.5%;
}
.po-password-strength-bar.secure {
background-color: #35cc62;
width: 100%;
}
</style>