TT#84211 CSC: As a Customer, I want to recover/reset my password

- Can click password recovery button/link
- Can request recovery mail by input mail address and confirm
- Can set new password after forwarded from recovery mail

For AR:
- click on Forgot Password in CSC login screen
- after submitting, login into mysql in your development env and execute
'select * from billing.password_resets;'
- copy the most recent uuid
- go to localhost:8080/#/recoverpassword?token=uuid and
proceed with password reset

Change-Id: Iff10165f98daa65a0ac85ec55c5d62926513fe0d
mr9.1.1
Carlo Venusino 5 years ago committed by Hans-Peter Herzog
parent 1cd9ff14fa
commit cd9c2a8bcb

@ -517,3 +517,20 @@ export function changePassword (subscriber, newPassword) {
})
})
}
export async function resetPassword (userName) {
const payLoad = {
domain: Vue.$config.baseHttpUrl.replace(/(^\w+:|^)\/\//, ''),
type: 'subscriber',
username: userName
}
return await Vue.http.post('api/passwordreset/', payLoad)
}
export async function recoverPassword (data) {
const payLoad = {
new_password: data.password,
token: data.token
}
return await Vue.http.post('api/passwordrecovery/', payLoad)
}

@ -11,22 +11,35 @@ import {
export default ({ app, router, store }) => {
router.beforeEach((to, from, next) => {
if (!hasJwt() && to.path !== '/login') {
next({
path: '/login'
})
} else if (hasJwt() && to.path === '/login') {
next({
path: '/'
})
} else if (hasJwt() && to.path === '/conference') {
next({
path: '/conference/room123'
})
const publicUrls = ['/login', '/recoverpassword']
// not authorized user
if (!hasJwt()) {
if (!publicUrls.includes(to.path)) {
next({
path: '/login'
})
} else {
next()
}
} else {
next()
// already authorized user
switch (to.path) {
case '/login':
next({
path: '/'
})
break
case '/conference':
next({
path: '/conference/room123'
})
break
default:
next()
}
}
})
router.afterEach((to, from) => {
const mainTitle = app.i18n.t('title')
let title = _.get(to, 'meta.title', '')

@ -1,6 +1,15 @@
import Vuelidate from 'vuelidate'
import _ from 'lodash'
export default ({ Vue, store }) => {
export default ({ Vue, app }) => {
Vue.use(Vuelidate)
Vue.prototype.$errorMessage = (def) => {
let message = null
_.forEach(def.$params, (param, paramName) => {
if (def[paramName] === false) {
message = app.i18n.t('validators.' + paramName)
}
})
return message
}
}

@ -4,9 +4,10 @@
:value="value"
:loading="loading"
title-icon="vpn_key"
title="Change password"
:title="$t('pages.login.changePassword')"
class="csc-pbx-password-dialog"
@input="$emit('input')"
@hide="$emit('dialog-closed')"
>
<div
slot="content"

@ -0,0 +1,121 @@
<template>
<csc-dialog
:value="value"
title-icon="vpn_key"
:title="$t('pages.login.forgotPassword')"
@input="$emit('input')"
@hide="resetForm()"
>
<template
v-slot:content
>
<q-form>
<q-item>
<q-item-section>
<q-input
v-model.trim="username"
clearable
dense
:label="$t('pages.login.username')"
type="text"
:error="$v.username.$error"
:error-message="$errorMessage($v.username)"
@blur="$v.username.$touch()"
>
<template
v-slot:prepend
>
<q-icon
name="fas fa-user-cog"
/>
</template>
</q-input>
</q-item-section>
</q-item>
</q-form>
</template>
<template
v-slot:actions
>
<q-btn
icon="check"
unelevated
color="primary"
:label="$t('toasts.send')"
:loading="newPasswordRequesting"
:disable="!username || username.length < 1 || newPasswordRequesting"
@click="submit()"
/>
</template>
</csc-dialog>
</template>
<script>
import {
required
} from 'vuelidate/lib/validators'
import {
mapActions,
mapState
} from 'vuex'
import CscDialog from './CscDialog'
export default {
name: 'CscRetrievePasswordDialog',
components: {
CscDialog
},
props: {
value: {
type: Boolean,
default: false
}
},
data () {
return {
username: ''
}
},
validations: {
username: {
required
}
},
computed: {
...mapState('user', [
'newPasswordRequesting'
])
},
methods: {
...mapActions('user', [
'resetPassword'
]),
async submit () {
this.$v.$touch()
if (!this.$v.$invalid) {
try {
const res = await this.resetPassword(this.username)
this.$q.notify({
position: 'top',
color: 'positive',
icon: 'check',
message: res.data.message
})
} catch (err) {
this.$q.notify({
position: 'top',
color: 'negative',
icon: 'error',
message: this.$t('toasts.errorPasswordReset')
})
} finally {
this.$emit('close')
}
}
},
resetForm () {
this.$v.$reset()
this.username = ''
}
}
}
</script>

@ -42,7 +42,10 @@
"callAvailable": "You are now able to start and receive calls",
"callNotAvailable": "Could not initialize call functionality properly",
"conferencingAvailable": "You are now able to create WebRTC multiparty conferences",
"changeSessionLanguageSuccessMessage": "Session language successfully changed"
"changeSessionLanguageSuccessMessage": "Session language successfully changed",
"passwordChangedSuccessfully": "Password changed successfully",
"errorPasswordReset": "There was an error, please retry later",
"send": "Send"
},
"validationErrors": {
"generic": "You have invalid form input. Please check and try again.",
@ -142,7 +145,10 @@
"username": "Username",
"username_helper": "Input username or username@domain",
"password": "Password",
"password_helper": ""
"password_helper": "",
"forgotPassword": "Forgot password?",
"recoverPassword": "Recover password",
"changePassword": "Change password"
},
"callBlockingIncoming": {
"title": "Block/Allow incoming calls",

@ -0,0 +1,7 @@
<template>
<q-layout view="hHh lpR fFf">
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>

@ -76,8 +76,15 @@
</form>
</q-card-section>
<q-card-actions
class="justify-end"
class="justify-between"
>
<q-btn
color="primary"
unelevated
flat
:label="$t('pages.login.forgotPassword')"
@click="showRetrievePasswordDialog"
/>
<q-btn
icon="arrow_forward"
color="primary"
@ -93,6 +100,10 @@
</template>
</q-btn>
</q-card-actions>
<csc-retrieve-password-dialog
v-model="showDialog"
@close="showDialog=false"
/>
</q-card>
</q-page>
</q-page-container>
@ -117,18 +128,21 @@ import CscLanguageMenu from 'components/CscLanguageMenu'
import CscSpinner from 'components/CscSpinner'
import CscInputPassword from 'components/form/CscInputPassword'
import CscInput from 'components/form/CscInput'
import CscRetrievePasswordDialog from 'components/CscRetrievePasswordDialog'
export default {
name: 'Login',
components: {
CscInput,
CscInputPassword,
CscSpinner,
CscLanguageMenu
CscLanguageMenu,
CscRetrievePasswordDialog
},
data () {
return {
username: '',
password: ''
password: '',
showDialog: false
}
},
computed: {
@ -176,6 +190,9 @@ export default {
},
changeLanguage (language) {
this.$store.dispatch('user/changeSessionLanguage', language)
},
showRetrievePasswordDialog () {
this.showDialog = true
}
}
}

@ -0,0 +1,84 @@
<template>
<q-page
class="flex flex-center"
>
<csc-change-password-dialog
v-model="showDialog"
:title="$t('pages.login.recoverPassword')"
:loading="isPasswordChanging"
@change-password="recoverPassword({ password: $event.password, token: token })"
@dialog-closed="redirectToLogin()"
/>
</q-page>
</template>
<script>
import {
mapActions,
mapGetters,
mapState
} from 'vuex'
import {
RequestState
} from 'src/store/common'
import CscChangePasswordDialog from '../components/CscChangePasswordDialog'
export default {
name: 'CscRecoverPassword',
components: {
CscChangePasswordDialog
},
props: {
token: {
type: String,
default: null
}
},
data () {
return {
showDialog: true
}
},
computed: {
...mapState('user', [
'changePasswordState',
'changePasswordError'
]),
...mapGetters('user', [
'isPasswordChanging'
])
},
watch: {
changePasswordState (state) {
if (state === RequestState.succeeded) {
this.$q.notify({
position: 'top',
color: 'positive',
icon: 'check',
message: this.$t('toasts.passwordChangedSuccessfully')
})
this.redirectToLogin()
} else if (state === RequestState.failed) {
this.$q.notify({
position: 'top',
color: 'negative',
icon: 'error',
message: this.changePasswordError || this.$t('toasts.errorPasswordReset')
})
}
}
},
mounted () {
if (!this.token) {
this.redirectToLogin()
}
},
methods: {
...mapActions('user', [
'recoverPassword'
]),
redirectToLogin () {
this.$router.push({ path: '/login' })
}
}
}
</script>

@ -1,6 +1,7 @@
import CscLayoutConference from 'src/layouts/CscLayoutConference'
import CscLayoutMain from 'src/layouts/CscLayoutMain'
import CscLayoutLogin from 'src/layouts/CscLayoutLogin'
import CscPageLogin from 'src/pages/CscPageLogin'
import CscPageHome from 'src/pages/CscPageHome'
@ -24,6 +25,13 @@ import CscPagePbxSettings from 'src/pages/CscPagePbxSettings'
import CscPageVoicebox from 'src/pages/CscPageVoicebox'
import CscPageUserSettings from 'src/pages/CscPageUserSettings'
import CscPageError404 from 'src/pages/CscPageError404'
import CscRecoverPassword from 'src/pages/CscRecoverPassword'
const getToken = (route) => {
return {
token: route.query.token
}
}
export default function routes (app) {
const i18n = app.i18n
@ -214,6 +222,21 @@ export default function routes (app) {
title: 'Conference'
}
},
{
path: '/recoverpassword',
component: CscLayoutLogin,
children: [
{
path: '',
component: CscRecoverPassword,
props: getToken,
meta: {
title: 'Reset Password',
permission: 'public'
}
}
]
},
{
path: '/',
redirect: {
@ -226,21 +249,3 @@ export default function routes (app) {
}
]
}
// const routes = [
// {
// path: '/',
// component: () => import('layouts/MainLayout.vue'),
// children: [
// { path: '', component: () => import('pages/Index.vue') }
// ]
// },
// // Always leave this as last one,
// // but you can also remove it
// {
// path: '*',
// component: () => import('pages/Error404.vue')
// }
// ]
//
// export default routes

@ -13,7 +13,7 @@ import {
login,
getUserData
} from '../api/user'
import { changePassword } from '../api/subscriber'
import { changePassword, resetPassword, recoverPassword } from '../api/subscriber'
import { deleteJwt, getJwt, getSubscriberId, setJwt, setSubscriberId } from 'src/auth'
import { setSession } from 'src/storage'
@ -41,7 +41,8 @@ export default {
changeSessionLocaleError: null,
languageLabels: [],
changePasswordState: RequestState.initiated,
changePasswordError: null
changePasswordError: null,
newPasswordRequesting: false
},
getters: {
isLogged (state) {
@ -243,8 +244,11 @@ export default {
state.changePasswordError = null
},
userPasswordFailed (state, error) {
state.changePasswordState = RequestState.failed
state.changePasswordError = error
state.changePasswordState = RequestState.failed
},
newPasswordRequesting (state, isRequesting) {
state.newPasswordRequesting = isRequesting
}
},
actions: {
@ -317,6 +321,25 @@ export default {
context.commit('userPasswordFailed', err.message)
})
},
async resetPassword ({ commit }, data) {
commit('newPasswordRequesting', true)
const response = await resetPassword(data)
commit('newPasswordRequesting', false)
return response
},
async recoverPassword ({ commit, dispatch, state, rootGetters }, data) {
commit('userPasswordRequesting')
try {
const res = await recoverPassword(data)
if (res.status === 200 || res.status === 201) {
commit('userPasswordSucceeded')
} else {
commit('userPasswordFailed')
}
} catch (err) {
commit('userPasswordFailed', err.message)
}
},
async forwardHome (context) {
if (context.rootState.route.path === '/user/home' && !context.getters.isRtcEngineUiVisible) {
await router.push({ path: '/user/conversations' })

Loading…
Cancel
Save