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

@ -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'