4077-login_recover-password & account_verifyEmail #1063

Merged
alexm merged 52 commits from 4077-login_recover-password into dev 2022-11-28 11:34:03 +00:00
47 changed files with 734 additions and 141 deletions

View File

@ -0,0 +1,30 @@
module.exports = Self => {
Self.remoteMethod('recoverPassword', {
description: 'Send email to the user',
accepts: [
{
arg: 'email',
type: 'string',
description: 'The email of user',
required: true
}
],
http: {
path: `/recoverPassword`,
verb: 'POST'
}
});
Self.recoverPassword = async function(email) {
const models = Self.app.models;
try {
await models.user.resetPassword({email, emailTemplate: 'recover-password'});
alexm marked this conversation as resolved Outdated
Outdated
Review

He dejado esta ruta(recoverPassword) porque si se llama directamente resetPassword y el correo que se le pasa no pertenece a un usuario, devuelve un error al frontend.

Usando una ruta con try catch, hacemos que no devuelva nunca error y asi no pueden saber si ese correo es de un usuario nuestro o no.

He dejado esta ruta(recoverPassword) porque si se llama directamente resetPassword y el correo que se le pasa no pertenece a un usuario, devuelve un error al frontend. Usando una ruta con try catch, hacemos que no devuelva nunca error y asi no pueden saber si ese correo es de un usuario nuestro o no.
Outdated
Review

Nomes deuria de ignorar el error de tipo "usuario no existe", tots els demes deuria de rellançarlos

catch(err) {
	if (err.code === 'EMAIL_NOT_FOUND')
    	console.error(err);
    else
    	throw err;
}
	
Nomes deuria de ignorar el error de tipo "usuario no existe", tots els demes deuria de rellançarlos ``` catch(err) { if (err.code === 'EMAIL_NOT_FOUND') console.error(err); else throw err; } ```
} catch (err) {
if (err.code === 'EMAIL_NOT_FOUND')
return;
else
throw err;
}
};
};

View File

