4077-login_recover-password & account_verifyEmail #1063
|
@ -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'});
|
||||
} catch (err) {
|
||||
if (err.code === 'EMAIL_NOT_FOUND')
|
||||
return;
|
||||
else
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
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() => {
|
||||
let req = app.models.Account.setPassword(1, 'insecurePass');
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
/* eslint max-len: ["error", { "code": 150 }]*/
|
||||
const md5 = require('md5');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
const {Email} = require('vn-print');
|
||||
|
||||
module.exports = Self => {
|
||||
require('../methods/account/login')(Self);
|
||||
|
@ -6,6 +9,7 @@ module.exports = Self => {
|
|||
require('../methods/account/acl')(Self);
|
||||
require('../methods/account/change-password')(Self);
|
||||
require('../methods/account/set-password')(Self);
|
||||
require('../methods/account/recover-password')(Self);
|
||||
require('../methods/account/validate-token')(Self);
|
||||
require('../methods/account/privileges')(Self);
|
||||
|
||||
|
@ -27,17 +31,62 @@ module.exports = Self => {
|
|||
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) {
|
||||
const params = {
|
||||
url: verifyOptions.verifyHref,
|
||||
recipient: verifyOptions.to,
|
||||
lang: ctx.req.getLocale()
|
||||
};
|
||||
|
||||
const email = new Email('email-verify', params);
|
||||
email.send();
|
||||
|
||||
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', {
|
||||
description: 'Gets the current user data',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'ctx',
|
||||
type: 'Object',
|
||||
type: 'object',
|
||||
http: {source: 'context'}
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'Object',
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
|
@ -58,7 +107,7 @@ module.exports = Self => {
|
|||
*
|
||||
* @param {Integer} userId The user id
|
||||
* @param {String} name The role name
|
||||
* @param {Object} options Options
|
||||
* @param {object} options Options
|
||||
* @return {Boolean} %true if user has the role, %false otherwise
|
||||
*/
|
||||
Self.hasRole = async function(userId, name, options) {
|
||||
|
@ -70,8 +119,8 @@ module.exports = Self => {
|
|||
* Get all user roles.
|
||||
*
|
||||
* @param {Integer} userId The user id
|
||||
* @param {Object} options Options
|
||||
* @return {Object} User role list
|
||||
* @param {object} options Options
|
||||
* @return {object} User role list
|
||||
*/
|
||||
Self.getRoles = async(userId, options) => {
|
||||
let result = await Self.rawSql(
|
||||
|
|
|
@ -40,6 +40,9 @@
|
|||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailVerified": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"created": {
|
||||
"type": "date"
|
||||
},
|
||||
|
@ -88,16 +91,23 @@
|
|||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
},
|
||||
{
|
||||
"property": "recoverPassword",
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "logout",
|
||||
"property": "logout",
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$authenticated",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "validateToken",
|
||||
"property": "validateToken",
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$authenticated",
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
const models = require('vn-loopback/server/server').models;
|
||||
|
||||
describe('loopback model Account', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
DELETE FROM `salix`.`ACL`
|
||||
WHERE model = 'UserPassword';
|
|
@ -29,6 +29,11 @@ export default {
|
|||
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
|
||||
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: {
|
||||
addAccount: 'vn-user-index button vn-icon[icon="add"]',
|
||||
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',
|
||||
|
|
|
@ -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!');
|
||||
});
|
||||
});
|
|
@ -5,80 +5,78 @@ describe('Ticket Future path', () => {
|
|||
let browser;
|
||||
let page;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('employee', 'ticket');
|
||||
await page.accessToSection('ticket.future');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
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.clearInput(selectors.ticketFuture.warehouseFk);
|
||||
await page.waitToClick(selectors.ticketFuture.submit);
|
||||
let message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('warehouseFk is a required argument');
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
|
||||
await page.clearInput(selectors.ticketFuture.litersMax);
|
||||
await page.waitToClick(selectors.ticketFuture.submit);
|
||||
message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('litersMax is a required argument');
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
|
||||
await page.clearInput(selectors.ticketFuture.linesMax);
|
||||
await page.waitToClick(selectors.ticketFuture.submit);
|
||||
message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('linesMax is a required argument');
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
|
||||
await page.clearInput(selectors.ticketFuture.futureDated);
|
||||
await page.waitToClick(selectors.ticketFuture.submit);
|
||||
message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('futureDated is a required argument');
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
|
||||
await page.clearInput(selectors.ticketFuture.originDated);
|
||||
await page.waitToClick(selectors.ticketFuture.submit);
|
||||
message = await page.waitForSnackbar();
|
||||
|
||||
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.submit);
|
||||
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.pickDate(selectors.ticketFuture.shipped, now);
|
||||
await page.waitToClick(selectors.ticketFuture.submit);
|
||||
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.pickDate(selectors.ticketFuture.shipped, tomorrow);
|
||||
await page.waitToClick(selectors.ticketFuture.submit);
|
||||
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.clearInput(selectors.ticketFuture.shipped);
|
||||
await page.pickDate(selectors.ticketFuture.tfShipped, now);
|
||||
|
@ -86,14 +84,14 @@ describe('Ticket Future path', () => {
|
|||
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.pickDate(selectors.ticketFuture.tfShipped, tomorrow);
|
||||
await page.waitToClick(selectors.ticketFuture.submit);
|
||||
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.clearInput(selectors.ticketFuture.shipped);
|
||||
|
@ -108,7 +106,7 @@ describe('Ticket Future path', () => {
|
|||
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.clearInput(selectors.ticketFuture.shipped);
|
||||
|
@ -123,7 +121,7 @@ describe('Ticket Future path', () => {
|
|||
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.clearInput(selectors.ticketFuture.shipped);
|
||||
|
@ -138,7 +136,7 @@ describe('Ticket Future path', () => {
|
|||
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.clearInput(selectors.ticketFuture.shipped);
|
||||
|
@ -164,10 +162,10 @@ describe('Ticket Future path', () => {
|
|||
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.write(selectors.ticketFuture.tableId, "13");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.write(selectors.ticketFuture.tableId, '13');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 2);
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
|
||||
|
@ -176,10 +174,10 @@ describe('Ticket Future path', () => {
|
|||
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.write(selectors.ticketFuture.tableTfId, "12");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.write(selectors.ticketFuture.tableTfId, '12');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
|
||||
|
@ -188,7 +186,7 @@ describe('Ticket Future path', () => {
|
|||
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.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical');
|
||||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
|
||||
|
@ -199,7 +197,7 @@ describe('Ticket Future path', () => {
|
|||
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.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical');
|
||||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
|
||||
|
@ -210,10 +208,10 @@ describe('Ticket Future path', () => {
|
|||
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.write(selectors.ticketFuture.tableLines, "0");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.write(selectors.ticketFuture.tableLines, '0');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
|
||||
|
@ -222,8 +220,8 @@ describe('Ticket Future path', () => {
|
|||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
|
||||
await page.write(selectors.ticketFuture.tableLines, "1");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.write(selectors.ticketFuture.tableLines, '1');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
|
||||
|
@ -232,10 +230,10 @@ describe('Ticket Future path', () => {
|
|||
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.write(selectors.ticketFuture.tableLiters, "0");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.write(selectors.ticketFuture.tableLiters, '0');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
|
||||
|
@ -244,8 +242,8 @@ describe('Ticket Future path', () => {
|
|||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
|
||||
await page.write(selectors.ticketFuture.tableLiters, "28");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.write(selectors.ticketFuture.tableLiters, '28');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
|
||||
|
||||
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
|
||||
|
@ -254,13 +252,13 @@ describe('Ticket Future path', () => {
|
|||
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.firstCheck);
|
||||
await page.waitToClick(selectors.ticketFuture.moveButton);
|
||||
await page.waitToClick(selectors.ticketFuture.acceptButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('Tickets moved successfully!');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -12,9 +12,10 @@ export default class Component extends EventEmitter {
|
|||
* @param {HTMLElement} $element The main component element
|
||||
* @param {$rootScope.Scope} $scope The element scope
|
||||
* @param {Function} $transclude The transclusion function
|
||||
* @param {Function} $location The location function
|
||||
*/
|
||||
constructor($element, $scope, $transclude) {
|
||||
super();
|
||||
constructor($element, $scope, $transclude, $location) {
|
||||
super($element, $scope, $transclude, $location);
|
||||
this.$ = $scope;
|
||||
|
||||
if (!$element) return;
|
||||
|
@ -164,7 +165,7 @@ export default class Component extends EventEmitter {
|
|||
$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
|
||||
|
|
|
@ -23,7 +23,10 @@ export default class Auth {
|
|||
|
||||
initialize() {
|
||||
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 => {
|
||||
if (this.loggedIn)
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<vn-layout
|
||||
ng-if="$ctrl.showLayout">
|
||||
</vn-layout>
|
||||
<ui-view
|
||||
name="login"
|
||||
<vn-out-layout
|
||||
ng-if="!$ctrl.showLayout">
|
||||
</ui-view>
|
||||
</vn-out-layout>
|
||||
<vn-snackbar vn-id="snackbar"></vn-snackbar>
|
||||
<vn-debug-info></vn-debug-info>
|
||||
|
|
|
@ -9,13 +9,20 @@ import Component from 'core/lib/component';
|
|||
* @property {SideMenu} rightMenu The left menu, if it's present
|
||||
*/
|
||||
export default class App extends Component {
|
||||
constructor($element, $, $location, $state) {
|
||||
super($element, $, $location, $state);
|
||||
this.$location = $location;
|
||||
this.$state = $state;
|
||||
}
|
||||
|
||||
$postLink() {
|
||||
this.vnApp.logger = this;
|
||||
}
|
||||
|
||||
get showLayout() {
|
||||
let state = this.$state.current.name;
|
||||
return state && state != 'login';
|
||||
const state = this.$state.current.name || this.$location.$$path.substring(1).replace('/', '.');
|
||||
const outLayout = ['login', 'recoverPassword', 'resetPassword'];
|
||||
return state && !outLayout.some(ol => ol == state);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
|
|
|
@ -5,7 +5,10 @@ import './descriptor-popover';
|
|||
import './home/home';
|
||||
import './layout';
|
||||
import './left-menu/left-menu';
|
||||
import './login/index';
|
||||
import './login/login';
|
||||
import './login/recover-password';
|
||||
import './login/reset-password';
|
||||
import './module-card';
|
||||
import './module-main';
|
||||
import './side-menu/side-menu';
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<div class="box">
|
||||
<img src="./logo.svg"/>
|
||||
<form name="form">
|
||||
<ui-view></ui-view>
|
||||
</form>
|
||||
</div>
|
|
@ -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
|
||||
});
|
|
@ -2,3 +2,7 @@ User: User
|
|||
Password: Password
|
||||
Do not close session: Do not close session
|
||||
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: $%&.)
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
User: Usuario
|
||||
Password: Contraseña
|
||||
Email: Correo electrónico
|
||||
Do not close session: No cerrar sesión
|
||||
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: $%&.)
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
<div class="box">
|
||||
<img src="./logo.svg"/>
|
||||
<form name="form" ng-submit="$ctrl.submit()">
|
||||
<vn-textfield
|
||||
label="User"
|
||||
ng-model="$ctrl.user"
|
||||
vn-id="userField"
|
||||
vn-focus>
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="Password"
|
||||
ng-model="$ctrl.password"
|
||||
type="password">
|
||||
</vn-textfield>
|
||||
<vn-check
|
||||
label="Do not close session"
|
||||
ng-model="$ctrl.remember"
|
||||
name="remember">
|
||||
</vn-check>
|
||||
<div class="footer">
|
||||
<vn-submit label="Enter"></vn-submit>
|
||||
<div class="spinner-wrapper">
|
||||
<vn-spinner enable="$ctrl.loading"></vn-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<vn-textfield
|
||||
label="User"
|
||||
ng-model="$ctrl.user"
|
||||
vn-id="userField"
|
||||
vn-focus>
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="Password"
|
||||
ng-model="$ctrl.password"
|
||||
type="password">
|
||||
</vn-textfield>
|
||||
<vn-check
|
||||
label="Do not close session"
|
||||
ng-model="$ctrl.remember"
|
||||
name="remember">
|
||||
</vn-check>
|
||||
<div class="footer">
|
||||
<vn-submit label="Enter" ng-click="$ctrl.submit()"></vn-submit>
|
||||
<div class="spinner-wrapper">
|
||||
<vn-spinner enable="$ctrl.loading"></vn-spinner>
|
||||
</div>
|
||||
<div class="vn-pt-lg">
|
||||
<a ui-sref="recoverPassword" translate>
|
||||
I do not remember my password
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
});
|
|
@ -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>
|
|
@ -0,0 +1,48 @@
|
|||
import ngModule from '../../module';
|
||||
import './style.scss';
|
||||
|
||||
alexm marked this conversation as resolved
|
||||
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
|
||||
});
|
|
@ -1,6 +1,31 @@
|
|||
@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;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -39,28 +64,17 @@ vn-login {
|
|||
white-space: inherit;
|
||||
}
|
||||
}
|
||||
& > .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h5{
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.text-secondary{
|
||||
text-align: center;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
|
@ -71,4 +85,8 @@ vn-login {
|
|||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ function $exceptionHandler(vnApp, $window, $state, $injector) {
|
|||
|
||||
switch (exception.status) {
|
||||
case 401:
|
||||
if ($state.current.name != 'login') {
|
||||
if (!$state.current.name.includes('login')) {
|
||||
messageT = 'Session has expired';
|
||||
let params = {continue: $window.location.hash};
|
||||
$state.go('login', params);
|
||||
|
|
|
@ -9,9 +9,17 @@ function config($stateProvider, $urlRouterProvider) {
|
|||
.state('login', {
|
||||
url: '/login?continue',
|
||||
description: 'Login',
|
||||
views: {
|
||||
login: {template: '<vn-login></vn-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', {
|
||||
url: '/',
|
||||
|
|
|
@ -131,12 +131,15 @@
|
|||
"Fichadas impares": "Odd signs",
|
||||
"Descanso diario 9h.": "Daily rest 9h.",
|
||||
"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",
|
||||
"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",
|
||||
"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 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}}})",
|
||||
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production",
|
||||
"Receipt's bank was not found": "Receipt's bank was not found",
|
||||
|
|
|
@ -245,6 +245,8 @@
|
|||
"Already has this status": "Ya tiene este estado",
|
||||
"There aren't records for this week": "No existen registros para esta semana",
|
||||
"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",
|
||||
"This receipt was not compensated": "Este recibo no ha sido compensado",
|
||||
"Client's email was not found": "No se encontró el email del cliente"
|
||||
|
|
|
@ -30,5 +30,13 @@
|
|||
"type": "number",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2,6 +2,11 @@ import ngModule from '../module';
|
|||
import Section from 'salix/components/section';
|
||||
|
||||
export default class Controller extends Section {
|
||||
$onInit() {
|
||||
if (this.$params.emailConfirmed)
|
||||
this.vnApp.showSuccess(this.$t('Email verified successfully!'));
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.$.watcher.submit()
|
||||
.then(() => this.card.reload());
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Email verified successfully!: Correo verificado correctamente!
|
|
@ -74,7 +74,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"url": "/basic-data",
|
||||
"url": "/basic-data?emailConfirmed",
|
||||
"state": "account.card.basicData",
|
||||
"component": "vn-user-basic-data",
|
||||
"description": "Basic data",
|
||||
|
|
|
@ -8,10 +8,12 @@ module.exports = {
|
|||
this.transporter = nodemailer.createTransport(config.smtp);
|
||||
},
|
||||
|
||||
send(options) {
|
||||
async send(options) {
|
||||
options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const notProductionError = {message: 'This not production, this email not sended'};
|
||||
alexm
commented
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)
|
||||
return Promise.resolve(true);
|
||||
|
||||
|
@ -24,29 +26,35 @@ module.exports = {
|
|||
|
||||
throw err;
|
||||
}).finally(async() => {
|
||||
const attachments = [];
|
||||
if (options.attachments) {
|
||||
for (let attachment of options.attachments) {
|
||||
const fileName = attachment.filename;
|
||||
const filePath = attachment.path;
|
||||
if (fileName.includes('.png')) continue;
|
||||
|
||||
if (fileName || filePath)
|
||||
attachments.push(filePath ? filePath : fileName);
|
||||
}
|
||||
}
|
||||
|
||||
const fileNames = attachments.join(',\n');
|
||||
await db.rawSql(`
|
||||
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
|
||||
VALUES (?, ?, 1, ?, ?, ?, ?)`, [
|
||||
options.to,
|
||||
options.replyTo,
|
||||
options.subject,
|
||||
options.text || options.html,
|
||||
fileNames,
|
||||
error && error.message || 'Sent'
|
||||
]);
|
||||
await this.mailLog(options, error);
|
||||
});
|
||||
},
|
||||
|
||||
async mailLog(options, error) {
|
||||
const attachments = [];
|
||||
if (options.attachments) {
|
||||
for (let attachment of options.attachments) {
|
||||
const fileName = attachment.filename;
|
||||
const filePath = attachment.path;
|
||||
if (fileName.includes('.png')) continue;
|
||||
|
||||
if (fileName || filePath)
|
||||
attachments.push(filePath ? filePath : fileName);
|
||||
}
|
||||
}
|
||||
|
||||
const fileNames = attachments.join(',\n');
|
||||
|
||||
await db.rawSql(`
|
||||
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
|
||||
VALUES (?, ?, 1, ?, ?, ?, ?)`, [
|
||||
options.to,
|
||||
options.replyTo,
|
||||
options.subject,
|
||||
options.text || options.html,
|
||||
fileNames,
|
||||
error && error.message || 'Sent'
|
||||
]);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.external-link {
|
||||
border: 2px dashed #8dba25;
|
||||
border-radius: 3px;
|
||||
text-align: center
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.external-link {
|
||||
border: 2px dashed #8dba25;
|
||||
border-radius: 3px;
|
||||
text-align: center
|
||||
}
|
|
@ -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.
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Perque gastes axios y no el servei $http?