Client - add password strength and suggestions
This commit is contained in:
parent
9bb894dc03
commit
29760e57f1
@ -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",
|
||||
|
@ -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;
|
||||
|
154
fittrackee_client/src/components/Common/PasswordStength.vue
Normal file
154
fittrackee_client/src/components/Common/PasswordStength.vue
Normal 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>
|
@ -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'
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
||||
}
|
39
fittrackee_client/src/utils/password.ts
Normal file
39
fittrackee_client/src/utils/password.ts
Normal 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'
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user