@ -1,6 +1,6 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
describe('account changePassword()', () => { describe('account setPassword()', () => {
it('should throw an error when password does not meet requirements', async() => { it('should throw an error when password does not meet requirements', async() => {
let req = app.models.Account.setPassword(1, 'insecurePass'); let req = app.models.Account.setPassword(1, 'insecurePass');

View File

@ -1,4 +1,7 @@
/* eslint max-len: ["error", { "code": 150 }]*/
const md5 = require('md5'); const md5 = require('md5');
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
module.exports = Self => { module.exports = Self => {
require('../methods/account/login')(Self); require('../methods/account/login')(Self);
@ -6,6 +9,7 @@ module.exports = Self => {
require('../methods/account/acl')(Self); require('../methods/account/acl')(Self);
require('../methods/account/change-password')(Self); require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self); require('../methods/account/set-password')(Self);
require('../methods/account/recover-password')(Self);
require('../methods/account/validate-token')(Self); require('../methods/account/validate-token')(Self);
require('../methods/account/privileges')(Self); require('../methods/account/privileges')(Self);
@ -27,17 +31,62 @@ module.exports = Self => {
ctx.data.password = md5(ctx.data.password); ctx.data.password = md5(ctx.data.password);
}); });
Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
if (!ctx.args || !ctx.args.data.email) return;
const models = Self.app.models;
const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const url = origin.split(':');
const userId = ctx.instance.id;
const user = await models.user.findById(userId);
class Mailer {
async send(verifyOptions, cb) {
alexm marked this conversation as resolved Outdated
Outdated
Review

Plantilla ejs

Plantilla *ejs*
const params = {
url: verifyOptions.verifyHref,
recipient: verifyOptions.to,
lang: ctx.req.getLocale()
};
const email = new Email('email-verify', params);
email.send();
alexm marked this conversation as resolved Outdated
Outdated
Review
Cridar al métode `User.verify()` com ací: * https://gitea.verdnatura.es/juan/hedera-web/src/branch/master/back/common/models/user.js#L22 * https://loopback.io/doc/en/lb2/Registering-users.html
cb(null, verifyOptions.to);
}
}
const options = {
type: 'email',
to: instance.email,
from: {},
redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`,
template: false,
mailer: new Mailer,
host: url[1].split('/')[2],
port: url[2],
protocol: url[0],
user: Self
};
await user.verify(options);
});
Self.remoteMethod('getCurrentUserData', { Self.remoteMethod('getCurrentUserData', {
description: 'Gets the current user data', description: 'Gets the current user data',
accepts: [ accepts: [
{ {
arg: 'ctx', arg: 'ctx',
type: 'Object', type: 'object',
http: {source: 'context'} http: {source: 'context'}
} }
], ],
returns: { returns: {
type: 'Object', type: 'object',
root: true root: true
}, },
http: { http: {
@ -58,7 +107,7 @@ module.exports = Self => {
* *
* @param {Integer} userId The user id * @param {Integer} userId The user id
* @param {String} name The role name * @param {String} name The role name
* @param {Object} options Options * @param {object} options Options
* @return {Boolean} %true if user has the role, %false otherwise * @return {Boolean} %true if user has the role, %false otherwise
*/ */
Self.hasRole = async function(userId, name, options) { Self.hasRole = async function(userId, name, options) {
@ -70,8 +119,8 @@ module.exports = Self => {
* Get all user roles. * Get all user roles.
* *
* @param {Integer} userId The user id * @param {Integer} userId The user id
* @param {Object} options Options * @param {object} options Options
* @return {Object} User role list * @return {object} User role list
*/ */
Self.getRoles = async(userId, options) => { Self.getRoles = async(userId, options) => {
let result = await Self.rawSql( let result = await Self.rawSql(

View File

@ -40,6 +40,9 @@
"email": { "email": {
"type": "string" "type": "string"
}, },
"emailVerified": {
"type": "boolean"
},
"created": { "created": {
"type": "date" "type": "date"
}, },
@ -88,6 +91,13 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
},
{
"property": "recoverPassword",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}, },
{ {
"property": "logout", "property": "logout",

View File

@ -1,14 +1,14 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('loopback model Account', () => { describe('loopback model Account', () => {
it('should return true if the user has the given role', async() => { it('should return true if the user has the given role', async() => {
let result = await app.models.Account.hasRole(1, 'employee'); let result = await models.Account.hasRole(1, 'employee');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
}); });
it('should return false if the user doesnt have the given role', async() => { it('should return false if the user doesnt have the given role', async() => {
let result = await app.models.Account.hasRole(1, 'administrator'); let result = await models.Account.hasRole(1, 'administrator');
expect(result).toBeFalsy(); expect(result).toBeFalsy();
}); });

View File

@ -0,0 +1,32 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('account recoverPassword()', () => {
const userId = 1107;
const activeCtx = {
accessToken: {userId: userId},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should send email with token', async() => {
const userId = 1107;
const user = await models.Account.findById(userId);
await models.Account.recoverPassword(user.email);
const result = await models.AccessToken.findOne({where: {userId: userId}});
expect(result).toBeDefined();
});
});

27
back/models/user.js Normal file
View File

@ -0,0 +1,27 @@
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
module.exports = function(Self) {
Self.on('resetPasswordRequest', async function(info) {
const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const user = await Self.app.models.Account.findById(info.user.id);
const params = {
recipient: info.email,
lang: user.lang,
url: `${origin}/#!/reset-password?access_token=${info.accessToken.id}`
};
const options = Object.assign({}, info.options);
for (const param in options)
params[param] = options[param];
const email = new Email(options.emailTemplate, params);
return email.send();
});
};

View File

@ -0,0 +1,2 @@
DELETE FROM `salix`.`ACL`
WHERE model = 'UserPassword';

View File

@ -29,6 +29,11 @@ export default {
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]', firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]' firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]'
}, },
recoverPassword: {
recoverPasswordButton: 'vn-login a[ui-sref="recoverPassword"]',
email: 'vn-recover-password vn-textfield[ng-model="$ctrl.email"]',
sendEmailButton: 'vn-recover-password vn-submit',
},
accountIndex: { accountIndex: {
addAccount: 'vn-user-index button vn-icon[icon="add"]', addAccount: 'vn-user-index button vn-icon[icon="add"]',
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]', newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',

View File

@ -0,0 +1,40 @@
import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer';
describe('Login path', async() => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
await page.waitForState('recoverPassword');
});
afterAll(async() => {
await browser.close();
});
it('should not throw error if not exist user', async() => {
await page.write(selectors.recoverPassword.email, 'fakeEmail@mydomain.com');
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Notification sent!');
});
it('should send email', async() => {
await page.waitForState('login');
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
await page.write(selectors.recoverPassword.email, 'BruceWayne@mydomain.com');
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const message = await page.waitForSnackbar();
await page.waitForState('login');
expect(message.text).toContain('Notification sent!');
});
});

