#6427 - SMS Recover Password #2037

Open
jsegarra wants to merge 72 commits from 6427_sms_resetPassword into dev
33 changed files with 421 additions and 111 deletions

View File

@ -1,41 +1,16 @@
const got = require('got'); const got = require('got');
const UserError = require('vn-loopback/util/user-error');
const isProduction = require('vn-loopback/server/boot/isProduction'); const isProduction = require('vn-loopback/server/boot/isProduction');
const {models} = require('vn-loopback/server/server');
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('send', { Self.send = async(senderFk, destination, message, options) => {
description: 'Sends SMS to a destination phone', const smsConfig = await models.SmsConfig.findOne();
accessType: 'WRITE',
accepts: [
{
arg: 'destination',
type: 'string',
required: true,
},
{
arg: 'message',
type: 'string',
required: true,
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/send`,
verb: 'POST'
}
});
Self.send = async(ctx, destination, message) => { const [{prefix: spainPrefix}] = await Self.rawSql(
const userId = ctx.req.accessToken.userId; 'SELECT prefix FROM pbx.prefix WHERE country = ?', ['es'], options
const smsConfig = await Self.app.models.SmsConfig.findOne(); );
if (destination.length == 9)
if (destination.length == 9) {
const spainPrefix = '0034';
destination = spainPrefix + destination; destination = spainPrefix + destination;
}
const params = { const params = {
api_key: smsConfig.apiKey, api_key: smsConfig.apiKey,
@ -51,25 +26,26 @@ module.exports = Self => {
if (!isProduction(false)) if (!isProduction(false))
response = {result: [{status: 'ok'}]}; response = {result: [{status: 'ok'}]};
else { else {
const jsonTest = { const body = {
json: params json: params
}; };
response = await got.post(smsConfig.uri, jsonTest).json(); response = await got.post(smsConfig.uri, body).json();
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
if (!options?.insert) return;
const [result] = response.result; const [result] = response.result;
const error = result.error_id; const error = result.error_id;
if (senderFk) senderFk = senderFk.req.accessToken.userId;
const newSms = { const newSms = {
senderFk: userId, senderFk,
destination: destination, destination: destination,
message: message, message: message,
status: error status: error
}; };
const sms = await Self.create(newSms); const sms = await Self.create(newSms);
if (error) if (error)

View File

@ -3,8 +3,15 @@ const app = require('vn-loopback/server/server');
describe('sms send()', () => { describe('sms send()', () => {
it('should not return status error', async() => { it('should not return status error', async() => {
const ctx = {req: {accessToken: {userId: 1}}}; const ctx = {req: {accessToken: {userId: 1}}};
const result = await app.models.Sms.send(ctx, '123456789', 'My SMS Body'); const result = await app.models.Sms.send(ctx, '123456789', 'My SMS Body', {insert: true});
expect(result.status).toBeUndefined(); expect(result.status).toBeUndefined();
}); });
it('should not insert', async() => {
const ctx = {req: {accessToken: {userId: 1}}};
const result = await app.models.Sms.send(ctx, '123456789', 'My SMS Body', {insert: false});
expect(result).toBeUndefined();
});
}); });

View File

@ -0,0 +1,67 @@
const UserError = require('vn-loopback/util/user-error');
const isProduction = require('vn-loopback/server/boot/isProduction');
const authCode = require('../../models/authCode');
module.exports = Self => {
Self.remoteMethod('recoverPasswordSMS', {
description: 'Send SMS to the user',
accepts: [
{
arg: 'user',
type: 'string',
description: 'The recoveryPhone user\'s',
required: true
},
{
arg: 'verificationCode',
type: 'string',
description: 'Code tovalidate operation'
}
],
returns: {
type: 'Object',
root: true
},
http: {
path: `/recoverPasswordSMS`,
verb: 'POST'
}
});
Self.recoverPasswordSMS = async function(user, verificationCode, options) {
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const usesEmail = user.indexOf('@') !== -1;
const filter = usesEmail ? {email: user} : {name: user};
const account = await models.VnUser.findOne({
fields: ['id', 'name', 'recoveryPhone'],
where: filter
});
if (!account && !verificationCode) return;
user = account;
if (verificationCode) {
if (!account)
throw new UserError('Invalid or expired verification code');
await Self.validateCode(user.name, verificationCode);
return {
token: await user.accessTokens.create({})
};
}
const code = await authCode(user, myOptions);
if (!isProduction()) {
try {
await Self.app.models.Sms.send(null, +user.recoveryPhone, code, {insert: false});
} catch (e) {
throw new UserError(`We weren't able to send this SMS`);
}
}
return {code: true};
};
};

View File

@ -1,5 +1,6 @@
const ForbiddenError = require('vn-loopback/util/forbiddenError'); const ForbiddenError = require('vn-loopback/util/forbiddenError');
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const authCode = require('../../models/authCode');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('signIn', { Self.remoteMethodCtx('signIn', {
@ -65,18 +66,7 @@ module.exports = Self => {
Self.sendTwoFactor = async(ctx, vnUser, myOptions) => { Self.sendTwoFactor = async(ctx, vnUser, myOptions) => {
if (vnUser.twoFactor === 'email') { if (vnUser.twoFactor === 'email') {
const $ = Self.app.models; const code = await authCode(vnUser, myOptions);
const min = 100000;
const max = 999999;
const code = String(Math.floor(Math.random() * (max - min + 1)) + min);
const maxTTL = ((60 * 1000) * 5); // 5 min
await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
userFk: vnUser.id,
code: code,
expires: Date.vnNow() + maxTTL
}, myOptions);
const headers = ctx.req.headers; const headers = ctx.req.headers;
const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, ''); const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, ''); const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, '');

View File

@ -20,6 +20,10 @@ module.exports = Self => {
arg: 'email', arg: 'email',
type: 'string', type: 'string',
description: 'The user email' description: 'The user email'
}, {
arg: 'recoveryPhone',
type: 'string',
description: 'The user email'
}, { }, {
arg: 'lang', arg: 'lang',
type: 'string', type: 'string',
@ -36,8 +40,8 @@ module.exports = Self => {
} }
}); });
Self.updateUser = async(ctx, id, name, nickname, email, lang, twoFactor) => { Self.updateUser = async(ctx, id, name, nickname, email, recoveryPhone, lang, twoFactor) => {
await Self.userSecurity(ctx, id); await Self.userSecurity(ctx, id);
await Self.upsertWithWhere({id}, {name, nickname, email, lang, twoFactor}); await Self.upsertWithWhere({id}, {name, nickname, email, recoveryPhone, lang, twoFactor});
}; };
}; };

20
back/models/authCode.js Normal file
View File

@ -0,0 +1,20 @@
const models = require('vn-loopback/server/server').models;
const maxTTL = ((60 * 1000) * 5); // 5 min
module.exports = authCode = async(vnUser, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const min = 100000;
const max = 999999;
const code = String(Math.floor(Math.random() * (max - min + 1)) + min);
await models.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
userFk: vnUser.id,
code,
expires: Date.vnNow() + maxTTL
}, myOptions);
return code;
};

View File

@ -4,7 +4,7 @@
"base": "VnModel", "base": "VnModel",
"options": { "options": {
"mysql": { "mysql": {
"table": "smsConfig" "table": "vn.smsConfig"
} }
}, },
"properties": { "properties": {

View File

@ -1,4 +1,3 @@
module.exports = Self => { module.exports = Self => {
// Methods
require('../methods/sms/send')(Self); require('../methods/sms/send')(Self);
}; };

View File

@ -4,7 +4,7 @@
"base": "VnModel", "base": "VnModel",
"options": { "options": {
"mysql": { "mysql": {
"table": "sms" "table": "vn.sms"
} }
}, },
"properties": { "properties": {

View File

@ -1,7 +1,6 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('VnUser recoverPassword', () => {
describe('VnUser recoverPassword()', () => {
const userId = 1107; const userId = 1107;
const activeCtx = { const activeCtx = {
@ -19,14 +18,29 @@ describe('VnUser recoverPassword()', () => {
}); });
}); });
it('should send email with token', async() => { describe('By email', () => {
const userId = 1107; it('should send email with token', async() => {
const user = await models.VnUser.findById(userId); const userId = 1107;
const user = await models.VnUser.findById(userId);
await models.VnUser.recoverPassword(user.email); await models.VnUser.recoverPassword(user.email);
const result = await models.AccessToken.findOne({where: {userId: userId}}); const result = await models.AccessToken.findOne({where: {userId: userId}});
expect(result).toBeDefined(); expect(result).toBeDefined();
});
});
describe('By SMS()', () => {
it('should send sms with token', async() => {
const userId = 1107;
const user = await models.VnUser.findById(userId);
await models.VnUser.recoverPasswordSMS(user.email);
const result = await models.AuthCode.findOne({where: {userId: userId}});
expect(result).toBeDefined();
});
}); });
}); });

View File

@ -34,7 +34,7 @@ describe('loopback model VnUser', () => {
await models.VnUser.userSecurity(ctx, employeeId); await models.VnUser.userSecurity(ctx, employeeId);
}); });
it('should throw an error if you have medium privileges and the users email is verified', async() => { it('should throw an error when update emailVerified field if you have medium privileges and the users email is verified', async() => {
const tx = await models.VnUser.beginTransaction({}); const tx = await models.VnUser.beginTransaction({});
const ctx = {options: {accessToken: {userId: hrId}}}; const ctx = {options: {accessToken: {userId: hrId}}};
try { try {
@ -50,5 +50,32 @@ describe('loopback model VnUser', () => {
expect(error).toEqual(new ForbiddenError()); expect(error).toEqual(new ForbiddenError());
} }
}); });
it('should throw an error when update recoveryPhone if you have medium privileges and the users email is verified', async() => {
const tx = await models.VnUser.beginTransaction({});
const ctx = {options: {accessToken: {userId: hrId}}};
try {
const options = {transaction: tx};
const userToUpdate = await models.VnUser.findById(1, null, options);
userToUpdate.updateAttribute('recoveryPhone', 123456789, options);
await models.VnUser.userSecurity(ctx, employeeId, options);
await tx.rollback();
} catch (error) {
await tx.rollback();
expect(error).toEqual(new ForbiddenError());
}
});
it('should update recoveryPhone if you are the same user', async() => {
const ctx = {options: {accessToken: {userId: employeeId}}};
const newRecoveryPhone = 123456789;
const userToUpdate = await models.VnUser.findById(1, null);
userToUpdate.updateAttribute('recoveryPhone', newRecoveryPhone);
await models.VnUser.userSecurity(ctx, employeeId);
});
}); });
}); });

View File

@ -10,6 +10,7 @@ module.exports = function(Self) {
require('../methods/vn-user/sign-in')(Self); require('../methods/vn-user/sign-in')(Self);
require('../methods/vn-user/acl')(Self); require('../methods/vn-user/acl')(Self);
require('../methods/vn-user/recover-password')(Self); require('../methods/vn-user/recover-password')(Self);
require('../methods/vn-user/recover-passwordSMS')(Self);
require('../methods/vn-user/privileges')(Self); require('../methods/vn-user/privileges')(Self);
require('../methods/vn-user/validate-auth')(Self); require('../methods/vn-user/validate-auth')(Self);
require('../methods/vn-user/renew-token')(Self); require('../methods/vn-user/renew-token')(Self);

View File

@ -61,6 +61,9 @@
}, },
"twoFactor": { "twoFactor": {
"type": "string" "type": "string"
},
"recoveryPhone": {
"type": "string"
} }
}, },
"relations": { "relations": {
@ -106,6 +109,13 @@
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
}, },
{
"property": "recoverPasswordSMS",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{ {
"property": "validateAuth", "property": "validateAuth",
"accessType": "EXECUTE", "accessType": "EXECUTE",
@ -166,6 +176,7 @@
"realm", "realm",
"email", "email",
"emailVerified", "emailVerified",
"recoveryPhone",
"twoFactor" "twoFactor"
] ]
} }

View File

