From 7cff8ad8a8ca746dd4f76e0bdf75f46f62a5f91b Mon Sep 17 00:00:00 2001 From: Alain Abbas Date: Mon, 11 Nov 2024 15:49:22 +0100 Subject: [PATCH 1/3] save --- package.json | 2 + src/components/identityForm/actions.vue | 38 ++- src/components/inputNewPassword.vue | 326 ++++++++++++++++++++++++ yarn.lock | 23 ++ 4 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 src/components/inputNewPassword.vue diff --git a/package.json b/package.json index 56ca89b..f17d6b9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "@quasar/extras": "^1.16.9", "@vueuse/router": "^10.7.2", "cookie": "^0.6.0", + "fast-password-entropy": "^1.1.1", + "hibp": "^14.1.2", "moment": "^2.30.1", "openapi-fetch": "^0.8.2", "pinia": "^2.1.7", diff --git a/src/components/identityForm/actions.vue b/src/components/identityForm/actions.vue index ac83596..7419730 100644 --- a/src/components/identityForm/actions.vue +++ b/src/components/identityForm/actions.vue @@ -13,6 +13,8 @@ div.flex :true-value="1" :false-value="0" ) + q-btn.q-mx-xs(@click="resetPasswordModal = true" color="red-8" icon="mdi-account-key" :disabled="props.identity.state != IdentityState.SYNCED") + q-tooltip.text-body2(slot="trigger") Définir le mot de passe q-btn.q-mx-xs(@click="sendInit" color="primary" icon="mdi-email-arrow-right" :disabled="props.identity.state != IdentityState.SYNCED") q-tooltip.text-body2(slot="trigger") Envoyer le mail d'invitation q-btn.q-mx-xs(@click="submit" color="positive" icon="mdi-check" v-show="!isNew" v-if="crud.update") @@ -24,6 +26,24 @@ div.flex q-tooltip.text-body2(slot="trigger") Voir les logs de l'identité q-btn.q-mx-xs(v-if="props.identity?._id" @click="deleteIdentity" color="negative" icon="mdi-delete") q-tooltip.text-body2(slot="trigger") Supprimer l'identité + q-dialog(v-model="resetPasswordModal" persistent medium) + q-card(style="width:800px") + q-card-section(class="text-h6 bg-primary text-white") definition du mot de passe + q-card-section + input-new-password(v-model="newpassword" + :min="passwordPolicies.len" + :min-upper="passwordPolicies.hasUpperCase" + :min-lower="passwordPolicies.hasLowerCase" + :min-number="passwordPolicies.hasNumbers" + :min-special="passwordPolicies.hasSpecialChars" + :min-entropy="passwordPolicies.minComplexity" + :entropy-bad="passwordPolicies.minComplexity" + :entropy-good="passwordPolicies.goodComplexity" + :check-pwned="passwordPolicies.checkPwned") + q-card-actions(align="right" class="bg-white text-teal") + q-btn( label="Abandonner" color="negative" @click="resetPasswordModal = false" ) + q-btn( label="Sauver" color="positive" @click="resetPasswordModal = false" :disabled="newpassword === ''") + + + diff --git a/yarn.lock b/yarn.lock index 2e8880e..3751c61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3390,6 +3390,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-password-entropy@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fast-password-entropy/-/fast-password-entropy-1.1.1.tgz#47ba9933095fd5a32fb184915fc8e76ee19cf429" + integrity sha512-dxm29/BPFrNgyEDygg/lf9c2xQR0vnQhG7+hZjAI39M/3um9fD4xiqG6F0ZjW6bya5m9CI0u6YryHGRtxCGCiw== + fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" @@ -3784,6 +3789,14 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hibp@^14.1.2: + version "14.1.2" + resolved "https://registry.yarnpkg.com/hibp/-/hibp-14.1.2.tgz#a484b5ed24e4bd916ff0b4e63e13347545a645f7" + integrity sha512-DAMzWEEsjKFZMv4g8mDp1qPPo4FPmwLPhTlnJ6I1sBiC0x5FhjSyLhwvGA90uHCl8/6ckHwlgNuoinYrBCh3cQ== + dependencies: + jssha "^3.3.1" + undici "^6.14.1" + hookable@^5.5.3: version "5.5.3" resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" @@ -4233,6 +4246,11 @@ jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jssha@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jssha/-/jssha-3.3.1.tgz#c5b7fc7fb9aa745461923b87df0e247dd59c7ea8" + integrity sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ== + jstransformer@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" @@ -6815,6 +6833,11 @@ undici@^5.28.2: dependencies: "@fastify/busboy" "^2.0.0" +undici@^6.14.1: + version "6.20.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.20.1.tgz#fbb87b1e2b69d963ff2d5410a40ffb4c9e81b621" + integrity sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA== + unenv@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.9.0.tgz#469502ae85be1bd3a6aa60f810972b1a904ca312" From 983d48a30333cabb0ac116c58977955c1338f3d0 Mon Sep 17 00:00:00 2001 From: Alain Abbas Date: Wed, 27 Nov 2024 12:49:15 +0100 Subject: [PATCH 2/3] save --- src/components/identityForm/actions.vue | 50 +++++++------- src/components/inputNewPassword.vue | 86 +++++++++++-------------- 2 files changed, 61 insertions(+), 75 deletions(-) diff --git a/src/components/identityForm/actions.vue b/src/components/identityForm/actions.vue index 7419730..08ee53d 100644 --- a/src/components/identityForm/actions.vue +++ b/src/components/identityForm/actions.vue @@ -30,19 +30,10 @@ div.flex q-card(style="width:800px") q-card-section(class="text-h6 bg-primary text-white") definition du mot de passe q-card-section - input-new-password(v-model="newpassword" - :min="passwordPolicies.len" - :min-upper="passwordPolicies.hasUpperCase" - :min-lower="passwordPolicies.hasLowerCase" - :min-number="passwordPolicies.hasNumbers" - :min-special="passwordPolicies.hasSpecialChars" - :min-entropy="passwordPolicies.minComplexity" - :entropy-bad="passwordPolicies.minComplexity" - :entropy-good="passwordPolicies.goodComplexity" - :check-pwned="passwordPolicies.checkPwned") + input-new-password(v-model="newpassword") q-card-actions(align="right" class="bg-white text-teal") q-btn( label="Abandonner" color="negative" @click="resetPasswordModal = false" ) - q-btn( label="Sauver" color="positive" @click="resetPasswordModal = false" :disabled="newpassword === ''") + q-btn( label="Sauver" color="positive" @click="doChangePassword" :disabled="newpassword === ''") @@ -57,20 +48,7 @@ import { useIdentityStates } from '~/composables' import { useErrorHandling } from '#imports' import InputNewPassword from "~/components/inputNewPassword.vue"; const resetPasswordModal=ref(false) -const passwordPolicies = ref({ - bannedTime: 3600, - checkPwned: true, - goodComplexity: 60, - hasLowerCase: 1, - hasNumbers: 1, - hasSpecialChars: 1, - hasUpperCase: 1, - len: 10, - maxRetry: 10, - minComplexity: 20, - resetBySms: false, - redirectUrl: '' -}) + const newpassword=ref('') type IdentityResponse = operations['IdentitiesController_search']['responses']['200']['content']['application/json'] @@ -97,6 +75,27 @@ const { handleError } = useErrorHandling() const emits = defineEmits(['submit', 'sync', 'logs', 'create', 'delete']) +async function doChangePassword(){ + const requestOptions={method: 'POST', + body:JSON.stringify({id:props.identity._id,newPassword: newpassword.value})} + try{ + const data=await $http.post('/management/identities/forcepassword', requestOptions) + $q.notify({ + message: 'Le mot de passe a été changé : ', + color: 'positive', + position: 'top-right', + icon: 'mdi-check-circle-outline', + }) + }catch(error){ + $q.notify({ + message: 'Impossible de modifier le mot de passe : ' + error.response._data.message, + color: 'negative', + position: 'top-right', + icon: 'mdi-alert-circle-outline', + }) + } + resetPasswordModal.value = false +} async function submit() { // console.log('submit from actions') emits('submit') @@ -135,7 +134,6 @@ async function activate(){ bouton="Activer" initialStatus=0 } - debugger if (showActivate() === false){ props.identity.dataStatus = initialStatus return diff --git a/src/components/inputNewPassword.vue b/src/components/inputNewPassword.vue index 1fad79f..eadc38a 100644 --- a/src/components/inputNewPassword.vue +++ b/src/components/inputNewPassword.vue @@ -20,7 +20,7 @@  doit avoir au moins {{min}} caractères