View File

@ -5,80 +5,78 @@ describe('Ticket Future path', () => {
let browser; let browser;
let page; let page;
beforeAll(async () => { beforeAll(async() => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('employee', 'ticket'); await page.loginAndModule('employee', 'ticket');
await page.accessToSection('ticket.future'); await page.accessToSection('ticket.future');
}); });
afterAll(async () => { afterAll(async() => {
await browser.close(); await browser.close();
}); });
const now = new Date(); const now = new Date();
const tomorrow = new Date(now.getDate() + 1); const tomorrow = new Date(now.getDate() + 1);
const ticket = {
originDated: now,
futureDated: now,
linesMax: '9999',
litersMax: '9999',
warehouseFk: 'Warehouse One'
};
it('should show errors snackbar because of the required data', async () => { it('should show errors snackbar because of the required data', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.warehouseFk); await page.clearInput(selectors.ticketFuture.warehouseFk);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
let message = await page.waitForSnackbar(); let message = await page.waitForSnackbar();
expect(message.text).toContain('warehouseFk is a required argument'); expect(message.text).toContain('warehouseFk is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.litersMax); await page.clearInput(selectors.ticketFuture.litersMax);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('litersMax is a required argument'); expect(message.text).toContain('litersMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.linesMax); await page.clearInput(selectors.ticketFuture.linesMax);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('linesMax is a required argument'); expect(message.text).toContain('linesMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.futureDated); await page.clearInput(selectors.ticketFuture.futureDated);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('futureDated is a required argument'); expect(message.text).toContain('futureDated is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.originDated); await page.clearInput(selectors.ticketFuture.originDated);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('originDated is a required argument'); expect(message.text).toContain('originDated is a required argument');
}); });
it('should search with the required data', async () => { it('should search with the required data', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search with the origin shipped today', async () => { it('should search with the origin shipped today', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, now); await page.pickDate(selectors.ticketFuture.shipped, now);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search with the origin shipped tomorrow', async () => { it('should search with the origin shipped tomorrow', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, tomorrow); await page.pickDate(selectors.ticketFuture.shipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
}); });
it('should search with the destination shipped today', async () => { it('should search with the destination shipped today', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
await page.pickDate(selectors.ticketFuture.tfShipped, now); await page.pickDate(selectors.ticketFuture.tfShipped, now);
@ -86,14 +84,14 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search with the destination shipped tomorrow', async () => { it('should search with the destination shipped tomorrow', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow); await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
}); });
it('should search with the origin IPT', async () => { it('should search with the origin IPT', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
@ -108,7 +106,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
}); });
it('should search with the destination IPT', async () => { it('should search with the destination IPT', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
@ -123,7 +121,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
}); });
it('should search with the origin grouped state', async () => { it('should search with the origin grouped state', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
@ -138,7 +136,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 3); await page.waitForNumberOfElements(selectors.ticketFuture.table, 3);
}); });
it('should search with the destination grouped state', async () => { it('should search with the destination grouped state', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
@ -164,10 +162,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search in smart-table with an ID Origin', async () => { it('should search in smart-table with an ID Origin', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableId, "13"); await page.write(selectors.ticketFuture.tableId, '13');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 2); await page.waitForNumberOfElements(selectors.ticketFuture.table, 2);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -176,10 +174,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search in smart-table with an ID Destination', async () => { it('should search in smart-table with an ID Destination', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableTfId, "12"); await page.write(selectors.ticketFuture.tableTfId, '12');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -188,7 +186,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search in smart-table with an IPT Origin', async () => { it('should search in smart-table with an IPT Origin', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical'); await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
@ -199,7 +197,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search in smart-table with an IPT Destination', async () => { it('should search in smart-table with an IPT Destination', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical'); await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
@ -210,10 +208,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search in smart-table with especified Lines', async () => { it('should search in smart-table with especified Lines', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLines, "0"); await page.write(selectors.ticketFuture.tableLines, '0');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -222,8 +220,8 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLines, "1"); await page.write(selectors.ticketFuture.tableLines, '1');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -232,10 +230,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should search in smart-table with especified Liters', async () => { it('should search in smart-table with especified Liters', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLiters, "0"); await page.write(selectors.ticketFuture.tableLiters, '0');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -244,8 +242,8 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLiters, "28"); await page.write(selectors.ticketFuture.tableLiters, '28');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -254,13 +252,13 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
}); });
it('should check the three last tickets and move to the future', async () => { it('should check the three last tickets and move to the future', async() => {
await page.waitToClick(selectors.ticketFuture.multiCheck); await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck); await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton); await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.ticketFuture.acceptButton); await page.waitToClick(selectors.ticketFuture.acceptButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!'); expect(message.text).toContain('Tickets moved successfully!');
}); });
}); });

