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
|
||||
ldap: LDAP
|
||||
samba: Samba
|
||||
twoFactor: Two factor
|
||||
recoverPassword: Recover password
|
||||
resetPassword: Reset password
|
||||
created: Created
|
||||
worker: Worker
|
||||
now: Now
|
||||
|
@ -288,14 +291,17 @@ twoFactor:
|
|||
explanation: >-
|
||||
Please, enter the verification code that we have sent to your email in the
|
||||
next 5 minutes
|
||||
pageTitles:
|
||||
twoFactor: Two-Factor
|
||||
verifyEmail:
|
||||
pageTitles:
|
||||
verifyEmail: Email verification
|
||||
dashboard:
|
||||
pageTitles:
|
||||
|
||||
recoverPassword:
|
||||
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:
|
||||
list:
|
||||
phone: Phone
|
||||
|
|
|
@ -253,6 +253,9 @@ globals:
|
|||
packages: Bultos
|
||||
ldap: LDAP
|
||||
samba: Samba
|
||||
twoFactor: Doble factor
|
||||
recoverPassword: Recuperar contraseña
|
||||
resetPassword: Restablecer contraseña
|
||||
created: Fecha creación
|
||||
worker: Trabajador
|
||||
now: Ahora
|
||||
|
@ -289,14 +292,17 @@ twoFactor:
|
|||
validate: Validar
|
||||
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
|
||||
pageTitles:
|
||||
twoFactor: Doble factor
|
||||
verifyEmail:
|
||||
pageTitles:
|
||||
verifyEmail: Verificación de correo
|
||||
dashboard:
|
||||
pageTitles:
|
||||
|
||||
recoverPassword:
|
||||
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:
|
||||
list:
|
||||
phone: Teléfono
|
||||
|
|
|
@ -72,7 +72,8 @@ async function onSubmit() {
|
|||
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
|
||||
class="red"
|
||||
/>
|
||||
<div>
|
||||
<QToggle v-model="keepLogin" :label="t('login.keepLogin')" />
|
||||
<div class="column flex-center q-mt-lg">
|
||||
<QBtn
|
||||
:label="t('login.submit')"
|
||||
type="submit"
|
||||
|
@ -81,11 +82,15 @@ async function onSubmit() {
|
|||
rounded
|
||||
unelevated
|
||||
/>
|
||||
<RouterLink
|
||||
class="q-mt-md text-primary"
|
||||
:to="`/recoverPassword?user=${username}`"
|
||||
>
|
||||
{{ t('I do not remember my password') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<QToggle v-model="keepLogin" :label="t('login.keepLogin')" />
|
||||
</QForm>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.formCard {
|
||||
max-width: 350px;
|
||||
|
@ -101,3 +106,7 @@ async function onSubmit() {
|
|||
}
|
||||
}
|
||||
</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 { useLogin } from 'src/composables/useLogin';
|
||||
import VnInput from 'src/components/common/VnInput.vue';
|
||||
import VnOutForm from 'src/components/ui/VnOutForm.vue';
|
||||
|
||||
const quasar = useQuasar();
|
||||
const session = useSession();
|
||||
|
@ -38,11 +39,8 @@ async function onSubmit() {
|
|||
}
|
||||
</script>
|
||||
<template>
|
||||
<QForm @submit="onSubmit" class="q-gutter-y-md q-pa-lg formCard">
|
||||
<div class="column items-center">
|
||||
<QIcon name="phonelink_lock" size="xl" color="primary" />
|
||||
<h5 class="text-center q-my-md">{{ t('twoFactor.insert') }}</h5>
|
||||
</div>
|
||||
<VnOutForm @submit="onSubmit" :title="t('twoFactor.insert')">
|
||||
<template #default>
|
||||
<VnInput
|
||||
v-model="code"
|
||||
:hint="t('twoFactor.explanation')"
|
||||
|
@ -55,7 +53,8 @@ async function onSubmit() {
|
|||
<QIcon name="lock" />
|
||||
</template>
|
||||
</VnInput>
|
||||
<div class="q-mt-xl">
|
||||
</template>
|
||||
<template #buttons>
|
||||
<QBtn
|
||||
:label="t('twoFactor.validate')"
|
||||
type="submit"
|
||||
|
@ -64,18 +63,6 @@ async function onSubmit() {
|
|||
rounded
|
||||
unelevated
|
||||
/>
|
||||
</div>
|
||||
</QForm>
|
||||
</template>
|
||||
</VnOutForm>
|
||||
</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 } */) {
|
||||
Router.beforeEach(async (to, from, next) => {
|
||||
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)) {
|
||||
return next({ name: 'Login', query: { redirect: to.fullPath } });
|
||||
}
|
||||
|
|
|
@ -46,6 +46,18 @@ const routes = [
|
|||
meta: { title: 'verifyEmail' },
|
||||
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