forked from verdnatura/salix-front
Merge pull request '7848-recoveryPassword' (!617) from 7848-recoveryPassword into dev
Reviewed-on: verdnatura/salix-front#617 Reviewed-by: Jorge Penades <jorgep@verdnatura.es>
This commit is contained in:
commit
e9873a43b1
|
@ -0,0 +1,32 @@
|
||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['submit']);
|
||||||
|
defineProps({
|
||||||
|
icon: { type: String, required: false, default: 'phonelink_lock' },
|
||||||
|
title: { type: String, required: true },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<QForm @submit="emit('submit')" class="q-gutter-y-md q-pa-lg formCard">
|
||||||
|
<div class="column items-center">
|
||||||
|
<QIcon v-if="icon != false" :name="icon" size="xl" color="primary" />
|
||||||
|
<h5 class="text-center q-my-md">
|
||||||
|
{{ title }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
<div class="q-mt-lg">
|
||||||
|
<slot name="buttons"></slot>
|
||||||
|
</div>
|
||||||
|
</QForm>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.formCard {
|
||||||
|
max-width: 350px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
@media (max-width: $breakpoint-xs-max) {
|
||||||
|
.formCard {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -251,6 +251,9 @@ globals:
|
||||||
privileges: Privileges
|
privileges: Privileges
|
||||||
ldap: LDAP
|
ldap: LDAP
|
||||||
samba: Samba
|
samba: Samba
|
||||||
|
twoFactor: Two factor
|
||||||
|
recoverPassword: Recover password
|
||||||
|
resetPassword: Reset password
|
||||||
created: Created
|
created: Created
|
||||||
worker: Worker
|
worker: Worker
|
||||||
now: Now
|
now: Now
|
||||||
|
@ -288,14 +291,17 @@ twoFactor:
|
||||||
explanation: >-
|
explanation: >-
|
||||||
Please, enter the verification code that we have sent to your email in the
|
Please, enter the verification code that we have sent to your email in the
|
||||||
next 5 minutes
|
next 5 minutes
|
||||||
pageTitles:
|
|
||||||
twoFactor: Two-Factor
|
|
||||||
verifyEmail:
|
verifyEmail:
|
||||||
pageTitles:
|
pageTitles:
|
||||||
verifyEmail: Email verification
|
verifyEmail: Email verification
|
||||||
dashboard:
|
recoverPassword:
|
||||||
pageTitles:
|
userOrEmail: User or recovery email
|
||||||
|
explanation: >-
|
||||||
|
We will sent you an email to recover your password
|
||||||
|
resetPassword:
|
||||||
|
repeatPassword: Repeat password
|
||||||
|
passwordNotMatch: Passwords don't match
|
||||||
|
passwordChanged: Password changed
|
||||||
customer:
|
customer:
|
||||||
list:
|
list:
|
||||||
phone: Phone
|
phone: Phone
|
||||||
|
|
|
@ -253,6 +253,9 @@ globals:
|
||||||
packages: Bultos
|
packages: Bultos
|
||||||
ldap: LDAP
|
ldap: LDAP
|
||||||
samba: Samba
|
samba: Samba
|
||||||
|
twoFactor: Doble factor
|
||||||
|
recoverPassword: Recuperar contraseña
|
||||||
|
resetPassword: Restablecer contraseña
|
||||||
created: Fecha creación
|
created: Fecha creación
|
||||||
worker: Trabajador
|
worker: Trabajador
|
||||||
now: Ahora
|
now: Ahora
|
||||||
|
@ -289,14 +292,17 @@ twoFactor:
|
||||||
validate: Validar
|
validate: Validar
|
||||||
insert: Introduce el código de verificación
|
insert: Introduce el código de verificación
|
||||||
explanation: Por favor introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos
|
explanation: Por favor introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos
|
||||||
pageTitles:
|
|
||||||
twoFactor: Doble factor
|
|
||||||
verifyEmail:
|
verifyEmail:
|
||||||
pageTitles:
|
pageTitles:
|
||||||
verifyEmail: Verificación de correo
|
verifyEmail: Verificación de correo
|
||||||
dashboard:
|
recoverPassword:
|
||||||
pageTitles:
|
userOrEmail: Usuario o correo de recuperación
|
||||||
|
explanation: >-
|
||||||
|
Te enviaremos un correo para restablecer tu contraseña
|
||||||
|
resetPassword:
|
||||||
|
repeatPassword: Repetir contraseña
|
||||||
|
passwordNotMatch: Las contraseñas no coinciden
|
||||||
|
passwordChanged: Contraseña cambiada
|
||||||
customer:
|
customer:
|
||||||
list:
|
list:
|
||||||
phone: Teléfono
|
phone: Teléfono
|
||||||
|
|
|
@ -72,7 +72,8 @@ async function onSubmit() {
|
||||||
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
|
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
|
||||||
class="red"
|
class="red"
|
||||||
/>
|
/>
|
||||||
<div>
|
<QToggle v-model="keepLogin" :label="t('login.keepLogin')" />
|
||||||
|
<div class="column flex-center q-mt-lg">
|
||||||
<QBtn
|
<QBtn
|
||||||
:label="t('login.submit')"
|
:label="t('login.submit')"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -81,11 +82,15 @@ async function onSubmit() {
|
||||||
rounded
|
rounded
|
||||||
unelevated
|
unelevated
|
||||||
/>
|
/>
|
||||||
|
<RouterLink
|
||||||
|
class="q-mt-md text-primary"
|
||||||
|
:to="`/recoverPassword?user=${username}`"
|
||||||
|
>
|
||||||
|
{{ t('I do not remember my password') }}
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<QToggle v-model="keepLogin" :label="t('login.keepLogin')" />
|
|
||||||
</QForm>
|
</QForm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.formCard {
|
.formCard {
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
|
@ -101,3 +106,7 @@ async function onSubmit() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
I do not remember my password: No recuerdo mi contraseña
|
||||||
|
</i18n>
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
|
import VnOutForm from 'src/components/ui/VnOutForm.vue';
|
||||||
|
|
||||||
|
const quasar = useQuasar();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const user = ref(route.query.user);
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
try {
|
||||||
|
await axios.post('VnUsers/recoverPassword', { user: user.value, app: 'lilium' });
|
||||||
|
router.push('Login');
|
||||||
|
quasar.notify({
|
||||||
|
message: t('globals.notificationSent'),
|
||||||
|
type: 'positive',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
quasar.notify({
|
||||||
|
message: e.response?.data?.error.message,
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VnOutForm @submit="onSubmit" :title="t('globals.pageTitles.recoverPassword')">
|
||||||
|
<template #default>
|
||||||
|
<VnInput
|
||||||
|
v-model="user"
|
||||||
|
:label="t('recoverPassword.userOrEmail')"
|
||||||
|
:hint="t('recoverPassword.explanation')"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="contact_mail" />
|
||||||
|
</template>
|
||||||
|
</VnInput>
|
||||||
|
</template>
|
||||||
|
<template #buttons>
|
||||||
|
<QBtn
|
||||||
|
:label="t('globals.pageTitles.recoverPassword')"
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
class="full-width q-mt-md"
|
||||||
|
rounded
|
||||||
|
unelevated
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VnOutForm>
|
||||||
|
</template>
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import VnInput from 'components/common/VnInput.vue';
|
||||||
|
import VnOutForm from 'components/ui/VnOutForm.vue';
|
||||||
|
|
||||||
|
const quasar = useQuasar();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const newPassword = ref();
|
||||||
|
const repeatPassword = ref();
|
||||||
|
const passRequirements = ref({});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
passRequirements.value = (await axios('UserPasswords/findOne')).data;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
if (newPassword.value != repeatPassword.value)
|
||||||
|
return quasar.notify({
|
||||||
|
message: t('resetPassword.passwordNotMatch'),
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Authorization: route.query.access_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('newPassword: ', newPassword);
|
||||||
|
await axios.post(
|
||||||
|
'VnUsers/reset-password',
|
||||||
|
{ newPassword: newPassword.value },
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
router.push('Login');
|
||||||
|
quasar.notify({
|
||||||
|
message: t('resetPassword.passwordChanged'),
|
||||||
|
type: 'positive',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
quasar.notify({
|
||||||
|
message: e.response?.data?.error.message,
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VnOutForm @submit="onSubmit" :title="t('globals.pageTitles.resetPassword')">
|
||||||
|
<template #default>
|
||||||
|
<VnInput
|
||||||
|
type="password"
|
||||||
|
:label="t('login.password')"
|
||||||
|
v-model="newPassword"
|
||||||
|
:info="
|
||||||
|
t('passwordRequirements', {
|
||||||
|
length: passRequirements.length,
|
||||||
|
nAlpha: passRequirements.nAlpha,
|
||||||
|
nUpper: passRequirements.nUpper,
|
||||||
|
nDigits: passRequirements.nDigits,
|
||||||
|
nPunct: passRequirements.nPunct,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="password" />
|
||||||
|
</template>
|
||||||
|
</VnInput>
|
||||||
|
<VnInput
|
||||||
|
type="password"
|
||||||
|
:label="t('resetPassword.repeatPassword')"
|
||||||
|
v-model="repeatPassword"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="password" />
|
||||||
|
</template>
|
||||||
|
</VnInput>
|
||||||
|
</template>
|
||||||
|
<template #buttons>
|
||||||
|
<QBtn
|
||||||
|
:label="t('globals.pageTitles.resetPassword')"
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
class="full-width q-mt-md"
|
||||||
|
rounded
|
||||||
|
unelevated
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VnOutForm>
|
||||||
|
</template>
|
|
@ -8,6 +8,7 @@ import axios from 'axios';
|
||||||
import { useSession } from 'src/composables/useSession';
|
import { useSession } from 'src/composables/useSession';
|
||||||
import { useLogin } from 'src/composables/useLogin';
|
import { useLogin } from 'src/composables/useLogin';
|
||||||
import VnInput from 'src/components/common/VnInput.vue';
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
|
import VnOutForm from 'src/components/ui/VnOutForm.vue';
|
||||||
|
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
@ -38,11 +39,8 @@ async function onSubmit() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<QForm @submit="onSubmit" class="q-gutter-y-md q-pa-lg formCard">
|
<VnOutForm @submit="onSubmit" :title="t('twoFactor.insert')">
|
||||||
<div class="column items-center">
|
<template #default>
|
||||||
<QIcon name="phonelink_lock" size="xl" color="primary" />
|
|
||||||
<h5 class="text-center q-my-md">{{ t('twoFactor.insert') }}</h5>
|
|
||||||
</div>
|
|
||||||
<VnInput
|
<VnInput
|
||||||
v-model="code"
|
v-model="code"
|
||||||
:hint="t('twoFactor.explanation')"
|
:hint="t('twoFactor.explanation')"
|
||||||
|
@ -55,7 +53,8 @@ async function onSubmit() {
|
||||||
<QIcon name="lock" />
|
<QIcon name="lock" />
|
||||||
</template>
|
</template>
|
||||||
</VnInput>
|
</VnInput>
|
||||||
<div class="q-mt-xl">
|
</template>
|
||||||
|
<template #buttons>
|
||||||
<QBtn
|
<QBtn
|
||||||
:label="t('twoFactor.validate')"
|
:label="t('twoFactor.validate')"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -64,18 +63,6 @@ async function onSubmit() {
|
||||||
rounded
|
rounded
|
||||||
unelevated
|
unelevated
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
</QForm>
|
</VnOutForm>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.formCard {
|
|
||||||
max-width: 350px;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-xs-max) {
|
|
||||||
.formCard {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ export { Router };
|
||||||
export default route(function (/* { store, ssrContext } */) {
|
export default route(function (/* { store, ssrContext } */) {
|
||||||
Router.beforeEach(async (to, from, next) => {
|
Router.beforeEach(async (to, from, next) => {
|
||||||
const { isLoggedIn } = session;
|
const { isLoggedIn } = session;
|
||||||
const outLayout = ['Login', 'TwoFactor', 'VerifyEmail'];
|
const outLayout = Router.options.routes[0].children.map((r) => r.name);
|
||||||
if (!isLoggedIn() && !outLayout.includes(to.name)) {
|
if (!isLoggedIn() && !outLayout.includes(to.name)) {
|
||||||
return next({ name: 'Login', query: { redirect: to.fullPath } });
|
return next({ name: 'Login', query: { redirect: to.fullPath } });
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,18 @@ const routes = [
|
||||||
meta: { title: 'verifyEmail' },
|
meta: { title: 'verifyEmail' },
|
||||||
component: () => import('../pages/Login/VerifyEmail.vue'),
|
component: () => import('../pages/Login/VerifyEmail.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/recoverPassword',
|
||||||
|
name: 'RecoverPassword',
|
||||||
|
meta: { title: 'recoverPassword' },
|
||||||
|
component: () => import('../pages/Login/RecoverPassword.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/resetPassword',
|
||||||
|
name: 'ResetPassword',
|
||||||
|
meta: { title: 'resetPassword' },
|
||||||
|
component: () => import('../pages/Login/ResetPassword.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
describe('Recover Password', () => {
|
||||||
|
const username = 'trainee';
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/#/login');
|
||||||
|
cy.get('#switchLanguage').click();
|
||||||
|
cy.get('.q-menu > :nth-child(1) > .q-item').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go to recover password section and send notification', () => {
|
||||||
|
cy.get('input[aria-label="Username"]').type(username);
|
||||||
|
cy.get(`a[href="#/recoverPassword?user=${username}"]`).click();
|
||||||
|
|
||||||
|
cy.waitForElement('input[aria-label="User or recovery email"]');
|
||||||
|
cy.get('input[aria-label="User or recovery email"]').should(
|
||||||
|
'have.value',
|
||||||
|
username
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.get('.q-notification__message').should('have.text', 'Notification sent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change password to user', () => {
|
||||||
|
// Get token from mail
|
||||||
|
cy.request(
|
||||||
|
`http://localhost:3000/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN`
|
||||||
|
).then((response) => {
|
||||||
|
const regex = /access_token=([a-zA-Z0-9]+)/;
|
||||||
|
const [match] = response.body[0].body.match(regex);
|
||||||
|
|
||||||
|
const resetUrl = '/#/resetPassword?' + match;
|
||||||
|
cy.visit(resetUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change password
|
||||||
|
const newPassword = 'test.1234';
|
||||||
|
cy.waitForElement('input[aria-label="Password"]');
|
||||||
|
cy.waitForElement('input[aria-label="Repeat password"]');
|
||||||
|
cy.get('input[aria-label="Password"]').type(newPassword);
|
||||||
|
cy.get('input[aria-label="Repeat password"]').type(newPassword);
|
||||||
|
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.get('.q-notification__message').should('have.text', 'Password changed');
|
||||||
|
|
||||||
|
// Try to login successfully
|
||||||
|
cy.get('input[aria-label="Username"]').type(username);
|
||||||
|
cy.get('input[aria-label="Password"]').type(newPassword);
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.url().should('contain', '/dashboard');
|
||||||
|
|
||||||
|
// ❗The password cannot be returned because "nightmare" does not meet the requirements
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,56 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
describe('Two Factor', () => {
|
||||||
|
const username = 'sysadmin';
|
||||||
|
const userId = 66;
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/#/login');
|
||||||
|
cy.get('#switchLanguage').click();
|
||||||
|
cy.get('.q-menu > :nth-child(1) > .q-item').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable two factor to sysadmin', () => {
|
||||||
|
cy.request(
|
||||||
|
'PATCH',
|
||||||
|
`http://localhost:3000/api/VnUsers/${userId}/update-user?access_token=DEFAULT_TOKEN`,
|
||||||
|
{ twoFactor: 'email' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when login with incorrect two factor', () => {
|
||||||
|
cy.get('input[aria-label="Username"]').type(username);
|
||||||
|
cy.get('input[aria-label="Password"]').type('nightmare');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.get('.q-notification__message').should(
|
||||||
|
'have.text',
|
||||||
|
'Two-factor verification required'
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get('input[type="text"]').type('123456');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.url().should('contain', '/twoFactor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should login with correct two factor', () => {
|
||||||
|
cy.get('input[aria-label="Username"]').type(username);
|
||||||
|
cy.get('input[aria-label="Password"]').type('nightmare');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.get('.q-notification__message').should(
|
||||||
|
'have.text',
|
||||||
|
'Two-factor verification required'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get code from mail
|
||||||
|
cy.request(
|
||||||
|
`http://localhost:3000/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN`
|
||||||
|
).then((response) => {
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = response.body[0].body;
|
||||||
|
const codeElement = tempDiv.querySelector('.code');
|
||||||
|
const code = codeElement ? codeElement.textContent.trim() : null;
|
||||||
|
|
||||||
|
cy.get('input[type="text"]').type(code);
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.url().should('contain', '/dashboard');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue