- add password strength meter for creating or editing user passwords

- 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

@ -14,7 +14,7 @@ import env from '#start/env';
// import { default as Dataset } from '#models/dataset';
const opensearchNode = env.get('OPENSEARCH_HOST', 'localhost');
const client = new Client({ node: `http://${opensearchNode}` }); // replace with your OpenSearch endpoint
const client = new Client({ node: `${opensearchNode}` }); // replace with your OpenSearch endpoint
export default class IndexDatasets extends BaseCommand {
static commandName = 'index:datasets';

View File

@ -13,12 +13,13 @@
"assets/resources_js_Pages_Admin_Role_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Show_vue.js",
"assets/resources_js_Pages_Admin_Settings_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Settings_vue.js",
"assets/resources_js_Pages_Admin_User_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Create_vue.js",
"assets/resources_js_Pages_Admin_User_Edit_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Edit_vue.js",
"assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.css": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.css",
"assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.js",
"assets/resources_js_Pages_Admin_User_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Index_vue.js",
"assets/resources_js_Pages_Admin_User_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Show_vue.js",
"assets/resources_js_Pages_App_vue.js": "http://localhost:8080/assets/resources_js_Pages_App_vue.js",
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css.css": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css.css",
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css.js": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css.js",
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.css": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.css",
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.js": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.js",
"assets/resources_js_Pages_Auth_Login_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_Login_vue.js",
"assets/resources_js_Pages_Auth_Register_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_Register_vue.js",
"assets/resources_js_Pages_Dashboard_vue.js": "http://localhost:8080/assets/resources_js_Pages_Dashboard_vue.js",
@ -49,9 +50,9 @@
"assets/vendors-node_modules_mdi_js_mdi_js-node_modules_vue-loader_dist_exportHelper_js.js": "http://localhost:8080/assets/vendors-node_modules_mdi_js_mdi_js-node_modules_vue-loader_dist_exportHelper_js.js",
"assets/vendors-node_modules_focus-trap_dist_focus-trap_esm_js-node_modules_notiwind_dist_index_esm_js.js": "http://localhost:8080/assets/vendors-node_modules_focus-trap_dist_focus-trap_esm_js-node_modules_notiwind_dist_index_esm_js.js",
"assets/vendors-node_modules_vue-facing-decorator_dist_esm_utils_js.js": "http://localhost:8080/assets/vendors-node_modules_vue-facing-decorator_dist_esm_utils_js.js",
"assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-4ee9a6.js": "http://localhost:8080/assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-4ee9a6.js",
"assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-adabdc.js": "http://localhost:8080/assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-adabdc.js",
"assets/vendors-node_modules_toastify-js_src_toastify_js.js": "http://localhost:8080/assets/vendors-node_modules_toastify-js_src_toastify_js.js",
"assets/vendors-node_modules_buffer_index_js-node_modules_leaflet_src_layer_tile_TileLayer_WMS_js-nod-e7bc71.js": "http://localhost:8080/assets/vendors-node_modules_buffer_index_js-node_modules_leaflet_src_layer_tile_TileLayer_WMS_js-nod-e7bc71.js",
"assets/vendors-node_modules_buffer_index_js-node_modules_vuedraggable_dist_vuedraggable_umd_js.js": "http://localhost:8080/assets/vendors-node_modules_buffer_index_js-node_modules_vuedraggable_dist_vuedraggable_umd_js.js",
"assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js": "http://localhost:8080/assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js",
"assets/resources_js_Components_BaseButton_vue.js": "http://localhost:8080/assets/resources_js_Components_BaseButton_vue.js",
"assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js": "http://localhost:8080/assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js",
@ -64,6 +65,7 @@
"assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js": "http://localhost:8080/assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js",
"assets/resources_js_Components_CardBoxModal_vue.js": "http://localhost:8080/assets/resources_js_Components_CardBoxModal_vue.js",
"assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-bdf2f9.js": "http://localhost:8080/assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-bdf2f9.js",
"assets/resources_js_Components_SectionTitleLineWithButton_vue-resources_js_Components_SimplePassword-945989.js": "http://localhost:8080/assets/resources_js_Components_SectionTitleLineWithButton_vue-resources_js_Components_SimplePassword-945989.js",
"assets/fonts/inter-latin-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.40b3b0d5.woff",
"assets/fonts/inter-latin-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.0f9e8d4e.woff2",
"assets/fonts/inter-latin-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-400-normal.08a02fd2.woff",
@ -85,5 +87,8 @@
"assets/images/marker-icon.png": "http://localhost:8080/assets/images/marker-icon.2b3e1faf.png",
"assets/images/layers-2x.png": "http://localhost:8080/assets/images/layers-2x.8f2c4d11.png",
"assets/images/layers.png": "http://localhost:8080/assets/images/layers.416d9136.png",
"assets/images/Close.svg": "http://localhost:8080/assets/images/Close.e4887675.svg"
"assets/images/Close.svg": "http://localhost:8080/assets/images/Close.e4887675.svg",
"assets/vendors-node_modules_vue-facing-decorator_dist_esm_index_js-node_modules_vue-facing-decorator-818045.js": "http://localhost:8080/assets/vendors-node_modules_vue-facing-decorator_dist_esm_index_js-node_modules_vue-facing-decorator-818045.js",
"assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.css": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.css",
"assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.js"
}