View File

@ -12,9 +12,10 @@ export default class Component extends EventEmitter {
* @param {HTMLElement} $element The main component element * @param {HTMLElement} $element The main component element
* @param {$rootScope.Scope} $scope The element scope * @param {$rootScope.Scope} $scope The element scope
* @param {Function} $transclude The transclusion function * @param {Function} $transclude The transclusion function
* @param {Function} $location The location function
*/ */
constructor($element, $scope, $transclude) { constructor($element, $scope, $transclude, $location) {
super(); super($element, $scope, $transclude, $location);
this.$ = $scope; this.$ = $scope;
if (!$element) return; if (!$element) return;
@ -164,7 +165,7 @@ export default class Component extends EventEmitter {
$transclude.$$boundTransclude.$$slots[slot]; $transclude.$$boundTransclude.$$slots[slot];
} }
} }
Component.$inject = ['$element', '$scope']; Component.$inject = ['$element', '$scope', '$location', '$state'];
/* /*
* Automatically adds the most used services to the prototype, so they are * Automatically adds the most used services to the prototype, so they are

View File

@ -23,7 +23,10 @@ export default class Auth {
initialize() { initialize() {
let criteria = { let criteria = {
to: state => state.name != 'login' to: state => {
const outLayout = ['login', 'recoverPassword', 'resetPassword'];
return !outLayout.some(ol => ol == state.name);
}
}; };
this.$transitions.onStart(criteria, transition => { this.$transitions.onStart(criteria, transition => {
if (this.loggedIn) if (this.loggedIn)

View File

@ -1,9 +1,8 @@
<vn-layout <vn-layout
ng-if="$ctrl.showLayout"> ng-if="$ctrl.showLayout">
</vn-layout> </vn-layout>
<ui-view <vn-out-layout
name="login"
ng-if="!$ctrl.showLayout"> ng-if="!$ctrl.showLayout">
</ui-view> </vn-out-layout>
<vn-snackbar vn-id="snackbar"></vn-snackbar> <vn-snackbar vn-id="snackbar"></vn-snackbar>
<vn-debug-info></vn-debug-info> <vn-debug-info></vn-debug-info>

View File

@ -9,13 +9,20 @@ import Component from 'core/lib/component';
* @property {SideMenu} rightMenu The left menu, if it's present * @property {SideMenu} rightMenu The left menu, if it's present
*/ */
export default class App extends Component { export default class App extends Component {
constructor($element, $, $location, $state) {
super($element, $, $location, $state);
this.$location = $location;
this.$state = $state;
}
$postLink() { $postLink() {
alexm marked this conversation as resolved Outdated
Outdated
Review

Açò funciona en tots els casos? Que passa si un estat conte login?

Açò funciona en **tots** els casos? Que passa si un estat conte login?
this.vnApp.logger = this; this.vnApp.logger = this;
} }
get showLayout() { get showLayout() {
let state = this.$state.current.name; const state = this.$state.current.name || this.$location.$$path.substring(1).replace('/', '.');
return state && state != 'login'; const outLayout = ['login', 'recoverPassword', 'resetPassword'];
return state && !outLayout.some(ol => ol == state);
} }
$onDestroy() { $onDestroy() {

View File

@ -5,7 +5,10 @@ import './descriptor-popover';
import './home/home'; import './home/home';
import './layout'; import './layout';
import './left-menu/left-menu'; import './left-menu/left-menu';
import './login/index';
import './login/login'; import './login/login';
import './login/recover-password';
import './login/reset-password';
import './module-card'; import './module-card';
import './module-main'; import './module-main';
import './side-menu/side-menu'; import './side-menu/side-menu';

View File

@ -0,0 +1,6 @@
<div class="box">
<img src="./logo.svg"/>
<form name="form">
<ui-view></ui-view>
</form>
</div>

View File

@ -0,0 +1,16 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
export default class OutLayout extends Component {
constructor($element, $scope) {
super($element, $scope);
}
}
OutLayout.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnOutLayout', {
template: require('./index.html'),
controller: OutLayout
});

View File

@ -2,3 +2,7 @@ User: User
Password: Password Password: Password
Do not close session: Do not close session Do not close session: Do not close session
Enter: Enter Enter: Enter
Password requirements: >
The password must have at least {{ length }} length characters,
{{nAlpha}} alphabetic characters, {{nUpper}} capital letters, {{nDigits}}
digits and {{nPunct}} symbols (Ex: $%&.)

View File

@ -1,4 +1,16 @@
User: Usuario User: Usuario
Password: Contraseña Password: Contraseña
Email: Correo electrónico
Do not close session: No cerrar sesión Do not close session: No cerrar sesión
Enter: Entrar Enter: Entrar
I do not remember my password: No recuerdo mi contraseña
Recover password: Recuperar contraseña
We will sent you an email to recover your password: Te enviaremos un correo para restablecer tu contraseña
Notification sent!: ¡Notificación enviada!
Reset password: Restrablecer contraseña
New password: Nueva contraseña
Repeat password: Repetir contraseña
Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud,
{{nAlpha}} caracteres alfabéticos, {{nUpper}} letras mayúsculas, {{nDigits}}
dígitos y {{nPunct}} símbolos (Ej: $%&.)

View File

@ -1,27 +1,27 @@
<div class="box"> <vn-textfield
<img src="./logo.svg"/>
<form name="form" ng-submit="$ctrl.submit()">
<vn-textfield
label="User" label="User"
ng-model="$ctrl.user" ng-model="$ctrl.user"
vn-id="userField" vn-id="userField"
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Password" label="Password"
ng-model="$ctrl.password" ng-model="$ctrl.password"
type="password"> type="password">
</vn-textfield> </vn-textfield>
<vn-check <vn-check
label="Do not close session" label="Do not close session"
ng-model="$ctrl.remember" ng-model="$ctrl.remember"
name="remember"> name="remember">
</vn-check> </vn-check>
<div class="footer"> <div class="footer">
<vn-submit label="Enter"></vn-submit> <vn-submit label="Enter" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper"> <div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner> <vn-spinner enable="$ctrl.loading"></vn-spinner>
</div> </div>
<div class="vn-pt-lg">
<a ui-sref="recoverPassword" translate>
I do not remember my password
</a>
</div> </div>
</form>
</div> </div>

View File

@ -0,0 +1,17 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Recover password</h5>
<vn-textfield
label="Email"
ng-model="$ctrl.email"
vn-focus>
</vn-textfield>
<div
class="text-secondary"
translate>
We will sent you an email to recover your password
</div>
<div class="footer">
<vn-submit label="Recover password" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>

View File

@ -0,0 +1,37 @@
import ngModule from '../../module';
import './style.scss';
export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state) {
Object.assign(this, {
$scope,
$element,
$http,
vnApp,
$translate,
$state
});
}
goToLogin() {
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
this.$state.go('login');
}
submit() {
const params = {
email: this.email
};
this.$http.post('Accounts/recoverPassword', params)
.then(() => {
this.goToLogin();
});
}
}
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state'];
ngModule.vnComponent('vnRecoverPassword', {
template: require('./recover-password.html'),
controller: Controller
});

