feat(registration): improve username and password validation

Username and password are validated in the api for length and whitespaces. Previously, the api would tell the user "hey you got it wrong" but the error was not reflected properly in the UI. This change implements a client-side validation which mirrors the one from the api, allowing instant validation and better error UX.
This commit is contained in:
kolaente 2024-02-12 17:32:24 +01:00
parent 8c6d98bb02
commit 390f71b0c6
Signed by untrusted user: konrad
GPG Key ID: F40E70337AB24C9B
3 changed files with 61 additions and 17 deletions

View File

@ -10,7 +10,8 @@
autocomplete="current-password" autocomplete="current-password"
:tabindex="props.tabindex" :tabindex="props.tabindex"
@keyup.enter="e => $emit('submit', e)" @keyup.enter="e => $emit('submit', e)"
@focusout="validate" @focusout="() => {validate(); validateAfterFirst = true}"
@keyup="() => {validateAfterFirst ? validate() : null}"
@input="handleInput" @input="handleInput"
> >
<BaseButton <BaseButton
@ -23,16 +24,17 @@
</BaseButton> </BaseButton>
</div> </div>
<p <p
v-if="!isValid" v-if="isValid !== true"
class="help is-danger" class="help is-danger"
> >
{{ $t('user.auth.passwordRequired') }} {{ isValid }}
</p> </p>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {ref, watch} from 'vue' import {ref, watch} from 'vue'
import {useDebounceFn} from '@vueuse/core' import {useDebounceFn} from '@vueuse/core'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
const props = defineProps({ const props = defineProps({
@ -41,12 +43,12 @@ const props = defineProps({
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input. // This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
validateInitially: Boolean, validateInitially: Boolean,
}) })
const emit = defineEmits(['submit', 'update:modelValue']) const emit = defineEmits(['submit', 'update:modelValue'])
const {t} = useI18n()
const passwordFieldType = ref('password') const passwordFieldType = ref('password')
const password = ref('') const password = ref('')
const isValid = ref(!props.validateInitially) const isValid = ref<true | string>(props.validateInitially === true ? true : '')
const validateAfterFirst = ref(false)
watch( watch(
() => props.validateInitially, () => props.validateInitially,
@ -55,7 +57,22 @@ watch(
) )
const validate = useDebounceFn(() => { const validate = useDebounceFn(() => {
isValid.value = password.value !== '' if (password.value === '') {
isValid.value = t('user.auth.passwordRequired')
return
}
if (password.value.length < 8) {
isValid.value = t('user.auth.passwordNotMin')
return
}
if (password.value.length > 250) {
isValid.value = t('user.auth.passwordNotMax')
return
}
isValid.value = true
}, 100) }, 100)
function togglePasswordFieldType() { function togglePasswordFieldType() {

View File

@ -57,7 +57,11 @@
"logout": "Logout", "logout": "Logout",
"emailInvalid": "Please enter a valid email address.", "emailInvalid": "Please enter a valid email address.",
"usernameRequired": "Please provide a username.", "usernameRequired": "Please provide a username.",
"usernameMustNotContainSpace": "The username must not contain spaces.",
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.", "passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"showPassword": "Show the password", "showPassword": "Show the password",
"hidePassword": "Hide the password", "hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?", "noAccountYet": "Don't have an account yet?",

View File

@ -28,21 +28,24 @@
type="text" type="text"
autocomplete="username" autocomplete="username"
@keyup.enter="submit" @keyup.enter="submit"
@focusout="validateUsername" @focusout="() => {validateUsername(); validateUsernameAfterFirst = true}"
@keyup="() => {validateUsernameAfterFirst ? validateUsername() : null}"
> >
</div> </div>
<p <p
v-if="!usernameValid" v-if="usernameValid !== true"
class="help is-danger" class="help is-danger"
> >
{{ $t('user.auth.usernameRequired') }} {{ usernameValid }}
</p> </p>
</div> </div>
<div class="field"> <div class="field">
<label <label
class="label" class="label"
for="email" for="email"
>{{ $t('user.auth.email') }}</label> >
{{ $t('user.auth.email') }}
</label>
<div class="control"> <div class="control">
<input <input
id="email" id="email"
@ -53,7 +56,8 @@
required required
type="email" type="email"
@keyup.enter="submit" @keyup.enter="submit"
@focusout="validateEmail" @focusout="() => {validateEmail(); validateEmailAfterFirst = true}"
@keyup="() => {validateEmailAfterFirst ? validateEmail() : null}"
> >
</div> </div>
<p <p
@ -107,7 +111,8 @@
<script setup lang="ts"> <script setup lang="ts">
import {useDebounceFn} from '@vueuse/core' import {useDebounceFn} from '@vueuse/core'
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue' import {computed, onBeforeMount, reactive, ref, toRaw} from 'vue'
import {useI18n} from 'vue-i18n'
import router from '@/router' import router from '@/router'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
@ -117,6 +122,7 @@ import Password from '@/components/input/password.vue'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
const {t} = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
const configStore = useConfigStore() const configStore = useConfigStore()
@ -142,13 +148,30 @@ const DEBOUNCE_TIME = 100
// debouncing to prevent error messages when clicking on the log in button // debouncing to prevent error messages when clicking on the log in button
const emailValid = ref(true) const emailValid = ref(true)
const validateEmailAfterFirst = ref(false)
const validateEmail = useDebounceFn(() => { const validateEmail = useDebounceFn(() => {
emailValid.value = isEmail(credentials.email) emailValid.value = isEmail(credentials.email)
}, DEBOUNCE_TIME) }, DEBOUNCE_TIME)
const usernameValid = ref(true) const usernameValid = ref<true | string>(true)
const validateUsernameAfterFirst = ref(false)
const validateUsername = useDebounceFn(() => { const validateUsername = useDebounceFn(() => {
usernameValid.value = credentials.username !== '' if (credentials.username === '') {
usernameValid.value = t('user.auth.usernameRequired')
return
}
if(credentials.username.indexOf(' ') !== -1) {
usernameValid.value = t('user.auth.usernameMustNotContainSpace')
return
}
if(credentials.username.indexOf('://') !== -1) {
usernameValid.value = t('user.auth.usernameMustNotLookLikeUrl')
return
}
usernameValid.value = true
}, DEBOUNCE_TIME) }, DEBOUNCE_TIME)
const everythingValid = computed(() => { const everythingValid = computed(() => {