View File

@ -7,7 +7,7 @@ import { canvas } from 'leaflet/src/layer/vector/Canvas';
import { svg } from 'leaflet/src/layer/vector/SVG';
import axios from 'axios';
import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
// import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
import { TileLayer } from 'leaflet/src/layer/tile/TileLayer';
import { Attribution } from 'leaflet/src/control/Control.Attribution';
import DrawControlComponent from '@/Components/Map/draw.component.vue';
@ -60,7 +60,7 @@ Map.include({
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
const DEFAULT_BASE_LAYER_ATTRIBUTION = '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
// const OPENSEARCH_HOST = 'http://localhost:9200';
const OPENSEARCH_HOST = 'http://192.168.21.18';
const OPENSEARCH_HOST = `${process.env.OPENSEARCH_HOST}`;
// const OPENSEARCH_HOST = `http://${process.env.OPENSEARCH_PUBLIC_HOST}`;
let map: Map;
@ -134,21 +134,21 @@ const initMap = async () => {
const attributionControl = new Attribution().addTo(map);
attributionControl.setPrefix(false);
// let osmGgray = tileLayerWMS('https://ows.terrestris.de/osm-gray/service', {
// format: 'image/png',
// attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
// layers: 'OSM-WMS',
// });
let baseAt = new TileLayer('https://{s}.wien.gv.at/basemap/bmapgrau/normal/google3857/{z}/{y}/{x}.png', {
subdomains: ['maps', 'maps1', 'maps2', 'maps3', 'maps4'],
let osmGgray = tileLayerWMS('https://ows.terrestris.de/osm-gray/service', {
format: 'image/png',
attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
layers: 'OSM-WMS',
});
// let baseAt = new TileLayer('https://{s}.wien.gv.at/basemap/bmapgrau/normal/google3857/{z}/{y}/{x}.png', {
// subdomains: ['maps', 'maps1', 'maps2', 'maps3', 'maps4'],
// attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
// });
let layerOptions = {
label: DEFAULT_BASE_LAYER_NAME,
visible: true,
layer: baseAt,
layer: osmGgray,
};
layerOptions.layer.addTo(map);

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>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Head, useForm, router } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline } from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
@ -13,6 +14,16 @@ import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
// import { Inertia } from '@inertiajs/inertia';
import passwordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
const enabled = ref(false);
const handleScore = (score: number) => {
if (score == 4){
enabled.value = true;
} else {
enabled.value = false;
}
};
const props = defineProps({
roles: {
@ -80,6 +91,7 @@ const submit = async () => {
</div>
</FormControl>
</FormField>
<password-meter :password="form.password" @score="handleScore" />
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
<FormControl
@ -115,7 +127,7 @@ const submit = async () => {
color="info"
label="Submit"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
:disabled="form.processing == true || enabled == false"
/>
</BaseButtons>
</template>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Head, useForm, router } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline } from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
@ -13,7 +14,9 @@ import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
// import { Inertia } from '@inertiajs/inertia';
import passwordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
const enabled = ref(false);
const props = defineProps({
user: {
type: Object,
@ -46,6 +49,15 @@ const submit = async () => {
// await Inertia.post(stardust.route('user.store'), form);
await router.put(stardust.route('settings.user.update', [props.user.id]), form);
};
const handleScore = (score: number) => {
if (score == 4){
enabled.value = true;
} else {
enabled.value = false;
}
// strengthLabel.value = scoreLabel;
// score.value = scoreValue;
};
</script>
<template>
@ -88,6 +100,8 @@ const submit = async () => {
</FormControl>
</FormField>
<password-meter :password="form.password" @score="handleScore" />
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
<FormControl
v-model="form.password_confirmation"
@ -121,7 +135,7 @@ const submit = async () => {
color="info"
label="Submit"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
:disabled="form.processing == true || enabled == false"
/>
</BaseButtons>
</template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
// import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
// import { ref } from 'vue';
// import { reactive } from 'vue';
@ -24,7 +25,7 @@ import NotificationBar from '@/Components/NotificationBar.vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
// import { Inertia } from '@inertiajs/inertia';
import passwordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
import { computed, Ref } from 'vue';
import { usePage } from '@inertiajs/vue3';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
@ -35,6 +36,17 @@ import PersonalTotpSettings from '@/Components/PersonalTotpSettings.vue';
const emit = defineEmits(['confirm', 'update:confirmation'])
const enabled = ref(false);
const handleScore = (score: number) => {
if (score == 4){
enabled.value = true;
} else {
enabled.value = false;
}
// strengthLabel.value = scoreLabel;
// score.value = scoreValue;
};
defineProps({
// user will be returned from controller action
user: {
@ -183,6 +195,8 @@ const flash: Ref<any> = computed(() => {
</div>
</FormControl>
</FormField>
<password-meter :password="passwordForm.new_password" @score="handleScore" />
<FormField label="Confirm password" help="Required. New password one more time"
:class="{ 'text-red-400': passwordForm.errors.confirm_password }">
@ -205,7 +219,7 @@ const flash: Ref<any> = computed(() => {
<template #footer>
<BaseButtons>
<BaseButton type="submit" color="info" label="Change password" />
<BaseButton type="submit" color="info" label="Change password" :disabled="passwordForm.processing == true || enabled == false" />
</BaseButtons>
</template>
</CardBox>