Client - add password strength and suggestions
This commit is contained in:
		| @@ -13,6 +13,10 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@tmcw/togeojson": "^4.5.0", |     "@tmcw/togeojson": "^4.5.0", | ||||||
|     "@vue-leaflet/vue-leaflet": "^0.6.1", |     "@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", |     "axios": "^0.26.0", | ||||||
|     "chart.js": "^3.7.0", |     "chart.js": "^3.7.0", | ||||||
|     "chartjs-plugin-datalabels": "^2.0.0", |     "chartjs-plugin-datalabels": "^2.0.0", | ||||||
|   | |||||||
| @@ -5,20 +5,23 @@ | |||||||
|       :placeholder="placeholder" |       :placeholder="placeholder" | ||||||
|       :required="required" |       :required="required" | ||||||
|       :type="showPassword ? 'text' : 'password'" |       :type="showPassword ? 'text' : 'password'" | ||||||
|  |       v-model="passwordValue" | ||||||
|       minlength="8" |       minlength="8" | ||||||
|       @input="updatePassword" |       @input="updatePassword" | ||||||
|       @invalid="invalidPassword" |       @invalid="invalidPassword" | ||||||
|     /> |     /> | ||||||
|     <span class="show-password" @click="togglePassword"> |     <div class="show-password" @click="togglePassword"> | ||||||
|       {{ $t(`user.${showPassword ? 'HIDE' : 'SHOW'}_PASSWORD`) }} |       {{ $t(`user.${showPassword ? 'HIDE' : 'SHOW'}_PASSWORD`) }} | ||||||
|     </span> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|   import { ref, toRefs, withDefaults } from 'vue' |   import { Ref, ref, toRefs, watch, withDefaults } from 'vue' | ||||||
|  |  | ||||||
|   interface Props { |   interface Props { | ||||||
|     disabled?: boolean |     disabled?: boolean | ||||||
|  |     password?: string | ||||||
|     placeholder?: string |     placeholder?: string | ||||||
|     required?: boolean |     required?: boolean | ||||||
|   } |   } | ||||||
| @@ -27,8 +30,10 @@ | |||||||
|     disabled: false, |     disabled: false, | ||||||
|     required: false, |     required: false, | ||||||
|   }) |   }) | ||||||
|   const { disabled, placeholder, required } = toRefs(props) |   const { disabled, password, placeholder, required } = toRefs(props) | ||||||
|   const showPassword = ref(false) |  | ||||||
|  |   const showPassword: Ref<boolean> = ref(false) | ||||||
|  |   const passwordValue: Ref<string> = ref('') | ||||||
|  |  | ||||||
|   const emit = defineEmits(['updatePassword', 'passwordError']) |   const emit = defineEmits(['updatePassword', 'passwordError']) | ||||||
|  |  | ||||||
| @@ -41,6 +46,15 @@ | |||||||
|   function invalidPassword() { |   function invalidPassword() { | ||||||
|     emit('passwordError') |     emit('passwordError') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   watch( | ||||||
|  |     () => password.value, | ||||||
|  |     (newPassword) => { | ||||||
|  |       if (newPassword === '') { | ||||||
|  |         passwordValue.value = '' | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ) | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -49,6 +63,7 @@ | |||||||
|   .password-input { |   .password-input { | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|  |  | ||||||
|     .show-password { |     .show-password { | ||||||
|       font-style: italic; |       font-style: italic; | ||||||
|       font-size: 0.85em; |       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.ENTER_PASSWORD') | ||||||
|                   : $t('user.PASSWORD') |                   : $t('user.PASSWORD') | ||||||
|               " |               " | ||||||
|  |               :password="formData.password" | ||||||
|               @updatePassword="updatePassword" |               @updatePassword="updatePassword" | ||||||
|               @passwordError="invalidateForm" |               @passwordError="invalidateForm" | ||||||
|             /> |             /> | ||||||
|  |             <PasswordStrength | ||||||
|  |               v-if="['reset', 'register'].includes(action)" | ||||||
|  |               :password="formData.password" | ||||||
|  |             /> | ||||||
|           </div> |           </div> | ||||||
|           <button type="submit" :disabled="registration_disabled"> |           <button type="submit" :disabled="registration_disabled"> | ||||||
|             {{ $t(buttonText) }} |             {{ $t(buttonText) }} | ||||||
| @@ -93,6 +98,7 @@ | |||||||
|   import { useRoute } from 'vue-router' |   import { useRoute } from 'vue-router' | ||||||
|  |  | ||||||
|   import PasswordInput from '@/components/Common/PasswordInput.vue' |   import PasswordInput from '@/components/Common/PasswordInput.vue' | ||||||
|  |   import PasswordStrength from '@/components/Common/PasswordStength.vue' | ||||||
|   import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants' |   import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants' | ||||||
|   import { TAppConfig } from '@/types/application' |   import { TAppConfig } from '@/types/application' | ||||||
|   import { ILoginRegisterFormData } from '@/types/user' |   import { ILoginRegisterFormData } from '@/types/user' | ||||||
|   | |||||||
| @@ -14,6 +14,29 @@ | |||||||
|   "PASSWORD_FORGOTTEN": "Forgot password?", |   "PASSWORD_FORGOTTEN": "Forgot password?", | ||||||
|   "PASSWORD_RESET": "Password reset", |   "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_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.", |   "PASSWORD_UPDATED": "Your password have been updated. Click {0} to log in.", | ||||||
|   "PROFILE": { |   "PROFILE": { | ||||||
|     "BACK_TO_PROFILE": "Back to profile", |     "BACK_TO_PROFILE": "Back to profile", | ||||||
|   | |||||||
| @@ -14,6 +14,28 @@ | |||||||
|   "PASSWORD_FORGOTTEN": "Mot de passe oublié ?", |   "PASSWORD_FORGOTTEN": "Mot de passe oublié ?", | ||||||
|   "PASSWORD_RESET": "Réinitialisation du mot de passe", |   "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_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.", |   "PASSWORD_UPDATED": "Votre mot de passe a été mis à jour. Cliquez {0} pour vous connecter.", | ||||||
|   "PROFILE": { |   "PROFILE": { | ||||||
|     "BACK_TO_PROFILE": "Revenir au profil", |     "BACK_TO_PROFILE": "Revenir au profil", | ||||||
|   | |||||||
| @@ -66,6 +66,12 @@ | |||||||
|   --cell-heading-bg-color: #eeeeee; |   --cell-heading-bg-color: #eeeeee; | ||||||
|   --cell-heading-color: #696969; |   --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" |   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" | ||||||
|   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== |   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: | abab@^2.0.3, abab@^2.0.5: | ||||||
|   version "2.0.5" |   version "2.0.5" | ||||||
|   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" |   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" |   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" | ||||||
|   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= |   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: | fastq@^1.6.0: | ||||||
|   version "1.13.0" |   version "1.13.0" | ||||||
|   resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" |   resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user