View File

@ -0,0 +1,19 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Reset password</h5>
<vn-textfield
label="New password"
ng-model="$ctrl.newPassword"
type="password"
info="{{'Password requirements' | translate:$ctrl.passRequirements}}"
vn-focus>
</vn-textfield>
<vn-textfield
label="Repeat password"
ng-model="$ctrl.repeatPassword"
type="password">
</vn-textfield>
<div class="footer">
<vn-submit label="Reset password" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>

View File

@ -0,0 +1,48 @@
import ngModule from '../../module';
import './style.scss';
alexm marked this conversation as resolved
Review

Perque gastes axios y no el servei $http?

Perque gastes axios y no el servei $http?
export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state, $location) {
Object.assign(this, {
$scope,
$element,
$http,
vnApp,
$translate,
$state,
$location
});
}
$onInit() {
this.$http.get('UserPasswords/findOne')
.then(res => {
this.passRequirements = res.data;
});
}
submit() {
if (!this.newPassword)
throw new UserError(`You must enter a new password`);
if (this.newPassword != this.repeatPassword)
throw new UserError(`Passwords don't match`);
const headers = {
Authorization: this.$location.$$search.access_token
};
const newPassword = this.newPassword;
this.$http.post('users/reset-password', {newPassword}, {headers})
.then(() => {
this.vnApp.showSuccess(this.$translate.instant('Password changed!'));
this.$state.go('login');
});
}
}
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state', '$location'];
ngModule.vnComponent('vnResetPassword', {
template: require('./reset-password.html'),
controller: Controller
});

