#6427 - SMS Recover Password #2037
|
@ -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)
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
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
alexm
commented
Pondria directamente aqui el filtro Pondria directamente aqui el filtro
alexm
commented
```
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};
|
||||
};
|
||||
};
|
|
@ -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, '');
|
||||
|
|
|
@ -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});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "smsConfig"
|
||||
"table": "vn.smsConfig"
|
||||
}
|
||||
},
|
||||
"properties": {
|
|
@ -1,4 +1,3 @@
|
|||
module.exports = Self => {
|
||||
// Methods
|
||||
require('../methods/sms/send')(Self);
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "sms"
|
||||
"table": "vn.sms"
|
||||
}
|
||||
},
|
||||
"properties": {
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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"]',
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,16 +1,49 @@
|
|||
<h5 class="vn-mb-md vn-mt-lg" translate>Recover password</h5>
|
||||
<vn-textfield
|
||||
alexm
commented
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
jsegarra
commented
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"
|
||||
alexm
commented
No entiendo el poner 2 vn-textfields con ifs. 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)
jsegarra
commented
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.
alexm
commented
Entonces podría poner tu id, y mi numero de teléfono y te podría cambiar la contraseña? 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
jsegarra
commented
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
alexm
commented
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
juan
commented
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>
|
||||
|
|
|
@ -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
alexm
commented
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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"name": "Account",
|
||||
"base": "VnModel",
|
||||
"mixins": {
|
||||
"Loggable": true
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "account.account"
|
||||
|
|
|
@ -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
juan
commented
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>
|
|
@ -0,0 +1,3 @@
|
|||
Email verified successfully!: Correo verificado correctamente!
|
||||
Recovery phone: Teléfono de recuperación de cuenta
|
||||
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
pq aqui es
_opt
pero en accepts esopt
?Usar
otp
Correcto, el nombre de la variable está obsoleto
Gracias
Lo cambio todo por code
pq aqui es
_opt
pero en accepts esopt
?Usar
otp