refactor: Change Password

This commit is contained in:
Moriz Wahl 2021-07-01 04:47:56 +02:00
parent 0059cffa19
commit 4625e0d10b
5 changed files with 255 additions and 259 deletions

View File

@ -3,6 +3,8 @@
tag="div"
:rules="rules"
:name="name"
:bails="!showAllErrors"
:immediate="immediate"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label="label" :label-for="labelFor">
@ -22,7 +24,15 @@
</b-button>
</b-input-group-append>
<b-form-invalid-feedback v-bind="ariaMsg">
{{ errors[0] }}
<div v-if="showAllErrors">
<span v-for="error in errors" :key="error">
{{ error }}
<br />
</span>
</div>
<div v-else>
{{ errors[0] }}
</div>
</b-form-invalid-feedback>
</b-input-group>
</b-form-group>
@ -43,6 +53,8 @@ export default {
label: { type: String, default: 'Password' },
placeholder: { type: String, default: 'Password' },
value: { required: true, type: String },
showAllErrors: { type: Boolean, default: false },
immediate: { type: Boolean, default: false },
},
data() {
return {

View File

@ -2,16 +2,11 @@ import Vue from 'vue'
import DashboardPlugin from './plugins/dashboard-plugin'
import App from './App.vue'
import i18n from './i18n.js'
import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
// eslint-disable-next-line no-unused-vars
import validationRules from './validation-rules'
// store
import { store } from './store/store'
import loginAPI from './apis/loginAPI'
// router setup
import router from './routes/router'
// plugin setup
@ -26,68 +21,6 @@ router.beforeEach((to, from, next) => {
}
})
configure({
defaultMessage: (field, values) => {
values._field_ = i18n.t(`fields.${field}`)
return i18n.t(`validations.messages.${values._rule_}`, values)
},
})
extend('email', {
...email,
message: (_, values) => i18n.t('validations.messages.email', values),
})
extend('required', {
...required,
message: (_, values) => i18n.t('validations.messages.required', values),
})
extend('min', {
...min,
message: (_, values) => i18n.t('validations.messages.min', values),
})
extend('max', {
...max,
message: (_, values) => i18n.t('validations.messages.max', values),
})
extend('gddSendAmount', {
validate(value, { min, max }) {
value = value.replace(',', '.')
return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max
},
params: ['min', 'max'],
message: (_, values) => {
values.min = i18n.n(values.min, 'ungroupedDecimal')
values.max = i18n.n(values.max, 'ungroupedDecimal')
return i18n.t('form.validation.gddSendAmount', values)
},
})
extend('gddUsernameUnique', {
async validate(value) {
const result = await loginAPI.checkUsername(value)
return result.result.data.state === 'success'
},
message: (_, values) => i18n.t('form.validation.usernmae-unique', values),
})
extend('gddUsernameRgex', {
validate(value) {
return !!value.match(/^[a-zA-Z][-_a-zA-Z0-9]{2,}$/)
},
message: (_, values) => i18n.t('form.validation.usernmae-regex', values),
})
// eslint-disable-next-line camelcase
extend('is_not', {
// eslint-disable-next-line camelcase
...is_not,
message: (_, values) => i18n.t('form.validation.is-not', values),
})
/* eslint-disable no-new */
new Vue({
el: '#app',

View File

@ -0,0 +1,104 @@
import i18n from './i18n.js'
import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
import loginAPI from './apis/loginAPI'
configure({
defaultMessage: (field, values) => {
values._field_ = i18n.t(`fields.${field}`)
return i18n.t(`validations.messages.${values._rule_}`, values)
},
})
extend('email', {
...email,
message: (_, values) => i18n.t('validations.messages.email', values),
})
extend('required', {
...required,
message: (_, values) => i18n.t('validations.messages.required', values),
})
extend('min', {
...min,
message: (_, values) => i18n.t('validations.messages.min', values),
})
extend('max', {
...max,
message: (_, values) => i18n.t('validations.messages.max', values),
})
extend('gddSendAmount', {
validate(value, { min, max }) {
value = value.replace(',', '.')
return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max
},
params: ['min', 'max'],
message: (_, values) => {
values.min = i18n.n(values.min, 'ungroupedDecimal')
values.max = i18n.n(values.max, 'ungroupedDecimal')
return i18n.t('form.validation.gddSendAmount', values)
},
})
extend('gddUsernameUnique', {
async validate(value) {
const result = await loginAPI.checkUsername(value)
return result.result.data.state === 'success'
},
message: (_, values) => i18n.t('form.validation.usernmae-unique', values),
})
extend('gddUsernameRgex', {
validate(value) {
return !!value.match(/^[a-zA-Z][-_a-zA-Z0-9]{2,}$/)
},
message: (_, values) => i18n.t('form.validation.usernmae-regex', values),
})
// eslint-disable-next-line camelcase
extend('is_not', {
// eslint-disable-next-line camelcase
...is_not,
message: (_, values) => i18n.t('form.validation.is-not', values),
})
// Password validation
extend('containsLowercaseCharacter', {
validate(value) {
return !!value.match(/[a-z]+/)
},
message: (_, values) => i18n.t('site.signup.lowercase', values),
})
extend('containsUppercaseCharacter', {
validate(value) {
return !!value.match(/[A-Z]+/)
},
message: (_, values) => i18n.t('site.signup.uppercase', values),
})
extend('containsNumericCharacter', {
validate(value) {
return !!value.match(/[0-9]+/)
},
message: (_, values) => i18n.t('site.signup.one_number', values),
})
extend('atLeastEightCharactera', {
validate(value) {
return !!value.match(/.{8,}/)
},
message: (_, values) => i18n.t('site.signup.minimum', values),
})
extend('samePassword', {
validate(value, [pwd]) {
return value === pwd
},
message: (_, values) => i18n.t('site.signup.dont_match', values),
})

View File

@ -1,13 +1,32 @@
import { mount } from '@vue/test-utils'
import UserCardFormPasswort from './UserCard_FormUserPasswort'
import loginAPI from '../../../apis/loginAPI'
// import flushPromises from 'flush-promises'
import flushPromises from 'flush-promises'
import { extend } from 'vee-validate'
const rules = [
'containsLowercaseCharacter',
'containsUppercaseCharacter',
'containsNumericCharacter',
'atLeastEightCharactera',
'samePassword',
]
rules.forEach((rule) => {
extend(rule, {
validate(value) {
return true
},
})
})
jest.mock('../../../apis/loginAPI')
const localVue = global.localVue
const changePasswordProfileMock = jest.fn()
changePasswordProfileMock.mockReturnValue({ success: true })
loginAPI.changePasswordProfile = changePasswordProfileMock
const toastSuccessMock = jest.fn()
@ -59,8 +78,8 @@ describe('UserCardFormUserPasswort', () => {
let form
beforeEach(async () => {
wrapper.find('a').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('a').trigger('click')
await flushPromises()
form = wrapper.find('form')
})
@ -69,12 +88,11 @@ describe('UserCardFormUserPasswort', () => {
})
it('has a cancel button', () => {
expect(form.find('svg.bi-x-circle').exists()).toBeTruthy()
expect(wrapper.find('svg.bi-x-circle').exists()).toBeTruthy()
})
it('closes the form when cancel button is clicked', async () => {
form.find('svg.bi-x-circle').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.find('input').exists()).toBeFalsy()
})
@ -104,24 +122,52 @@ describe('UserCardFormUserPasswort', () => {
expect(form.find('button[type="submit"]').exists()).toBeTruthy()
})
/*
describe('submit', () => {
beforeEach(async () => {
await form.findAll('input').at(0).setValue('1234')
await form.findAll('input').at(1).setValue('Aa123456')
await form.findAll('input').at(2).setValue('Aa123456')
form.trigger('submit')
await wrapper.vm.$nextTick()
await flushPromises()
describe('valid data', () => {
beforeEach(async () => {
await form.findAll('input').at(0).setValue('1234')
await form.findAll('input').at(1).setValue('Aa123456')
await form.findAll('input').at(2).setValue('Aa123456')
await form.trigger('submit')
await flushPromises()
})
it('calls the API', () => {
expect(changePasswordProfileMock).toHaveBeenCalledWith(
1,
'user@example.org',
'1234',
'Aa123456',
)
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('site.thx.reset')
})
it('cancels the edit process', () => {
expect(wrapper.find('input').exists()).toBeFalsy()
})
})
it('calls the API', async () => {
await wrapper.vm.$nextTick()
await flushPromises()
expect(changePasswordProfileMock).toHaveBeenCalledWith(1, 'user@example.org', '1234', 'Aa123456')
describe('server response is error', () => {
beforeEach(async () => {
changePasswordProfileMock.mockReturnValue({
success: false,
result: { message: 'error' },
})
await form.findAll('input').at(0).setValue('1234')
await form.findAll('input').at(1).setValue('Aa123456')
await form.findAll('input').at(2).setValue('Aa123456')
await form.trigger('submit')
await flushPromises()
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('error')
})
})
})
*/
})
})
})

