diff --git a/src/components/ui/VnOutForm.vue b/src/components/ui/VnOutForm.vue new file mode 100644 index 000000000..e7ad441d0 --- /dev/null +++ b/src/components/ui/VnOutForm.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 12680d0cb..ee53da80c 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -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 diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 900de6d07..86963f3dc 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -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 diff --git a/src/pages/Login/LoginMain.vue b/src/pages/Login/LoginMain.vue index 4eb21f573..c17201969 100644 --- a/src/pages/Login/LoginMain.vue +++ b/src/pages/Login/LoginMain.vue @@ -72,7 +72,8 @@ async function onSubmit() { :rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]" class="red" /> -
+ +
+ + {{ t('I do not remember my password') }} +
- - + +es: + I do not remember my password: No recuerdo mi contraseña + diff --git a/src/pages/Login/RecoverPassword.vue b/src/pages/Login/RecoverPassword.vue new file mode 100644 index 000000000..5e0fd367a --- /dev/null +++ b/src/pages/Login/RecoverPassword.vue @@ -0,0 +1,59 @@ + + diff --git a/src/pages/Login/ResetPassword.vue b/src/pages/Login/ResetPassword.vue new file mode 100644 index 000000000..eff718e97 --- /dev/null +++ b/src/pages/Login/ResetPassword.vue @@ -0,0 +1,99 @@ + + diff --git a/src/pages/Login/TwoFactor.vue b/src/pages/Login/TwoFactor.vue index 31b4ccc79..dd404094f 100644 --- a/src/pages/Login/TwoFactor.vue +++ b/src/pages/Login/TwoFactor.vue @@ -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,24 +39,22 @@ async function onSubmit() { } - diff --git a/src/router/index.js b/src/router/index.js index faa3ab5d4..686da2dde 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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 } }); } diff --git a/src/router/routes.js b/src/router/routes.js index 805eefb8c..cced308b5 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -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'), + }, ], }, { diff --git a/test/cypress/integration/login.spec.js b/test/cypress/integration/outLogin/login.spec.js similarity index 100% rename from test/cypress/integration/login.spec.js rename to test/cypress/integration/outLogin/login.spec.js diff --git a/test/cypress/integration/logout.spec.js b/test/cypress/integration/outLogin/logout.spec.js similarity index 100% rename from test/cypress/integration/logout.spec.js rename to test/cypress/integration/outLogin/logout.spec.js diff --git a/test/cypress/integration/outLogin/recoverPassword.spec.js b/test/cypress/integration/outLogin/recoverPassword.spec.js new file mode 100755 index 000000000..eec81b661 --- /dev/null +++ b/test/cypress/integration/outLogin/recoverPassword.spec.js @@ -0,0 +1,54 @@ +/// +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 + }); +}); diff --git a/test/cypress/integration/outLogin/twoFactor.spec.js b/test/cypress/integration/outLogin/twoFactor.spec.js new file mode 100755 index 000000000..4d8561f0f --- /dev/null +++ b/test/cypress/integration/outLogin/twoFactor.spec.js @@ -0,0 +1,56 @@ +/// +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'); + }); + }); +});