View File

@ -1,6 +1,31 @@
@import "variables"; @import "variables";
vn-login { vn-login,
vn-reset-password,
vn-recover-password{
.footer {
margin-top: 32px;
text-align: center;
position: relative;
& > .vn-submit {
display: block;
& > input {
display: block;
width: 100%;
}
}
& > .spinner-wrapper {
position: absolute;
width: 0;
top: 3px;
right: -8px;
overflow: visible;
}
}
}
vn-out-layout{
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -39,28 +64,17 @@ vn-login {
white-space: inherit; white-space: inherit;
} }
} }
& > .footer { }
margin-top: 32px;
h5{
color: $color-primary;
}
.text-secondary{
text-align: center; text-align: center;
position: relative; padding-bottom: 16px;
}
& > vn-submit {
display: block;
& > input {
display: block;
width: 100%;
}
}
& > .spinner-wrapper {
position: absolute;
width: 0;
top: 3px;
right: -8px;
overflow: visible;
}
}
}
} }
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
@ -71,4 +85,8 @@ vn-login {
box-shadow: none; box-shadow: none;
} }
} }
a{
color: $color-primary;
}
} }

View File

@ -112,7 +112,7 @@ function $exceptionHandler(vnApp, $window, $state, $injector) {
switch (exception.status) { switch (exception.status) {
case 401: case 401:
if ($state.current.name != 'login') { if (!$state.current.name.includes('login')) {
messageT = 'Session has expired'; messageT = 'Session has expired';
let params = {continue: $window.location.hash}; let params = {continue: $window.location.hash};
$state.go('login', params); $state.go('login', params);

View File

@ -9,9 +9,17 @@ function config($stateProvider, $urlRouterProvider) {
.state('login', { .state('login', {
url: '/login?continue', url: '/login?continue',
description: 'Login', description: 'Login',
views: { template: '<vn-login></vn-login>'
login: {template: '<vn-login></vn-login>'} })
} .state('recoverPassword', {
url: '/recover-password',
description: 'Recover-password',
template: '<vn-recover-password>asd</vn-recover-password>'
})
.state('resetPassword', {
url: '/reset-password',
description: 'Reset-password',
template: '<vn-reset-password></vn-reset-password>'
}) })
.state('home', { .state('home', {
url: '/', url: '/',

View File

@ -131,12 +131,15 @@
"Fichadas impares": "Odd signs", "Fichadas impares": "Odd signs",
"Descanso diario 9h.": "Daily rest 9h.", "Descanso diario 9h.": "Daily rest 9h.",
"Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.", "Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.",
"Verify email": "Verify email",
"Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Click on the following link to verify this email. If you haven't requested this email, just ignore it",
"Password does not meet requirements": "Password does not meet requirements", "Password does not meet requirements": "Password does not meet requirements",
"You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies", "You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies",
"Not enough privileges to edit a client": "Not enough privileges to edit a client", "Not enough privileges to edit a client": "Not enough privileges to edit a client",
"Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*", "Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*",
"You don't have grant privilege": "You don't have grant privilege", "You don't have grant privilege": "You don't have grant privilege",
"You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user", "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user",
"Email verify": "Email verify",
"Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})", "Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})",
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production", "Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production",
"Receipt's bank was not found": "Receipt's bank was not found", "Receipt's bank was not found": "Receipt's bank was not found",

View File

@ -245,6 +245,8 @@
"Already has this status": "Ya tiene este estado", "Already has this status": "Ya tiene este estado",
"There aren't records for this week": "No existen registros para esta semana", "There aren't records for this week": "No existen registros para esta semana",
"Empty data source": "Origen de datos vacio", "Empty data source": "Origen de datos vacio",
"Email verify": "Correo de verificación",
"Landing cannot be lesser than shipment": "Landing cannot be lesser than shipment",
"Receipt's bank was not found": "No se encontró el banco del recibo", "Receipt's bank was not found": "No se encontró el banco del recibo",
"This receipt was not compensated": "Este recibo no ha sido compensado", "This receipt was not compensated": "Este recibo no ha sido compensado",
"Client's email was not found": "No se encontró el email del cliente" "Client's email was not found": "No se encontró el email del cliente"

View File

@ -30,5 +30,13 @@
"type": "number", "type": "number",
"required": true "required": true
} }
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
} }
]
} }

View File

@ -2,6 +2,11 @@ import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
export default class Controller extends Section { export default class Controller extends Section {
$onInit() {
if (this.$params.emailConfirmed)
this.vnApp.showSuccess(this.$t('Email verified successfully!'));
}
onSubmit() { onSubmit() {
this.$.watcher.submit() this.$.watcher.submit()
.then(() => this.card.reload()); .then(() => this.card.reload());

View File

@ -0,0 +1 @@
Email verified successfully!: Correo verificado correctamente!

View File

@ -74,7 +74,7 @@
} }
}, },
{ {
"url": "/basic-data", "url": "/basic-data?emailConfirmed",
"state": "account.card.basicData", "state": "account.card.basicData",
"component": "vn-user-basic-data", "component": "vn-user-basic-data",
"description": "Basic data", "description": "Basic data",

View File

@ -8,10 +8,12 @@ module.exports = {
this.transporter = nodemailer.createTransport(config.smtp); this.transporter = nodemailer.createTransport(config.smtp);
}, },
send(options) { async send(options) {
options.from = `${config.app.senderName} <${config.app.senderEmail}>`; options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
const notProductionError = {message: 'This not production, this email not sended'};
Review

He modificado esta parte porque como esta diseñado print. Solo inserta en mail(a modo de log) cuando realmente se envia el correo (producción). Haciendo este cambio podemos ver los logs tanto en local como en test.

Aunque al final no he usado esta funcionalidad para los test creo que es util para hacer pruebas en test

He modificado esta parte porque como esta diseñado print. Solo inserta en mail(a modo de log) cuando realmente se envia el correo (producción). Haciendo este cambio podemos ver los logs tanto en local como en test. Aunque al final no he usado esta funcionalidad para los test creo que es util para hacer pruebas en test
await this.mailLog(options, notProductionError);
if (!config.smtp.auth.user) if (!config.smtp.auth.user)
return Promise.resolve(true); return Promise.resolve(true);
@ -24,6 +26,11 @@ module.exports = {
throw err; throw err;
}).finally(async() => { }).finally(async() => {
await this.mailLog(options, error);
});
},
async mailLog(options, error) {
const attachments = []; const attachments = [];
if (options.attachments) { if (options.attachments) {
for (let attachment of options.attachments) { for (let attachment of options.attachments) {
@ -37,6 +44,7 @@ module.exports = {
} }
const fileNames = attachments.join(',\n'); const fileNames = attachments.join(',\n');
await db.rawSql(` await db.rawSql(`
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status) INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
VALUES (?, ?, 1, ?, ?, ?, ?)`, [ VALUES (?, ?, 1, ?, ?, ?, ?)`, [
@ -47,6 +55,6 @@ module.exports = {
fileNames, fileNames,
error && error.message || 'Sent' error && error.message || 'Sent'
]); ]);
});
} }
}; };