@ -311,4 +311,10 @@ INSERT INTO mysql.roles_mapping (`User`, `Host`, `Role`, `Admin_option`)
SELECT SUBSTR(`User`, @prefixLen + 1), `Host`, `Role`, `Admin_option` SELECT SUBSTR(`User`, @prefixLen + 1), `Host`, `Role`, `Admin_option`
FROM mysql.roles_mapping FROM mysql.roles_mapping
WHERE `User` LIKE @prefixedLike AND `Host` = @genRoleHost; WHERE `User` LIKE @prefixedLike AND `Host` = @genRoleHost;
-- Actualiza los valores de la nueva columna con los valores correspondientes de la tabla userInfo
UPDATE `account`.`user` as `user`
JOIN vn.client `client` ON `user`.id = `client`.id
SET `user`.recoveryPhone = `client`.phone;
FLUSH PRIVILEGES; FLUSH PRIVILEGES;

View File

@ -3190,6 +3190,10 @@ INSERT INTO `vn`.`cmr` (id,truckPlate,observations,senderInstruccions,paymentIns
(2,'123456N','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet',69,3,4,2,'Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet'), (2,'123456N','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet',69,3,4,2,'Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet'),
(3,'123456B','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet',567,5,6,69,'Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet'); (3,'123456B','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet',567,5,6,69,'Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet','Lorem ipsum dolor sit amet');
UPDATE `vn`.`client`
SET phone= 432978106
WHERE id=9;
UPDATE vn.department UPDATE vn.department
SET workerFk = null; SET workerFk = null;

View File

@ -0,0 +1,4 @@
#6427 -- Place your SQL code here
ALTER TABLE account.`user` ADD recoveryPhone varchar(20) NULL;
-- ALTER TABLE vn.`sms` MODIFY COLUMN senderFk int(10) unsigned NULL;

View File

@ -0,0 +1,5 @@
-- Actualiza los valores de la nueva columna con los valores correspondientes de la tabla userInfo
UPDATE `account`.`user` as `user`
JOIN vn.client `client` ON `user`.id = `client`.id
SET `user`.recoveryPhone = `client`.phone;

View File

@ -33,7 +33,10 @@ export default {
recoverPassword: { recoverPassword: {
recoverPasswordButton: 'vn-login a[ui-sref="recover-password"]', recoverPasswordButton: 'vn-login a[ui-sref="recover-password"]',
email: 'vn-recover-password vn-textfield[ng-model="$ctrl.user"]', email: 'vn-recover-password vn-textfield[ng-model="$ctrl.user"]',
code: 'vn-recover-password vn-textfield[ng-model="$ctrl.verificationCode"]',
sendEmailButton: 'vn-recover-password vn-submit', sendEmailButton: 'vn-recover-password vn-submit',
smsOption: 'vn-recover-password vn-radio[val="sms"]',
emailOption: 'vn-recover-password vn-radio[val="email"]',
}, },
accountIndex: { accountIndex: {
addAccount: 'vn-user-index button vn-icon[icon="add"]', addAccount: 'vn-user-index button vn-icon[icon="add"]',

View File

@ -8,6 +8,10 @@ describe('RecoverPassword path', async() => {
beforeAll(async() => { beforeAll(async() => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
});
beforeEach(async() => {
await page.waitForState('login');
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton); await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
await page.waitForState('recover-password'); await page.waitForState('recover-password');
@ -17,36 +21,78 @@ describe('RecoverPassword path', async() => {
await browser.close(); await browser.close();
}); });
it('should not throw error if not exist user', async() => { it('should not throw error if not exist user when select email option', async() => {
await page.write(selectors.recoverPassword.email, 'fakeEmail@mydomain.com'); await page.write(selectors.recoverPassword.email, 'fakeEmail@mydomain.com');
await page.waitToClick(selectors.recoverPassword.emailOption);
await page.waitToClick(selectors.recoverPassword.sendEmailButton); await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const httpDataResponse = await page.waitForResponse(response => {
return response.status() === 204 && response.url().includes(`VnUsers/recoverPassword`);
});
const code = await httpDataResponse.ok();
expect(code).toBeTrue();
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('Notification sent!'); expect(message.text).toContain('Notification sent!');
}); });
it('should send email using email', async() => { it('should send email using email', async() => {
await page.waitForState('login');
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
await page.write(selectors.recoverPassword.email, 'BruceWayne@mydomain.com'); await page.write(selectors.recoverPassword.email, 'BruceWayne@mydomain.com');
await page.waitToClick(selectors.recoverPassword.emailOption);
await page.waitToClick(selectors.recoverPassword.sendEmailButton); await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
await page.waitForState('login');
expect(message.text).toContain('Notification sent!'); expect(message.text).toContain('Notification sent!');
}); });
it('should send email using username', async() => { it('should send email using username', async() => {
await page.waitForState('login');
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
await page.write(selectors.recoverPassword.email, 'BruceWayne'); await page.write(selectors.recoverPassword.email, 'BruceWayne');
await page.waitToClick(selectors.recoverPassword.emailOption);
await page.waitToClick(selectors.recoverPassword.sendEmailButton); await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
await page.waitForState('login');
expect(message.text).toContain('Notification sent!'); expect(message.text).toContain('Notification sent!');
}); });
it('should send sms using username', async() => {
await page.setRequestInterception(true);
await page.write(selectors.recoverPassword.email, 'BruceWayne');
await page.waitToClick(selectors.recoverPassword.smsOption);
page.on('request', request => {
if (request.url().includes('recoverPasswordSMS')) {
const body = JSON.parse(request.postData());
const isVerificationCode = Object.keys(body).includes('verificationCode');
if (!isVerificationCode) {
request.respond({
content: 'application/json',
headers: {'Access-Control-Allow-Origin': '*'},
body: JSON.stringify({code: '123456'})
});
} else {
request.respond({
content: 'application/json',
headers: {'Access-Control-Allow-Origin': '*'},
body: JSON.stringify({token: {
'id': 'A7al0KNofU7RFL5XPNubKsVjOAj80eoydXhm9i6rF4gj5kom6nEx4BG2bubzLocm',
'ttl': 1209600,
'created': '2024-05-30T10:43:36.014Z',
'userId': 9
}})
});
}
} else
request.continue();
});
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const httpDataResponse = await page.waitForResponse(response => {
return response.status() === 200 && response.url().includes(`VnUsers/recoverPasswordSMS`);
});
const {code} = await httpDataResponse.json();
await page.write(selectors.recoverPassword.code, code);
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
await page.waitForState('reset-password');
expect(await page.getState()).toContain('reset-password');
});
}); });

View File

@ -1,16 +1,49 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Recover password</h5> <h5 class="vn-mb-md vn-mt-lg" translate>Recover password</h5>
<vn-textfield <vn-textfield
disabled="$ctrl.code"
label="User or recovery email" label="User or recovery email"
ng-model="$ctrl.user" ng-model="$ctrl.user"
vn-focus> vn-focus
>
</vn-textfield> </vn-textfield>
<div <vn-textfield
class="text-secondary" ng-if="$ctrl.code"
translate> label="Verification code"
We will sent you an email to recover your password ng-model="$ctrl.verificationCode"
vn-name="verificationCode"
autocomplete="false"
class="vn-mt-md">
</vn-textfield>
<vn-one>
<vn-vertical class="vn-mb-sm">
<vn-radio
disabled="$ctrl.code"
label="Teléfono móvil"
val="sms"
ng-model="$ctrl.method">
</vn-radio>
<vn-radio
disabled="$ctrl.code"
label="E-mail"
val="email"
ng-model="$ctrl.method">
</vn-radio>
</vn-vertical>
</vn-one>
<div class="text-secondary" ng-if="$ctrl.method && $ctrl.user">
<span ng-if="$ctrl.method ==='sms'" translate>
We will sent you a sms to recover your password
</span>
<span ng-if="$ctrl.method ==='email'" translate>
We will sent you an email to recover your password
</span>
</div> </div>
<div class="footer"> <div class="footer">
<vn-submit label="Recover password" ng-click="$ctrl.submit()"></vn-submit> <vn-submit
disabled="!$ctrl.user || !$ctrl.method || ($ctrl.code&&!$ctrl.verificationCode)"
label="Recover password"
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>

View File

@ -1,14 +1,16 @@
import UserError from '../../../core/lib/user-error';
import ngModule from '../../module'; import ngModule from '../../module';
export default class Controller { export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state) { constructor($scope, $element, $http, vnApp, $translate, $state, $location) {
Object.assign(this, { Object.assign(this, {
$scope, $scope,
$element, $element,
$http, $http,
vnApp, vnApp,
$translate, $translate,
$state $state,
$location
}); });
} }
@ -16,19 +18,49 @@ export default class Controller {
this.vnApp.showSuccess(this.$translate.instant('Notification sent!')); this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
this.$state.go('login'); this.$state.go('login');
} }
goToChangePassword({token}) {
if (!token)
this.vnApp.showError(this.$translate.instant('Invalid login'));
else
this.$location.path('/reset-password').search('access_token', token.id);
}
handleCode(code) {
this.code = true;
this.$state.params.verificationCode = code;
}
methodsAvailables() {
return {
'email': {
url: 'VnUsers/recoverPassword',
data: {user: this.user},
cb: data => {
data === '' && this.goToLogin();
}
},
'sms': {
url: 'VnUsers/recoverPasswordSMS',
data: {user: this.user, verificationCode: this.verificationCode},
cb: data => {
if (this.method && this.code) {
data.token && this.goToChangePassword(data);
if (!data.token) throw new UserError(`Credentials not valid`);
} else
this.handleCode(data.code);
}
},
};
}
submit() { submit() {
const params = { if (!this.user || (this.sms) || (this.code && !this.code))
user: this.user throw new UserError(`Credentials not valid`);
}; const method = this.methodsAvailables()[this.method];
this.$http.post(method.url, method.data)
this.$http.post('VnUsers/recoverPassword', params) .then(({data}) => method.cb(data));
.then(() => {
this.goToLogin();
});
} }
} }
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state']; Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state', '$location'];
ngModule.vnComponent('vnRecoverPassword', { ngModule.vnComponent('vnRecoverPassword', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -1,4 +1,9 @@
Recover password: Recuperar 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 We will sent you an email to recover your password: Te enviaremos un correo para restablecer tu contraseña
We will sent you a sms to recover your password: Te enviaremos un sms para restablecer tu contraseña
Notification sent!: ¡Notificación enviada! Notification sent!: ¡Notificación enviada!
User or recovery email: Usuario o correo de recuperación User or recovery email: Usuario
User's phone: Móvil del usuario
User's id: Id del usuario
Credentials not valid: Credenciales no válidas
E-mail: Correo electrónico

View File

@ -28,7 +28,7 @@ export default class Controller {
throw new UserError(`Passwords don't match`); throw new UserError(`Passwords don't match`);
const headers = { const headers = {
Authorization: this.$location.$$search.access_token Authorization: this.$location.$$search.access_token ?? this.$state.params.access_token
}; };
const newPassword = this.newPassword; const newPassword = this.newPassword;

View File

@ -1,4 +1,4 @@
Reset password: Restrablecer contraseña Reset password: Restablecer contraseña
New password: Nueva contraseña New password: Nueva contraseña
Repeat password: Repetir contraseña Repeat password: Repetir contraseña
Password changed!: ¡Contraseña cambiada! Password changed!: ¡Contraseña cambiada!

View File

@ -229,6 +229,7 @@
"InvoiceIn is already booked": "InvoiceIn is already booked", "InvoiceIn is already booked": "InvoiceIn is already booked",
"This workCenter is already assigned to this agency": "This workCenter is already assigned to this agency", "This workCenter is already assigned to this agency": "This workCenter is already assigned to this agency",
"You can only have one PDA": "You can only have one PDA", "You can only have one PDA": "You can only have one PDA",
"Credentials not valid": "Credentials not valid",
"Incoterms and Customs agent are required for a non UEE member": "Incoterms and Customs agent are required for a non UEE member", "Incoterms and Customs agent are required for a non UEE member": "Incoterms and Customs agent are required for a non UEE member",
"The invoices have been created but the PDFs could not be generated": "The invoices have been created but the PDFs could not be generated", "The invoices have been created but the PDFs could not be generated": "The invoices have been created but the PDFs could not be generated",
"It has been invoiced but the PDF of refund not be generated": "It has been invoiced but the PDF of refund not be generated", "It has been invoiced but the PDF of refund not be generated": "It has been invoiced but the PDF of refund not be generated",
@ -240,10 +241,6 @@
"There is already a tray with the same height": "There is already a tray with the same height", "There is already a tray with the same height": "There is already a tray with the same height",
"The height must be greater than 50cm": "The height must be greater than 50cm", "The height must be greater than 50cm": "The height must be greater than 50cm",
"The maximum height of the wagon is 200cm": "The maximum height of the wagon is 200cm", "The maximum height of the wagon is 200cm": "The maximum height of the wagon is 200cm",
"The quantity claimed cannot be greater than the quantity of the line": "The quantity claimed cannot be greater than the quantity of the line", "The quantity claimed cannot be greater than the quantity of the line": "The quantity claimed cannot be greater than the quantity of the line"
"There are tickets for this area, delete them first": "There are tickets for this area, delete them first", }
"ticketLostExpedition": "The ticket [{{ticketId}}]({{{ticketUrl}}}) has the following lost expedition:{{ expeditionId }}",
"null": "null",
"Invalid or expired verification code": "Invalid or expired verification code",
"Payment method is required": "Payment method is required"
}

View File

@ -350,7 +350,6 @@
"Cmr file does not exist": "El archivo del cmr no existe", "Cmr file does not exist": "El archivo del cmr no existe",
"You are not allowed to modify the alias": "No estás autorizado a modificar el alias", "You are not allowed to modify the alias": "No estás autorizado a modificar el alias",
"The address of the customer must have information about Incoterms and Customs Agent": "El consignatario del cliente debe tener informado Incoterms y Agente de aduanas", "The address of the customer must have information about Incoterms and Customs Agent": "El consignatario del cliente debe tener informado Incoterms y Agente de aduanas",
"No invoice series found for these parameters": "No se encontró una serie para estos parámetros",
"The line could not be marked": "La linea no puede ser marcada", "The line could not be marked": "La linea no puede ser marcada",
"Through this procedure, it is not possible to modify the password of users with verified email": "Mediante este procedimiento, no es posible modificar la contraseña de usuarios con correo verificado", "Through this procedure, it is not possible to modify the password of users with verified email": "Mediante este procedimiento, no es posible modificar la contraseña de usuarios con correo verificado",
"They're not your subordinate": "No es tu subordinado/a.", "They're not your subordinate": "No es tu subordinado/a.",
@ -364,6 +363,7 @@
"You can not use the same password": "No puedes usar la misma contraseña", "You can not use the same password": "No puedes usar la misma contraseña",
"This PDA is already assigned to another user": "Este PDA ya está asignado a otro usuario", "This PDA is already assigned to another user": "Este PDA ya está asignado a otro usuario",
"You can only have one PDA": "Solo puedes tener un PDA", "You can only have one PDA": "Solo puedes tener un PDA",
"Credentials not valid": "Credentials not valid",
"The invoices have been created but the PDFs could not be generated": "Se ha facturado pero no se ha podido generar el PDF", "The invoices have been created but the PDFs could not be generated": "Se ha facturado pero no se ha podido generar el PDF",
"It has been invoiced but the PDF of refund not be generated": "Se ha facturado pero no se ha podido generar el PDF del abono", "It has been invoiced but the PDF of refund not be generated": "Se ha facturado pero no se ha podido generar el PDF del abono",
"Payment method is required": "El método de pago es obligatorio", "Payment method is required": "El método de pago es obligatorio",
@ -382,10 +382,6 @@
"The entry does not have stickers": "La entrada no tiene etiquetas", "The entry does not have stickers": "La entrada no tiene etiquetas",
"This buyer has already made a reservation for this date": "Este comprador ya ha hecho una reserva para esta fecha", "This buyer has already made a reservation for this date": "Este comprador ya ha hecho una reserva para esta fecha",
"No valid travel thermograph found": "No se encontró un termógrafo válido", "No valid travel thermograph found": "No se encontró un termógrafo válido",
"The quantity claimed cannot be greater than the quantity of the line": "La cantidad reclamada no puede ser mayor que la cantidad de la línea", "The quantity claimed cannot be greater than the quantity of the line": "La cantidad reclamada no puede ser mayor que la cantidad de la línea"
"type cannot be blank": "Se debe rellenar el tipo", }
"There are tickets for this area, delete them first": "Hay tickets para esta sección, borralos primero",
"There is no company associated with that warehouse": "No hay ninguna empresa asociada a ese almacén",
"ticketLostExpedition": "El ticket [{{ticketId}}]({{{ticketUrl}}}) tiene la siguiente expedición perdida:{{ expeditionId }}",
"The web user's email already exists": "El correo del usuario web ya existe"
}

