Client - add password strength and suggestions
This commit is contained in:
		| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user