View File

@ -0,0 +1,13 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,5 @@
.external-link {
border: 2px dashed #8dba25;
border-radius: 3px;
text-align: center
}

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<p>
{{ $t(`click`) }}
<a :href="url">{{ $t('subject') }}</a>
</p>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,17 @@
const Component = require(`vn-print/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
module.exports = {
name: 'email-verify',
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build()
},
props: {
url: {
type: [String],
required: true
}
}
};

View File

@ -0,0 +1,3 @@
subject: Email Verify
title: Email Verify
click: Click on the following link to verify this email. If you haven't requested this email, just ignore it

View File

@ -0,0 +1,3 @@
subject: Verificar correo
title: Verificar correo
click: Pulsa en el siguiente link para verificar este correo. Si no has pedido este correo, simplemente ignóralo

View File

@ -0,0 +1,13 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,5 @@
.external-link {
border: 2px dashed #8dba25;
border-radius: 3px;
text-align: center
}

View File

@ -0,0 +1,3 @@
subject: Recuperar contraseña
title: Recuperar contraseña
Click on the following link to change your password.: Pulsa en el siguiente link para cambiar tu contraseña.

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<p>
{{ $t('Click on the following link to change your password.') }}
<a :href="url">{{ $t('subject') }}</a>
</p>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,17 @@
const Component = require(`vn-print/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
module.exports = {
name: 'recover-password',
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build()
},
props: {
url: {
type: [String],
required: true
}
}
};