View File

@ -1,6 +1,9 @@
{ {
"name": "Account", "name": "Account",
"base": "VnModel", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "account.account" "table": "account.account"

View File

@ -0,0 +1,57 @@
<mg-ajax path="VnUsers/{{patch.params.id}}/update-user" options="vnPatch"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.user"
form="form"
save="patch">
</vn-watcher>
<form
name="form"
ng-submit="$ctrl.onSubmit()"
class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-textfield
label="User"
ng-model="$ctrl.user.name"
rule="VnUser"
vn-focus>
</vn-textfield>
<vn-textfield
label="Nickname"
ng-model="$ctrl.user.nickname"
rule="VnUser">
</vn-textfield>
<vn-textfield
label="Personal email"
ng-model="$ctrl.user.email"
rule="VnUser">
</vn-textfield>
<vn-textfield
vn-one
label="Recovery phone"
ng-model="$ctrl.user.recoveryPhone"
disabled="$root.user.id !== $ctrl.user.id">
</vn-textfield>
<vn-autocomplete
label="Language"
ng-model="$ctrl.user.lang"
url="Languages"
value-field="code"
rule="VnUser">
</vn-autocomplete>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
<vn-button
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>

View File

@ -0,0 +1,3 @@
Email verified successfully!: Correo verificado correctamente!
Recovery phone: Teléfono de recuperación de cuenta

View File

@ -32,7 +32,7 @@ module.exports = Self => {
Self.sendSms = async(ctx, id, destination, message) => { Self.sendSms = async(ctx, id, destination, message) => {
const models = Self.app.models; const models = Self.app.models;
const sms = await models.Sms.send(ctx, destination, message); const sms = await models.Sms.send(ctx, destination, message, {insert: true});
await models.ClientSms.create({ await models.ClientSms.create({
clientFk: id, clientFk: id,

View File

@ -30,7 +30,7 @@ module.exports = Self => {
const allSms = []; const allSms = [];
for (let client of targetClients) { for (let client of targetClients) {
let sms = await Self.app.models.Sms.send(ctx, client, message); let sms = await Self.app.models.Sms.send(ctx, client, message, {insert: true});
allSms.push(sms); allSms.push(sms);
} }

View File

@ -48,7 +48,7 @@ module.exports = Self => {
CALL vn.sale_recalcComponent(null); CALL vn.sale_recalcComponent(null);
DROP TEMPORARY TABLE tmp.recalculateSales;`; DROP TEMPORARY TABLE tmp.recalculateSales;`;
const recalculation = await Self.rawSql(query, [salesIds], myOptions); const recalculation = await Self.rawSql(query, salesIds, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -32,7 +32,7 @@ module.exports = Self => {
Self.sendSms = async(ctx, id, destination, message) => { Self.sendSms = async(ctx, id, destination, message) => {
const models = Self.app.models; const models = Self.app.models;
const sms = await models.Sms.send(ctx, destination, message); const sms = await models.Sms.send(ctx, destination, message, {insert: true});
const {clientFk} = await models.Ticket.findById(id); const {clientFk} = await models.Ticket.findById(id);
await models.ClientSms.create({ await models.ClientSms.create({
clientFk, clientFk,