#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 UserError = require('vn-loopback/util/user-error');
const isProduction = require('vn-loopback/server/boot/isProduction');
const {models} = require('vn-loopback/server/server');
module.exports = Self => {
Self.remoteMethod('send', {
description: 'Sends SMS to a destination phone',
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(senderFk, destination, message, options) => {
const smsConfig = await models.SmsConfig.findOne();
Self.send = async(ctx, destination, message) => {
const userId = ctx.req.accessToken.userId;
const smsConfig = await Self.app.models.SmsConfig.findOne();
if (destination.length == 9) {
const spainPrefix = '0034';
const [{prefix: spainPrefix}] = await Self.rawSql(
'SELECT prefix FROM pbx.prefix WHERE country = ?', ['es'], options
);
if (destination.length == 9)
destination = spainPrefix + destination;
}
const params = {
api_key: smsConfig.apiKey,
@ -51,25 +26,26 @@ module.exports = Self => {
if (!isProduction(false))
response = {result: [{status: 'ok'}]};
else {
const jsonTest = {
const body = {
json: params
};
response = await got.post(smsConfig.uri, jsonTest).json();
response = await got.post(smsConfig.uri, body).json();
}
} catch (e) {
console.error(e);
}
if (!options?.insert) return;
const [result] = response.result;
const error = result.error_id;
if (senderFk) senderFk = senderFk.req.accessToken.userId;
const newSms = {
senderFk: userId,
senderFk,
destination: destination,
message: message,
status: error
};
const sms = await Self.create(newSms);
if (error)

View File

@ -3,8 +3,15 @@ const app = require('vn-loopback/server/server');
describe('sms send()', () => {
it('should not return status error', async() => {
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();
});
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'],
jsegarra marked this conversation as resolved Outdated
Outdated
Review

pq aqui es _opt pero en accepts es opt?
Usar otp

pq aqui es `_opt` pero en accepts es `opt`? Usar `otp`

Correcto, el nombre de la variable está obsoleto
Gracias
Lo cambio todo por code

Correcto, el nombre de la variable está obsoleto Gracias Lo cambio todo por code
Outdated
Review

pq aqui es _opt pero en accepts es opt?
Usar otp

pq aqui es `_opt` pero en accepts es `opt`? Usar `otp`
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({})
};
}
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Pondria directamente aqui el filtro

Pondria directamente aqui el filtro
Outdated
Review
const user = await Self.findOne({
            fields: ['id', 'phone', 'email', 'name'],
            where: {id, phone}
        });
``` const user = await Self.findOne({ fields: ['id', 'phone', 'email', 'name'], where: {id, phone} }); ```
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 UserError = require('vn-loopback/util/user-error');
const authCode = require('../../models/authCode');
module.exports = Self => {
Self.remoteMethodCtx('signIn', {
@ -65,18 +66,7 @@ module.exports = Self => {
Self.sendTwoFactor = async(ctx, vnUser, myOptions) => {
if (vnUser.twoFactor === 'email') {
const $ = Self.app.models;
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 code = await authCode(vnUser, myOptions);
const headers = ctx.req.headers;
const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, '');

View File

@ -20,6 +20,10 @@ module.exports = Self => {
arg: 'email',
type: 'string',
description: 'The user email'
}, {
arg: 'recoveryPhone',
type: 'string',
description: 'The user email'
}, {
arg: 'lang',
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.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",
"options": {
"mysql": {
"table": "smsConfig"
"table": "vn.smsConfig"
}
},
"properties": {

View File

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

View File

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

View File

@ -1,7 +1,6 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('VnUser recoverPassword()', () => {
describe('VnUser recoverPassword', () => {
const userId = 1107;
const activeCtx = {
@ -19,14 +18,29 @@ describe('VnUser recoverPassword()', () => {
});
});
it('should send email with token', async() => {
const userId = 1107;
const user = await models.VnUser.findById(userId);
describe('By email', () => {
it('should send email with token', async() => {
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);
});
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 ctx = {options: {accessToken: {userId: hrId}}};
try {
@ -50,5 +50,32 @@ describe('loopback model VnUser', () => {
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/acl')(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/validate-auth')(Self);
require('../methods/vn-user/renew-token')(Self);

View File

@ -61,6 +61,9 @@
},
"twoFactor": {
"type": "string"
},
"recoveryPhone": {
"type": "string"
}
},
"relations": {
@ -106,6 +109,13 @@
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "recoverPasswordSMS",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "validateAuth",
"accessType": "EXECUTE",
@ -166,6 +176,7 @@
"realm",
"email",
"emailVerified",
"recoveryPhone",
"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`
FROM mysql.roles_mapping
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;

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'),
(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
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: {
recoverPasswordButton: 'vn-login a[ui-sref="recover-password"]',
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',
smsOption: 'vn-recover-password vn-radio[val="sms"]',
emailOption: 'vn-recover-password vn-radio[val="email"]',
},
accountIndex: {
addAccount: 'vn-user-index button vn-icon[icon="add"]',

View File

@ -8,6 +8,10 @@ describe('RecoverPassword path', async() => {
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
});
beforeEach(async() => {
await page.waitForState('login');
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
await page.waitForState('recover-password');
@ -17,36 +21,78 @@ describe('RecoverPassword path', async() => {
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.waitToClick(selectors.recoverPassword.emailOption);
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();
expect(message.text).toContain('Notification sent!');
});
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.waitToClick(selectors.recoverPassword.emailOption);
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const message = await page.waitForSnackbar();
await page.waitForState('login');
expect(message.text).toContain('Notification sent!');
});
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.waitToClick(selectors.recoverPassword.emailOption);
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const message = await page.waitForSnackbar();
await page.waitForState('login');
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>
<vn-textfield
Outdated
Review

Igual pondría un radio button para que sepan que por defecto es email

Igual pondría un radio button para que sepan que por defecto es email

Si pones radio button tienes que añadir otra opción y gestionar el estado, entonces con el checkbox, te aseguras que es true o false, y por tanto muestra el otro método o no

Si pones radio button tienes que añadir otra opción y gestionar el estado, entonces con el checkbox, te aseguras que es true o false, y por tanto muestra el otro método o no
disabled="$ctrl.code"
label="User or recovery email"
ng-model="$ctrl.user"
vn-focus>
vn-focus
>
</vn-textfield>
<div
class="text-secondary"
translate>
We will sent you an email to recover your password
<vn-textfield
ng-if="$ctrl.code"
label="Verification code"
ng-model="$ctrl.verificationCode"
vn-name="verificationCode"
Outdated
Review

No entiendo el poner 2 vn-textfields con ifs.
Con poner `label="User, phone, or recovery email" sobraria.

Y tampoco se si deberia poder poner numeros de telefono para decir que son ellos (lo consultaria con Juan)

No entiendo el poner 2 vn-textfields con ifs. Con poner `label="User, phone, or recovery email" sobraria. Y tampoco se si deberia poder poner numeros de telefono para decir que son ellos (lo consultaria con Juan)

El teléfono se usa para validar la acción de recuperar la contraseña. Porque puede darse el caso que el usuario ponga su id y no le esté llegando el SMS porque en algún momento se equivocó de teléfono.

El teléfono se usa para validar la acción de recuperar la contraseña. Porque puede darse el caso que el usuario ponga su id y no le esté llegando el SMS porque en algún momento se equivocó de teléfono.
Outdated
Review

Entonces podría poner tu id, y mi numero de teléfono y te podría cambiar la contraseña?
Lo que se hacia con el correo es apartir del correo sacar el id del usuario. Supongo que con el telefono sera igual

Entonces podría poner tu id, y mi numero de teléfono y te podría cambiar la contraseña? Lo que se hacia con el correo es apartir del correo sacar el id del usuario. Supongo que con el telefono sera igual

No podrías, porque tu pones el id de usuario y teléfono, y si ambos valores no existen, no te envía SMS. En local puedes probar con el userId:9 que tiene el teléfono "432978106"

El teléfono de recuperación solo lo puede cambiar quien es propietario del registro, ya que tiene una validación del id del registro contra el id del usuario logeado

Pero vamos, que yo podría estar contaminado con el desarrollo, y a lo mejor tu consigues bordear la restricción. si es así, repórtamelo, por favor.

No podrías, porque tu pones el id de usuario y teléfono, y si ambos valores no existen, no te envía SMS. En local puedes probar con el userId:9 que tiene el teléfono "432978106" El teléfono de recuperación solo lo puede cambiar quien es propietario del registro, ya que tiene una validación del id del registro contra el id del usuario logeado Pero vamos, que yo podría estar contaminado con el desarrollo, y a lo mejor tu consigues bordear la restricción. si es así, repórtamelo, por favor.
autocomplete="false"
class="vn-mt-md">
</vn-textfield>
<vn-one>
<vn-vertical class="vn-mb-sm">
<vn-radio
disabled="$ctrl.code"
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Y aqui igual en vez de poner dos, pondria algo como. te enviaremos un mensaje por el tipo de envio elegido o algo asi

Y aqui igual en vez de poner dos, pondria algo como. te enviaremos un mensaje por el tipo de envio elegido o algo asi
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>
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Corregir tabulación

Corregir tabulación
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 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">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>

View File

@ -1,14 +1,16 @@
import UserError from '../../../core/lib/user-error';
import ngModule from '../../module';
export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state) {
constructor($scope, $element, $http, vnApp, $translate, $state, $location) {
Object.assign(this, {
$scope,
$element,
$http,
vnApp,
$translate,
$state
$state,
$location
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Esto cuando se usa??

Esto cuando se usa??
});
}
@ -16,19 +18,49 @@ export default class Controller {
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
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() {
const params = {
user: this.user
};
this.$http.post('VnUsers/recoverPassword', params)
.then(() => {
this.goToLogin();
});
if (!this.user || (this.sms) || (this.code && !this.code))
throw new UserError(`Credentials not valid`);
const method = this.methodsAvailables()[this.method];
this.$http.post(method.url, method.data)
.then(({data}) => method.cb(data));
}
}
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state'];
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state', '$location'];
ngModule.vnComponent('vnRecoverPassword', {
template: require('./index.html'),

View File

@ -1,4 +1,9 @@
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 a sms to recover your password: Te enviaremos un sms para restablecer tu contraseña
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`);
const headers = {
Authorization: this.$location.$$search.access_token
Authorization: this.$location.$$search.access_token ?? this.$state.params.access_token
};
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
Repeat password: Repetir contraseña
Password changed!: ¡Contraseña cambiada!

View File

@ -229,6 +229,7 @@
"InvoiceIn is already booked": "InvoiceIn is already booked",
"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",
"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",
"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",
@ -240,10 +241,6 @@
"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 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",
"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"
}
"The quantity claimed cannot be greater than the quantity of the line": "The quantity claimed cannot be greater than the quantity of the line"
}

View File

@ -350,7 +350,6 @@
"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",
"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",
"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.",
@ -364,6 +363,7 @@
"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",
"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",
"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",
@ -382,10 +382,6 @@
"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",
"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",
"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"
}
"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"
}

View File

@ -1,6 +1,9 @@
{
"name": "Account",
"base": "VnModel",
"mixins": {
"Loggable": true
},
"options": {
"mysql": {
"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>
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Corregir tabulación

Corregir tabulación
<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) => {
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({
clientFk: id,

View File

@ -30,7 +30,7 @@ module.exports = Self => {
const allSms = [];
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);
}

View File

@ -48,7 +48,7 @@ module.exports = Self => {
CALL vn.sale_recalcComponent(null);
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();

View File

@ -32,7 +32,7 @@ module.exports = Self => {
Self.sendSms = async(ctx, id, destination, message) => {
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);
await models.ClientSms.create({
clientFk,