View File

@ -1,183 +1,110 @@
<template>
<b-card id="change_pwd" class="bg-transparent" style="background-color: #ebebeba3 !important">
<b-container>
<b-form @keyup.prevent="loadSubmitButton">
<div v-if="!editPassword">
<b-row class="mb-4 text-right">
<b-col class="text-right">
<a href="#change_pwd" v-if="!editPassword" @click="editPassword = !editPassword">
<a href="#change_pwd" @click="editPassword = !editPassword">
<span>{{ $t('form.change-password') }}</span>
<b-icon class="pointer ml-3" icon="pencil" />
</a>
<b-icon
v-else
@click="cancelEdit()"
class="pointer"
icon="x-circle"
variant="danger"
></b-icon>
</b-col>
</b-row>
<div v-if="editPassword">
<b-row class="mb-5">
<b-col class="col-12 col-lg-3 col-md-10 col-sm-10 text-md-left text-lg-right">
<small>{{ $t('form.password_old') }}</small>
</b-col>
<b-col class="col-md-9 col-sm-10">
<b-input-group>
<b-form-input
class="mb-0"
v-model="password"
name="Password"
:type="passwordVisibleOldPwd ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
</div>
<div v-if="editPassword">
<b-row class="mb-4 text-right">
<b-col class="text-right">
<b-icon @click="cancelEdit()" class="pointer" icon="x-circle" variant="danger"></b-icon>
</b-col>
</b-row>
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<b-row class="mb-2">
<b-col>
<input-password
:label="$t('form.password_old')"
:placeholder="$t('form.password_old')"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibilityOldPwd">
<b-icon :icon="passwordVisibleOldPwd ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-col>
</b-row>
<b-row class="mb-3">
<b-col class="col-12 col-lg-3 col-md-10 col-sm-10 text-md-left text-lg-right">
<small>{{ $t('form.password_new') }}</small>
</b-col>
<b-col class="col-md-9 col-sm-10">
<b-input-group>
<b-form-input
class="mb-0"
v-model="passwordNew"
name="Password"
:type="passwordVisibleNewPwd ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
v-model="form.password"
></input-password>
</b-col>
</b-row>
<b-row class="mb-2">
<b-col>
<input-password
:rules="{
required: true,
containsLowercaseCharacter: true,
containsUppercaseCharacter: true,
containsNumericCharacter: true,
atLeastEightCharactera: true,
}"
:label="$t('form.password_new')"
:showAllErrors="true"
:immediate="true"
:name="$t('form.password_new')"
:placeholder="$t('form.password_new')"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibilityNewPwd">
<b-icon :icon="passwordVisibleNewPwd ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-col>
</b-row>
<b-row class="mb-3">
<b-col class="col-12 col-lg-3 col-md-10 col-sm-10 text-md-left text-lg-right">
<small>{{ $t('form.password_new_repeat') }}</small>
</b-col>
<b-col class="col-md-9 col-sm-10">
<b-input-group>
<b-form-input
class="mb-0"
v-model="passwordNewRepeat"
name="Password"
:type="passwordVisibleNewPwdRepeat ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
v-model="form.passwordNew"
></input-password>
</b-col>
</b-row>
<b-row class="mb-2">
<b-col>
<input-password
:rules="{ samePassword: form.passwordNew }"
:label="$t('form.password_new_repeat')"
:placeholder="$t('form.password_new_repeat')"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibilityNewPwdRepeat">
<b-icon :icon="passwordVisibleNewPwdRepeat ? 'eye' : 'eye-slash'" />
v-model="form.passwordNewRepeat"
></input-password>
</b-col>
</b-row>
<b-row class="text-right" v-if="editPassword">
<b-col>
<div class="text-right">
<b-button type="submit" variant="primary" class="mt-4">
{{ $t('form.save') }}
</b-button>
</b-input-group-append>
</b-input-group>
</b-col>
</b-row>
<b-row>
<b-col></b-col>
<b-col class="col-12">
<transition name="hint" appear>
<div v-if="passwordValidation.errors.length > 0" class="hints">
<ul>
<li v-for="error in passwordValidation.errors" :key="error">
<small>{{ error }}</small>
</li>
</ul>
</div>
</transition>
</b-col>
</b-row>
<b-row class="text-right" v-if="editPassword">
<b-col>
<div class="text-right" ref="submitButton">
<b-button
:variant="loading ? 'default' : 'success'"
@click="onSubmit"
type="submit"
class="mt-4"
:disabled="loading"
>
{{ $t('form.save') }}
</b-button>
</div>
</b-col>
</b-row>
</div>
</b-form>
</b-col>
</b-row>
</b-form>
</validation-observer>
</div>
</b-container>
</b-card>
</template>
<script>
import loginAPI from '../../../apis/loginAPI'
import InputPassword from '../../../components/Inputs/InputPassword'
export default {
name: 'FormUserPasswort',
components: {
InputPassword,
},
data() {
return {
editPassword: false,
email: null,
password: '',
passwordNew: '',
passwordNewRepeat: '',
passwordVisibleOldPwd: false,
passwordVisibleNewPwd: false,
passwordVisibleNewPwdRepeat: false,
loading: true,
form: {
password: '',
passwordNew: '',
passwordNewRepeat: '',
},
}
},
methods: {
cancelEdit() {
this.editPassword = false
this.password = ''
this.passwordNew = ''
this.passwordNewRepeat = ''
this.form.password = ''
this.form.passwordNew = ''
this.form.passwordNewRepeat = ''
},
togglePasswordVisibilityNewPwd() {
this.passwordVisibleNewPwd = !this.passwordVisibleNewPwd
},
togglePasswordVisibilityNewPwdRepeat() {
this.passwordVisibleNewPwdRepeat = !this.passwordVisibleNewPwdRepeat
},
togglePasswordVisibilityOldPwd() {
this.passwordVisibleOldPwd = !this.passwordVisibleOldPwd
},
loadSubmitButton() {
if (
this.password !== '' &&
this.passwordNew !== '' &&
this.passwordNewRepeat !== '' &&
this.passwordNew === this.passwordNewRepeat
) {
this.loading = false
} else {
this.loading = true
}
},
async onSubmit(event) {
event.preventDefault()
async onSubmit() {
const result = await loginAPI.changePasswordProfile(
this.$store.state.sessionId,
this.$store.state.email,
this.password,
this.passwordNew,
this.form.password,
this.form.passwordNew,
)
if (result.success) {
this.$toast.success(this.$t('site.thx.reset'))
@ -187,31 +114,5 @@ export default {
}
},
},
computed: {
samePasswords() {
return this.password === this.passwordNew
},
rules() {
return [
{ message: this.$t('site.signup.lowercase'), regex: /[a-z]+/ },
{ message: this.$t('site.signup.uppercase'), regex: /[A-Z]+/ },
{ message: this.$t('site.signup.minimum'), regex: /.{8,}/ },
{ message: this.$t('site.signup.one_number'), regex: /[0-9]+/ },
]
},
passwordValidation() {
const errors = []
for (const condition of this.rules) {
if (!condition.regex.test(this.passwordNew)) {
errors.push(condition.message)
}
}
if (errors.length === 0) {
return { valid: true, errors }
}
return { valid: false, errors }
},
},
}
</script>
<style></style>