diff --git a/back/methods/account/recover-password.js b/back/methods/account/recover-password.js
new file mode 100644
index 000000000..ddea76829
--- /dev/null
+++ b/back/methods/account/recover-password.js
@@ -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;
+ }
+ };
+};
diff --git a/back/methods/account/specs/set-password.spec.js b/back/methods/account/specs/set-password.spec.js
index c76fd52b8..fe71873de 100644
--- a/back/methods/account/specs/set-password.spec.js
+++ b/back/methods/account/specs/set-password.spec.js
@@ -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');
diff --git a/back/models/account.js b/back/models/account.js
index f74052b5c..c2502380a 100644
--- a/back/models/account.js
+++ b/back/models/account.js
@@ -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(
diff --git a/back/models/account.json b/back/models/account.json
index d0c17e70f..5e35c711a 100644
--- a/back/models/account.json
+++ b/back/models/account.json
@@ -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",
diff --git a/back/models/specs/account.spec.js b/back/models/specs/account.spec.js
index c52bc4378..f31c81b75 100644
--- a/back/models/specs/account.spec.js
+++ b/back/models/specs/account.spec.js
@@ -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();
});
diff --git a/back/models/specs/user.spec.js b/back/models/specs/user.spec.js
new file mode 100644
index 000000000..124afdc0c
--- /dev/null
+++ b/back/models/specs/user.spec.js
@@ -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();
+ });
+});
diff --git a/back/models/user.js b/back/models/user.js
new file mode 100644
index 000000000..b24d702b3
--- /dev/null
+++ b/back/models/user.js
@@ -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();
+ });
+};
diff --git a/db/changes/10502-november/00-aclUserPassword.sql b/db/changes/10502-november/00-aclUserPassword.sql
new file mode 100644
index 000000000..b92b54c28
--- /dev/null
+++ b/db/changes/10502-november/00-aclUserPassword.sql
@@ -0,0 +1,2 @@
+DELETE FROM `salix`.`ACL`
+ WHERE model = 'UserPassword';
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index 06fabe3e6..f550e3a9d 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -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"]',
diff --git a/e2e/paths/01-salix/04_recoverPassword.spec.js b/e2e/paths/01-salix/04_recoverPassword.spec.js
new file mode 100644
index 000000000..80ef32cb5
--- /dev/null
+++ b/e2e/paths/01-salix/04_recoverPassword.spec.js
@@ -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!');
+ });
+});
diff --git a/e2e/paths/05-ticket/20_future.spec.js b/e2e/paths/05-ticket/20_future.spec.js
index 4fee9523b..6db2bf4f0 100644
--- a/e2e/paths/05-ticket/20_future.spec.js
+++ b/e2e/paths/05-ticket/20_future.spec.js
@@ -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!');
});
-
});
diff --git a/front/core/lib/component.js b/front/core/lib/component.js
index f17db68a2..5695d9449 100644
--- a/front/core/lib/component.js
+++ b/front/core/lib/component.js
@@ -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
diff --git a/front/core/services/auth.js b/front/core/services/auth.js
index a1dcfa395..04520cd0b 100644
--- a/front/core/services/auth.js
+++ b/front/core/services/auth.js
@@ -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)
diff --git a/front/salix/components/app/app.html b/front/salix/components/app/app.html
index d32c9f68b..f14fab2dd 100644
--- a/front/salix/components/app/app.html
+++ b/front/salix/components/app/app.html
@@ -1,9 +1,8 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {{ $t(`click`) }} + {{ $t('subject') }} + +
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {{ $t('Click on the following link to change your password.') }} + {{ $t('subject') }} + +
+
+
+
+
+
+
+
+ |
+