The backend returns "403 - Password Expired" when user's password is too old and we redirect the user to the /changepassword page where they can update their pw. Users will be required to insert username, current password, new password and to retype the new one. Success: user redirected to login page. Failure: a banner with the error is shown for 10 seconds. NOTE: * Password requirements are always presented in the frontend as guidelines but validations are not enforced. It was a joint decision with the backend team not to expose whether the web_validation is active on the platform. * If the web_validation is active, the backend will respond with a 422 and the error message will describe the missed password requirements. Change-Id: Ic8b34980ff3099712f72042294409a2eb8e9347fmr13.1
parent
95e93e0365
commit
46357637cd
@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<q-layout
|
||||
id="csc-layout-login"
|
||||
view="lHh lpR lFf"
|
||||
class="bg-page"
|
||||
>
|
||||
<q-header
|
||||
id="csc-header-login"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<q-toolbar
|
||||
id="csc-header-toolbar-login"
|
||||
>
|
||||
<csc-selection-language />
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
<q-page-container>
|
||||
<q-page
|
||||
id="csc-page-login"
|
||||
class="flex flex-center row"
|
||||
>
|
||||
<q-card
|
||||
id="csc-login-card"
|
||||
class="bg-main-menu no-shadow no-border-radius col-xs-12 col-sm-6 col-md-4 q-pa-sm"
|
||||
>
|
||||
<q-card-section class="text-h5 text-center">
|
||||
{{ $t('Create New Password') }}
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-banner
|
||||
dense
|
||||
inline-actions
|
||||
class="text-white text-center bg-red-14"
|
||||
>
|
||||
{{ $t('Your password has expired') }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div
|
||||
v-if="validationGuidelines && validationGuidelines.length > 0"
|
||||
inline-actions
|
||||
class="q-mb-md q-pa-md"
|
||||
>
|
||||
<p>Suggested password format:</p>
|
||||
<q-item
|
||||
v-for="(message, index) in validationGuidelines"
|
||||
:key="index"
|
||||
dense
|
||||
>
|
||||
<q-item-section>
|
||||
<span>
|
||||
<q-icon
|
||||
name="lock"
|
||||
size="1em"
|
||||
class="q-pa-xs"
|
||||
/> {{ message }}
|
||||
</span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form key="login-form">
|
||||
<csc-input
|
||||
v-model.trim="username"
|
||||
outlined
|
||||
:label="$t('Username')"
|
||||
data-cy="csc-login-username"
|
||||
:error="v$.username && v$.username.$errors.length > 0"
|
||||
:error-message="$errMsg(v$.username.$errors)"
|
||||
@keyup.enter="changePasswordAction"
|
||||
>
|
||||
<template
|
||||
#prepend
|
||||
>
|
||||
<q-icon
|
||||
name="person"
|
||||
/>
|
||||
</template>
|
||||
</csc-input>
|
||||
|
||||
<csc-input-password
|
||||
v-model.trim="currentPassword"
|
||||
outlined
|
||||
clearable
|
||||
:label="$t('Current Password')"
|
||||
data-cy="csc-login-password"
|
||||
:error="v$.currentPassword && v$.currentPassword.$errors.length > 0"
|
||||
:error-message="$errMsg(v$.currentPassword.$errors)"
|
||||
@keyup.enter="changePasswordAction"
|
||||
/>
|
||||
|
||||
<csc-input-password
|
||||
v-model.trim="newPassword"
|
||||
outlined
|
||||
clearable
|
||||
:label="$t('New Password')"
|
||||
data-cy="csc-login-password"
|
||||
:error="v$.newPassword && v$.newPassword.$errors.length > 0"
|
||||
:error-message="$errMsg(v$.newPassword.$errors)"
|
||||
@keyup.enter="changePasswordAction"
|
||||
/>
|
||||
|
||||
<csc-input
|
||||
ref="passwordRetypeInput"
|
||||
v-model.trim="passwordRetype"
|
||||
outlined
|
||||
clearable
|
||||
icon="lock"
|
||||
color="secondary"
|
||||
:label="$t('Password Retype')"
|
||||
data-cy="password-retype-field"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:error="v$.passwordRetype && v$.passwordRetype.$errors.length > 0"
|
||||
:error-message="$errMsg(v$.passwordRetype.$errors)"
|
||||
@keyup.enter="changePasswordAction"
|
||||
>
|
||||
<template
|
||||
#prepend
|
||||
>
|
||||
<q-icon
|
||||
name="lock"
|
||||
/>
|
||||
</template>
|
||||
</csc-input>
|
||||
|
||||
<div class="row justify-center q-pa-md">
|
||||
<q-btn
|
||||
data-cy="sign-in"
|
||||
unelevated
|
||||
color="primary"
|
||||
icon="arrow_forward"
|
||||
:label="$t('Change Password')"
|
||||
@click="changePasswordAction"
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import useValidate from '@vuelidate/core'
|
||||
import { required } from '@vuelidate/validators'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import CscSelectionLanguage from 'src/components/CscSelectionLanguage.vue'
|
||||
import CscInput from 'src/components/form/CscInput.vue'
|
||||
import CscInputPassword from 'src/components/form/CscInputPassword.vue'
|
||||
import { RequestState } from 'src/store/common'
|
||||
import { mapWaitingActions } from 'vue-wait'
|
||||
|
||||
export default {
|
||||
name: 'ChangeExpiredPassword',
|
||||
components: {
|
||||
CscSelectionLanguage,
|
||||
CscInput,
|
||||
CscInputPassword
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
v$: useValidate(),
|
||||
username: '',
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
passwordStrengthScore: null,
|
||||
passwordRetype: '',
|
||||
validationGuidelines: []
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
username: {
|
||||
required
|
||||
},
|
||||
currentPassword: {
|
||||
required
|
||||
},
|
||||
newPassword: {
|
||||
required
|
||||
},
|
||||
passwordRetype: {
|
||||
sameAsPassword (val) {
|
||||
return val === this.newPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('user', [
|
||||
'changePasswordState',
|
||||
'changePasswordError'
|
||||
])
|
||||
},
|
||||
watch: {
|
||||
changePasswordState (state) {
|
||||
if (state === RequestState.succeeded) {
|
||||
this.$q.notify({
|
||||
position: 'top',
|
||||
color: 'positive',
|
||||
icon: 'check',
|
||||
message: this.$t('Password changed successfully')
|
||||
})
|
||||
this.redirectToLogin()
|
||||
} else if (state === RequestState.failed) {
|
||||
this.$q.notify({
|
||||
position: 'top',
|
||||
color: 'negative',
|
||||
icon: 'error',
|
||||
timeout: 10000,
|
||||
message: this.changePasswordError || this.$t('There was an error, please retry later')
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
const guidelines = await this.getValidationGuidelines()
|
||||
this.validationGuidelines = this.formatValidationGuidelines(guidelines)
|
||||
},
|
||||
methods: {
|
||||
...mapWaitingActions('user', [
|
||||
'fetchPreLoginPasswordInfo'
|
||||
]),
|
||||
...mapActions('user', [
|
||||
'changeExpiredPassword'
|
||||
]),
|
||||
strengthMeterScoreUpdate (score) {
|
||||
this.passwordStrengthScore = score
|
||||
},
|
||||
async changePasswordAction () {
|
||||
this.v$.$touch()
|
||||
if (this.v$.$errors.length === 0) {
|
||||
return this.changeExpiredPassword({
|
||||
username: this.username,
|
||||
old_password: this.currentPassword,
|
||||
new_password: this.newPassword
|
||||
})
|
||||
}
|
||||
},
|
||||
formatValidationGuidelines (validationRulesObject) {
|
||||
const guidelines = []
|
||||
for (const rule in validationRulesObject) {
|
||||
switch (rule) {
|
||||
case 'min_length':
|
||||
guidelines.push(`Minimum ${validationRulesObject[rule]} characters long.`)
|
||||
break
|
||||
case 'max_length':
|
||||
guidelines.push(`Maximum ${validationRulesObject[rule]} characters long.`)
|
||||
break
|
||||
case 'musthave_digit':
|
||||
guidelines.push(`Contains a minimum of ${validationRulesObject[rule]} digits.`)
|
||||
break
|
||||
case 'musthave_lowercase':
|
||||
guidelines.push(`Contains a minimum of ${validationRulesObject[rule]} lowercases.`)
|
||||
break
|
||||
case 'musthave_specialchar':
|
||||
guidelines.push(`Contains a minimum of ${validationRulesObject[rule]} special characters.`)
|
||||
break
|
||||
case 'musthave_uppercase':
|
||||
guidelines.push(`Contains a minimum of ${validationRulesObject[rule]} uppercases.`)
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return guidelines
|
||||
},
|
||||
async getValidationGuidelines () {
|
||||
const defaultGuidelines = {
|
||||
max_length: 40,
|
||||
min_length: 12,
|
||||
musthave_digit: 3,
|
||||
musthave_lowercase: 3,
|
||||
musthave_specialchar: 3,
|
||||
musthave_uppercase: 3
|
||||
}
|
||||
const customGuidelines = await this.fetchPreLoginPasswordInfo()
|
||||
|
||||
return customGuidelines ?? defaultGuidelines
|
||||
},
|
||||
redirectToLogin () {
|
||||
this.$router.push({ path: '/login' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
Loading…
Reference in new issue