feat: move password to separate component
This commit is contained in:
parent
6041ad1482
commit
0322daf4d4
5 changed files with 98 additions and 86 deletions
85
src/components/input/password.vue
Normal file
85
src/components/input/password.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<div class="password-field">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||||
|
required
|
||||||
|
:type="passwordFieldType"
|
||||||
|
autocomplete="current-password"
|
||||||
|
@keyup.enter="e => $emit('submit', e)"
|
||||||
|
:tabindex="props.tabindex"
|
||||||
|
@focusout="validate"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
@click="togglePasswordFieldType"
|
||||||
|
class="password-field-type-toggle"
|
||||||
|
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
||||||
|
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
||||||
|
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="help is-danger" v-if="!isValid">
|
||||||
|
{{ $t('user.auth.passwordRequired') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref, watch} from "vue";
|
||||||
|
import {useDebounceFn} from "@vueuse/core";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabindex: String,
|
||||||
|
modelValue: String,
|
||||||
|
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
|
||||||
|
validateInitially: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit', 'update:modelValue'])
|
||||||
|
|
||||||
|
const passwordFieldType = ref<String>('password')
|
||||||
|
const password = ref<String>('')
|
||||||
|
const isValid = ref<Boolean>(!props.validateInitially)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.validateInitially,
|
||||||
|
(doValidate: Boolean) => {
|
||||||
|
if (doValidate) {
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
useDebounceFn(() => {
|
||||||
|
isValid.value = password.value !== ''
|
||||||
|
}, 100)()
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePasswordFieldType() {
|
||||||
|
passwordFieldType.value = passwordFieldType.value === 'password'
|
||||||
|
? 'text'
|
||||||
|
: 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(e) {
|
||||||
|
password.value = e.target.value
|
||||||
|
emit('update:modelValue', e.target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.password-field {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-field-type-toggle {
|
||||||
|
position: absolute;
|
||||||
|
color: var(--grey-400);
|
||||||
|
top: 50%;
|
||||||
|
right: 1rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,4 +4,3 @@
|
||||||
@import "task";
|
@import "task";
|
||||||
@import "tasks";
|
@import "tasks";
|
||||||
@import "namespaces";
|
@import "namespaces";
|
||||||
@import 'password-field-toggle';
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
.password-field-type-toggle {
|
|
||||||
position: absolute;
|
|
||||||
color: var(--grey-400);
|
|
||||||
top: 50%;
|
|
||||||
right: 1rem;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
|
@ -39,31 +39,7 @@
|
||||||
{{ $t('user.auth.forgotPassword') }}
|
{{ $t('user.auth.forgotPassword') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<password tabindex="2" @submit="submit" v-model="password" :validate-initially="validatePasswordInitially"/>
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
|
||||||
ref="password"
|
|
||||||
required
|
|
||||||
:type="passwordFieldType"
|
|
||||||
autocomplete="current-password"
|
|
||||||
@keyup.enter="submit"
|
|
||||||
tabindex="2"
|
|
||||||
@focusout="validateField('password')"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
@click="togglePasswordFieldType"
|
|
||||||
class="password-field-type-toggle"
|
|
||||||
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
|
||||||
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
|
||||||
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="!passwordValid">
|
|
||||||
{{ $t('user.auth.passwordRequired') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field" v-if="needsTotpPasscode">
|
<div class="field" v-if="needsTotpPasscode">
|
||||||
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
|
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
|
||||||
|
@ -87,7 +63,6 @@
|
||||||
@click="submit"
|
@click="submit"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
tabindex="4"
|
tabindex="4"
|
||||||
:disabled="!allFieldsValid"
|
|
||||||
>
|
>
|
||||||
{{ $t('user.auth.login') }}
|
{{ $t('user.auth.login') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
|
@ -129,9 +104,11 @@ import {getErrorText} from '@/message'
|
||||||
import Message from '@/components/misc/message'
|
import Message from '@/components/misc/message'
|
||||||
import {redirectToProvider} from '../../helpers/redirectToProvider'
|
import {redirectToProvider} from '../../helpers/redirectToProvider'
|
||||||
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
|
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
|
||||||
|
import Password from '@/components/input/password'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
Password,
|
||||||
Message,
|
Message,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -139,8 +116,8 @@ export default {
|
||||||
confirmedEmailSuccess: false,
|
confirmedEmailSuccess: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
usernameValid: true,
|
usernameValid: true,
|
||||||
passwordValid: true,
|
password: '',
|
||||||
passwordFieldType: 'password',
|
validatePasswordInitially: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
@ -185,9 +162,6 @@ export default {
|
||||||
this.openidConnect.providers &&
|
this.openidConnect.providers &&
|
||||||
this.openidConnect.providers.length > 0
|
this.openidConnect.providers.length > 0
|
||||||
},
|
},
|
||||||
allFieldsValid() {
|
|
||||||
return this.usernameValid && this.passwordValid
|
|
||||||
},
|
|
||||||
...mapState({
|
...mapState({
|
||||||
registrationEnabled: state => state.config.registrationEnabled,
|
registrationEnabled: state => state.config.registrationEnabled,
|
||||||
loading: LOADING,
|
loading: LOADING,
|
||||||
|
@ -215,12 +189,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
togglePasswordFieldType() {
|
|
||||||
this.passwordFieldType = this.passwordFieldType === 'password'
|
|
||||||
? 'text'
|
|
||||||
: 'password'
|
|
||||||
},
|
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
this.errorMessage = ''
|
this.errorMessage = ''
|
||||||
// Some browsers prevent Vue bindings from working with autofilled values.
|
// Some browsers prevent Vue bindings from working with autofilled values.
|
||||||
|
@ -228,13 +196,13 @@ export default {
|
||||||
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
||||||
const credentials = {
|
const credentials = {
|
||||||
username: this.$refs.username.value,
|
username: this.$refs.username.value,
|
||||||
password: this.$refs.password.value,
|
password: this.password,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (credentials.username === '' || credentials.password === '') {
|
if (credentials.username === '' || credentials.password === '') {
|
||||||
// Trigger the validation error messages
|
// Trigger the validation error messages
|
||||||
this.validateField('username')
|
this.validateField('username')
|
||||||
this.validateField('password')
|
this.validatePasswordInitially = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,30 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
||||||
<div class="control is-relative">
|
<password @submit="submit" @update:modelValue="v => credentials.password = v" :validate-initially="validatePasswordInitially"/>
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
|
||||||
required
|
|
||||||
:type="passwordFieldType"
|
|
||||||
autocomplete="new-password"
|
|
||||||
v-model="credentials.password"
|
|
||||||
@keyup.enter="submit"
|
|
||||||
@focusout="validatePassword"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
@click="togglePasswordFieldType"
|
|
||||||
class="password-field-type-toggle"
|
|
||||||
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
|
||||||
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
|
||||||
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="!passwordValid">
|
|
||||||
{{ $t('user.auth.passwordRequired') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-button
|
<x-button
|
||||||
|
@ -93,12 +70,13 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useDebounceFn} from '@vueuse/core'
|
import {useDebounceFn} from '@vueuse/core'
|
||||||
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
|
import {ref, reactive, toRaw, computed, onBeforeMount, watch} from 'vue'
|
||||||
|
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import {store} from '@/store'
|
import {store} from '@/store'
|
||||||
import Message from '@/components/misc/message'
|
import Message from '@/components/misc/message'
|
||||||
import {isEmail} from '@/helpers/isEmail'
|
import {isEmail} from '@/helpers/isEmail'
|
||||||
|
import Password from '@/components/input/password'
|
||||||
|
|
||||||
// FIXME: use the `beforeEnter` hook of vue-router
|
// FIXME: use the `beforeEnter` hook of vue-router
|
||||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||||
|
@ -116,6 +94,7 @@ const credentials = reactive({
|
||||||
|
|
||||||
const loading = computed(() => store.state.loading)
|
const loading = computed(() => store.state.loading)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
const validatePasswordInitially = ref(false)
|
||||||
|
|
||||||
const DEBOUNCE_TIME = 100
|
const DEBOUNCE_TIME = 100
|
||||||
|
|
||||||
|
@ -130,29 +109,17 @@ const validateUsername = useDebounceFn(() => {
|
||||||
usernameValid.value = credentials.username !== ''
|
usernameValid.value = credentials.username !== ''
|
||||||
}, DEBOUNCE_TIME)
|
}, DEBOUNCE_TIME)
|
||||||
|
|
||||||
const passwordValid = ref(true)
|
|
||||||
const validatePassword = useDebounceFn(() => {
|
|
||||||
passwordValid.value = credentials.password !== ''
|
|
||||||
}, DEBOUNCE_TIME)
|
|
||||||
|
|
||||||
const everythingValid = computed(() => {
|
const everythingValid = computed(() => {
|
||||||
return credentials.username !== '' &&
|
return credentials.username !== '' &&
|
||||||
credentials.email !== '' &&
|
credentials.email !== '' &&
|
||||||
credentials.password !== '' &&
|
credentials.password !== '' &&
|
||||||
emailValid.value &&
|
emailValid.value &&
|
||||||
usernameValid.value &&
|
usernameValid.value
|
||||||
passwordValid.value
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const passwordFieldType = ref('password')
|
|
||||||
const togglePasswordFieldType = () => {
|
|
||||||
passwordFieldType.value = passwordFieldType.value === 'password'
|
|
||||||
? 'text'
|
|
||||||
: 'password'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
validatePasswordInitially.value = true
|
||||||
|
|
||||||
if (!everythingValid.value) {
|
if (!everythingValid.value) {
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in a new issue