Client - add password strength and suggestions

This commit is contained in:
Sam 2022-02-26 13:40:13 +01:00
parent 9bb894dc03
commit 29760e57f1
9 changed files with 302 additions and 6 deletions

View File

@ -13,6 +13,10 @@
"dependencies": {
"@tmcw/togeojson": "^4.5.0",
"@vue-leaflet/vue-leaflet": "^0.6.1",
"@zxcvbn-ts/core": "^2.0.0",
"@zxcvbn-ts/language-common": "^2.0.0",
"@zxcvbn-ts/language-en": "^2.0.0",
"@zxcvbn-ts/language-fr": "^1.2.0",
"axios": "^0.26.0",
"chart.js": "^3.7.0",
"chartjs-plugin-datalabels": "^2.0.0",

View File

@ -5,20 +5,23 @@
:placeholder="placeholder"
:required="required"
:type="showPassword ? 'text' : 'password'"
v-model="passwordValue"
minlength="8"
@input="updatePassword"
@invalid="invalidPassword"
/>
<span class="show-password" @click="togglePassword">
<div class="show-password" @click="togglePassword">
{{ $t(`user.${showPassword ? 'HIDE' : 'SHOW'}_PASSWORD`) }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, toRefs, withDefaults } from 'vue'
import { Ref, ref, toRefs, watch, withDefaults } from 'vue'
interface Props {
disabled?: boolean
password?: string
placeholder?: string
required?: boolean
}
@ -27,8 +30,10 @@
disabled: false,
required: false,
})
const { disabled, placeholder, required } = toRefs(props)
const showPassword = ref(false)
const { disabled, password, placeholder, required } = toRefs(props)
const showPassword: Ref<boolean> = ref(false)
const passwordValue: Ref<string> = ref('')
const emit = defineEmits(['updatePassword', 'passwordError'])
@ -41,6 +46,15 @@
function invalidPassword() {
emit('passwordError')
}
watch(
() => password.value,
(newPassword) => {
if (newPassword === '') {
passwordValue.value = ''
}
}
)
</script>
<style lang="scss" scoped>
@ -49,6 +63,7 @@
.password-input {
display: flex;
flex-direction: column;
.show-password {
font-style: italic;
font-size: 0.85em;

View File

@ -0,0 +1,154 @@
<template>
<div class="password-strength">
<input
class="password-slider"
:class="`strength-${passwordScore}`"
:style="{ backgroundSize: backgroundSize }"
type="range"
:value="passwordScore"
min="0"
max="4"
step="1"
/>
<div v-if="passwordStrength" class="password-strength-details">
<span class="password-strength-value">
{{ $t('user.PASSWORD_STRENGTH.LABEL') }}:
{{ $t(`user.PASSWORD_STRENGTH.${passwordStrength}`) }}
</span>
<div class="info-box" v-if="passwordSuggestions.length > 0">
<ul class="password-feedback">
<li v-for="suggestion in passwordSuggestions" :key="suggestion">
{{ $t(`user.PASSWORD_STRENGTH.SUGGESTIONS.${suggestion}`) }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { zxcvbn } from '@zxcvbn-ts/core'
import {
ComputedRef,
Ref,
computed,
ref,
onBeforeMount,
toRefs,
watch,
} from 'vue'
import { ROOT_STORE } from '@/store/constants'
import { useStore } from '@/use/useStore'
import { getPasswordStrength, setZxcvbnOptions } from '@/utils/password'
interface Props {
password: string
}
const props = defineProps<Props>()
const { password } = toRefs(props)
const store = useStore()
const language: ComputedRef<string> = computed(
() => store.getters[ROOT_STORE.GETTERS.LANGUAGE]
)
const passwordScore: Ref<number> = ref(0)
const passwordStrength: Ref<string> = ref('')
const passwordSuggestions: Ref<string[]> = ref([])
const backgroundSize = ref('0% 100%')
onBeforeMount(async () => await setZxcvbnOptions(language.value))
function calculatePasswordStrength(password: string) {
let zxcvbnResult = zxcvbn(password)
passwordScore.value = zxcvbnResult.score
passwordStrength.value = getPasswordStrength(passwordScore.value)
passwordSuggestions.value = zxcvbnResult.feedback.suggestions
backgroundSize.value = (passwordScore.value * 100) / 4 + '% 100%'
}
watch(
() => language.value,
async (newLanguageValue) => {
await setZxcvbnOptions(newLanguageValue)
}
)
watch(
() => password.value,
async (newPassword) => {
calculatePasswordStrength(newPassword)
}
)
</script>
<style lang="scss" scoped>
@import '~@/scss/vars.scss';
.password-strength {
cursor: default;
display: flex;
flex-direction: column;
@mixin slider-background-image($color) {
background: var(--password-bg-color);
background-image: -webkit-gradient(
linear,
20% 0%,
20% 100%,
color-stop(0%, $color),
color-stop(100%, $color)
);
background-image: -webkit-linear-gradient(left, $color 0%, $color 100%);
background-image: -moz-linear-gradient(left, $color 0%, $color 100%);
background-image: -o-linear-gradient(to right, $color 0%, $color 100%);
background-image: linear-gradient(to right, $color 0%, $color 100%);
background-repeat: no-repeat;
}
.password-slider {
-webkit-appearance: none;
appearance: none;
border: none;
border-radius: 8px;
height: 5px;
outline: none;
padding: 0;
}
.strength-0,
.strength-1 {
@include slider-background-image(var(--password-color-weak));
}
.strength-2 {
@include slider-background-image(var(--password-color-medium));
}
.strength-3 {
@include slider-background-image(var(--password-color-good));
}
.strength-4 {
@include slider-background-image(var(--password-color-strong));
}
.password-slider::-webkit-slider-thumb,
.password-slider::-moz-range-thumb {
-webkit-appearance: none;
appearance: none;
opacity: 0;
}
.password-strength-details {
margin-bottom: $default-margin * 0.5;
margin-top: -1 * $default-margin;
padding: 0 $default-padding;
.password-strength-value {
font-size: 0.85em;
}
.info-box {
padding: $default-padding * 0.1 $default-padding;
.password-feedback {
padding-left: $default-padding * 2;
}
}
}
}
</style>

View File

@ -51,9 +51,14 @@
? $t('user.ENTER_PASSWORD')
: $t('user.PASSWORD')
"
:password="formData.password"
@updatePassword="updatePassword"
@passwordError="invalidateForm"
/>
<PasswordStrength
v-if="['reset', 'register'].includes(action)"
:password="formData.password"
/>
</div>
<button type="submit" :disabled="registration_disabled">
{{ $t(buttonText) }}
@ -93,6 +98,7 @@
import { useRoute } from 'vue-router'
import PasswordInput from '@/components/Common/PasswordInput.vue'
import PasswordStrength from '@/components/Common/PasswordStength.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
import { TAppConfig } from '@/types/application'
import { ILoginRegisterFormData } from '@/types/user'

View File

@ -14,6 +14,29 @@
"PASSWORD_FORGOTTEN": "Forgot password?",
"PASSWORD_RESET": "Password reset",
"PASSWORD_SENT_EMAIL_TEXT": "Check your email. If your address is in our database, you'll received an email with a link to reset your password.",
"PASSWORD_STRENGTH": {
"WEAK": "weak",
"AVERAGE": "average",
"GOOD": "good",
"STRONG": "strong",
"LABEL": "password strength",
"SUGGESTIONS": {
"l33t": "Avoid predictable letter substitutions like {'@'} for a.",
"reverseWords": "Avoid reversed spellings of common words.",
"allUppercase": "Capitalize some, but not all letters.",
"capitalization": "Capitalize more than the first letter.",
"dates": "Avoid dates and years that are associated with you.",
"recentYears": "Avoid recent years.",
"associatedYears": "Avoid years that are associated with you.",
"sequences": "Avoid common character sequences.",
"repeated": "Avoid repeated words and characters.",
"longerKeyboardPattern": "Use longer keyboard patterns and change typing direction multiple times.",
"anotherWord": "Add more words that are less common.",
"useWords": "Use multiple words, but avoid common phrases.",
"noNeed": "You can create strong passwords without using symbols, numbers, or uppercase letters.",
"pwned": "If you use this password elsewhere, you should change it."
}
},
"PASSWORD_UPDATED": "Your password have been updated. Click {0} to log in.",
"PROFILE": {
"BACK_TO_PROFILE": "Back to profile",

View File

@ -14,6 +14,28 @@
"PASSWORD_FORGOTTEN": "Mot de passe oublié ?",
"PASSWORD_RESET": "Réinitialisation du mot de passe",
"PASSWORD_SENT_EMAIL_TEXT": "Vérifiez votre boite mail. Si vote adresse est dans notre base de données, vous recevrez un email avec un lien pour réinitialiser votre mot de passe.",
"PASSWORD_STRENGTH": {
"WEAK": "faible",
"AVERAGE": "moyenne",
"GOOD": "bonne",
"STRONG": "forte",
"LABEL": "robustesse du mot de passe ",
"SUGGESTIONS": {
"l33t": "Évitez les substitutions de lettres prévisibles comme {'@'} pour a.",
"reverseWords": "Évitez les orthographes inversées des mots courants",
"allUppercase": "Mettez quelques lettres en majuscules, mais pas toutes.",
"capitalization": "Capitalisez mais pas seulement la première lettre.",
"dates": "Évitez les dates et les années qui vous sont associées. (ex: date ou année de naissance)",
"recentYears": "Évitez les dernières années.",
"associatedYears": "Évitez les années qui vous sont associées. (ex: date de naissance)",
"sequences": "Évitez les séquences de caractères courantes.",
"repeated": "Évitez les mots et les caractères répétés.",
"longerKeyboardPattern": "Utilisez des motifs de clavier plus longs et changez de sens de frappe plusieurs fois.",
"anotherWord": "Ajoutez des mots moins courants.",
"useWords": "Utilisez plusieurs mots, mais évitez les phrases courantes.",
"noNeed": "Vous pouvez créer des mots de passe forts sans utiliser de symboles, de chiffres ou de lettres majuscules."
}
},
"PASSWORD_UPDATED": "Votre mot de passe a été mis à jour. Cliquez {0} pour vous connecter.",
"PROFILE": {
"BACK_TO_PROFILE": "Revenir au profil",

View File

@ -66,6 +66,12 @@
--cell-heading-bg-color: #eeeeee;
--cell-heading-color: #696969;
--svg-filter: drop-shadow(10px 10px 10px var(--app-shadow-color))
--svg-filter: drop-shadow(10px 10px 10px var(--app-shadow-color));
--password-bg-color: #d7dadf;
--password-color-weak: #e46d6e;
--password-color-medium: #f8bc4a;
--password-color-good: #acc578;
--password-color-strong: #57c255;
}

View File

@ -0,0 +1,39 @@
import { zxcvbnOptions } from '@zxcvbn-ts/core'
export const setZxcvbnOptions = async (language: string) => {
const zxcvbnCommonPackage = await import(
/* webpackChunkName: "password" */ '@zxcvbn-ts/language-common'
)
const zxcvbnEnPackage = await import(
/* webpackChunkName: "password" */ '@zxcvbn-ts/language-en'
)
const zxcvbnFrPackage = await import(
/* webpackChunkName: "password" */ '@zxcvbn-ts/language-fr'
)
const zxcvbnLangPackages: Record<string, typeof zxcvbnEnPackage> = {
en: zxcvbnEnPackage,
fr: zxcvbnFrPackage,
}
const zxcvbnPackage = zxcvbnLangPackages[language]
const options = {
graphs: zxcvbnCommonPackage.default.adjacencyGraphs,
dictionary: {
...zxcvbnCommonPackage.default.dictionary,
...zxcvbnPackage.default.dictionary,
},
}
zxcvbnOptions.setOptions(options)
}
export const getPasswordStrength = (strength: number): string => {
switch (strength) {
case 2:
return 'AVERAGE'
case 3:
return 'GOOD'
case 4:
return 'STRONG'
default:
return 'WEAK'
}
}

View File

@ -2080,6 +2080,28 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
"@zxcvbn-ts/core@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/core/-/core-2.0.0.tgz#4b6969cd98c6b56ee75bce11c2c9d7ed1168c1db"
integrity sha512-j9XY5TQq6fldHQ5BC/3kVNcw9zIg91i7ddeIZzwL8xAq3nqi7gw/YZxPY8Ry4KE4xmcYCiB+6AG6/jHO9uylPg==
dependencies:
fastest-levenshtein "1.0.12"
"@zxcvbn-ts/language-common@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-common/-/language-common-2.0.0.tgz#77ee6c1107e116cb74e0e8c80147bc967dd2140c"
integrity sha512-RM4PmOev2pRQ1gMf5rjFKvsEb+qYTy+5YZY/g+vy7QibR66TiyD91VoOVaHArcj07wXj0J3eDpsiU+mpB1Oijg==
"@zxcvbn-ts/language-en@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-en/-/language-en-2.0.0.tgz#454b09578f8713bd204465354565972d8512de85"
integrity sha512-ijDtOYeJxBpuoXdTtyXpoOcMRTDjRiABIWX3X7T7XquZ2c6IfrKwXVrvwzVSlRhhPqaCI0tienDeQYG7631eHA==
"@zxcvbn-ts/language-fr@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-fr/-/language-fr-1.2.0.tgz#fb1d5f06d3bdf0b18bab75625a22153fe1dc4347"
integrity sha512-+iymwuu+GTlCyJTEhStxqcJfnYvhwIoo6dbXpRq4wzs0mk3uxy79UA6k9lEVy3RXu5VOq2cIpyeLtwqBR4+C7w==
abab@^2.0.3, abab@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
@ -3971,6 +3993,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fastest-levenshtein@1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"