-

+

 doit comporter au moins {{minUpper}} majuscules

@@ -79,44 +79,29 @@ const progress=ref(0) const progress_color=ref('red') const typePasswordProp=ref('password') const typeConfirmProp=ref('password') -const props = defineProps({ - min: { - type: Number, - default:8}, - minUpper:{ - type: Number, - default:1 - }, - minLower:{ - type: Number, - default:1 - }, - minNumber:{ - type: Number, - default:1 - }, - minSpecial:{ - type:Number, - default:1 - }, - minEntropy:{ - type:Number, - default:30 - }, - entropyBad:{ - type:Number, - default:10 - }, - entropyGood:{ - type:Number, - default:80 - }, - checkPwned:{ - type:Boolean, - default:true - } -}) +const minLower=ref(1) +const minUpper=ref(1) +const minNumber=ref(1) +const minSpecial=ref(1) +const minEntropy=ref(20) +const checkPwned=ref(false) +const min=ref(5) +const { data: props} = await useHttp('/management/passwd/getpolicies', + { + method:'GET', + transform:(result)=> { + return result.data + } + } +) +minLower.value=props.value.hasLowerCase +minUpper.value=props.value.hasUpperCase +minNumber.value=props.value.hasNumbers +minSpecial.value=props.value.hasSpecialChars +minEntropy.value=props.value.minComplexity +checkPwned.value=props.value.checkPwned +min.value=props.value.len async function checkPassword(ev, type) { let newP = newPassword.value let confirmP = confirmNewPassword.value @@ -130,13 +115,16 @@ async function checkPassword(ev, type) { console.log('emit ' + newPassword.value) //avant d accepter on cherche dans l api de pwned try{ - if (props.checkPwned === true ){ + if (checkPwned.value === true ){ const numPwns = await pwnedPassword(newP); if (numPwns >0){ iconIsPwnedOK(false) $q.notify({ message: 'Ce mot de passe est déjà apparu lors d\'une violation de données. Vous ne pouvez pas l\'utiliser', + html:true, + color: 'negative', + multiLine: true, }) emit('update:modelValue', '') return @@ -164,7 +152,7 @@ async function checkPassword(ev, type) { function checkPolicy(password) { has_len.value='highlight_off' let statut=true - if (props.minSpecial >= 1){ + if (minSpecial.value >= 1){ if (/[!@#\$%\^\&*\)\(+=._-]/.test(password) === false){ pwdColor.value = 'red' iconSpecialOK(false) @@ -173,7 +161,7 @@ function checkPolicy(password) { iconSpecialOK(true) } } - if (props.minNumber >= 1) { + if (minNumber.value >= 1) { if (/\d/.test(password) === false) { pwdColor.value = 'red' iconNumberOK(false) @@ -182,7 +170,7 @@ function checkPolicy(password) { iconNumberOK(true) } } - if (props.minLower >= 1) { + if (minLower.value >= 1) { if (/[a-z]/.test(password) === false) { pwdColor.value = 'red' iconLowerOK(false) @@ -191,7 +179,7 @@ function checkPolicy(password) { iconLowerOK(true) } } - if (props.minUpper >= 1) { + if (minUpper.value >= 1) { if (/[A-Z]/.test(password) === false) { pwdColor.value = 'red' iconUpperOK(false) @@ -200,8 +188,8 @@ function checkPolicy(password) { iconUpperOK(true) } } - if (password.length < props.min) { - console.log('trop court ' + props.min) + if (password.length < min.value) { + console.log('trop court ' + min.value) iconLenOK(false) statut=false }else{ @@ -287,18 +275,18 @@ function iconIsPwnedOK(value){ } function complexity(password){ console.log(stringEntropy(password)) - if (props.minEntropy > 0){ + if (minEntropy.value > 0){ let c = stringEntropy(password) progress.value = c / 100 console.log('entropy' + c) - if (c < props.entropyBad) { + if (c < props.value.minComplexity) { progress_color.value = 'red' - } else if (c >= props.entropyBad && c < props.entropyGood) { + } else if (c >= props.value.minComplexity && c < props.value.goodComplexity) { progress_color.value = 'warning' } else { progress_color.value = 'green' } - if (c >= props.minEntropy) { + if (c >= minEntropy.value) { return true } else { return false From ff1dc7d5008f36d955ee0d49354ed408ada744b1 Mon Sep 17 00:00:00 2001 From: Alain Abbas Date: Sun, 19 Jan 2025 22:12:24 +0100 Subject: [PATCH 3/3] Update actions.vue --- src/components/identityForm/actions.vue | 44 +++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/components/identityForm/actions.vue b/src/components/identityForm/actions.vue index 08ee53d..f98f3cc 100644 --- a/src/components/identityForm/actions.vue +++ b/src/components/identityForm/actions.vue @@ -11,8 +11,11 @@ div.flex label="Activation" v-model="props.identity.dataStatus" :true-value="1" - :false-value="0" + :indeterminate-value="-2" + :false-value="-3" ) + q-btn.q-mx-xs( @click="forceChangePassword()" color="orange-8" icon="mdi-lock-reset" :disabled="props.identity.state != IdentityState.SYNCED") + q-tooltip.text-body2(slot="trigger") Obliger l'utilisateur à changer son mot de passe q-btn.q-mx-xs(@click="resetPasswordModal = true" color="red-8" icon="mdi-account-key" :disabled="props.identity.state != IdentityState.SYNCED") q-tooltip.text-body2(slot="trigger") Définir le mot de passe q-btn.q-mx-xs(@click="sendInit" color="primary" icon="mdi-email-arrow-right" :disabled="props.identity.state != IdentityState.SYNCED") @@ -48,7 +51,7 @@ import { useIdentityStates } from '~/composables' import { useErrorHandling } from '#imports' import InputNewPassword from "~/components/inputNewPassword.vue"; const resetPasswordModal=ref(false) - +const forcePasswordModal=ref(false) const newpassword=ref('') type IdentityResponse = operations['IdentitiesController_search']['responses']['200']['content']['application/json'] @@ -119,6 +122,43 @@ function showActivate(){ return false } } +async function forceChangePassword(){ + $q.dialog({ + title: 'Confirmation', + message: "Voulez vous forcer le changement de mot de passe ? ", + persistent: true, + ok: { + push: true, + color: 'positive', + label: 'Forcer', + }, + cancel: { + push: true, + color: 'negative', + label: 'Annuler', + }, + }).onOk(async() => { + const requestOptions={method: 'POST', + body:JSON.stringify({id:props.identity._id})} + try{ + const data=await $http.post('/management/identities/needtochangepassword', requestOptions) + $q.notify({ + message: 'LE changement de mot de passe est forcé : ', + color: 'positive', + position: 'top-right', + icon: 'mdi-check-circle-outline', + }) + }catch(error){ + $q.notify({ + message: 'Impossible de forcer le changement de mot de passe : ' + error.response._data.message, + color: 'negative', + position: 'top-right', + icon: 'mdi-alert-circle-outline', + }) + } + + }) +} async function activate(){