Merge branch 'dev' into 4515-itemWeight
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Pablo Natek 2023-11-13 14:06:25 +00:00
commit 129001ad0b
462 changed files with 12722 additions and 7956 deletions

View File

@ -10,5 +10,8 @@
"eslint.format.enable": true, "eslint.format.enable": true,
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint"
} },
"cSpell.words": [
"salix"
]
} }

View File

@ -5,32 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2348.01] - 2023-11-30
## [2340.01] - 2023-10-05
### Added ### Added
### Changed ### Changed
### Fixed ### Fixed
## [2346.01] - 2023-11-16
### Added
### Changed
### Fixed
## [2342.01] - 2023-11-02
### Added
- (Usuarios -> Foto) Se muestra la foto del trabajador
### Fixed
- (Usuarios -> Historial) Abre el descriptor del usuario correctamente
## [2340.01] - 2023-10-05
## [2338.01] - 2023-09-21 ## [2338.01] - 2023-09-21
### Added ### Added
- (Ticket -> Servicios) Se pueden abonar servicios - (Ticket -> Servicios) Se pueden abonar servicios
- (Facturas -> Datos básicos) Muestra valores por defecto
- (Facturas -> Borrado) Notificación al borrar un asiento ya enlazado en Sage
### Changed ### Changed
- (Trabajadores -> Calendario) Icono de check arreglado cuando pulsas un tipo de dia - (Trabajadores -> Calendario) Icono de check arreglado cuando pulsas un tipo de dia
### Fixed
## [2336.01] - 2023-09-07 ## [2336.01] - 2023-09-07
### Added
### Changed
### Fixed
## [2334.01] - 2023-08-24 ## [2334.01] - 2023-08-24
### Added ### Added

View File

@ -26,15 +26,14 @@ module.exports = Self => {
Self.sendCheckingPresence = async(ctx, recipientId, message) => { Self.sendCheckingPresence = async(ctx, recipientId, message) => {
if (!recipientId) return false; if (!recipientId) return false;
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const sender = await models.VnUser.findById(userId, {fields: ['id']}); const sender = await models.VnUser.findById(userId, {fields: ['id']});
const recipient = await models.VnUser.findById(recipientId, null); const recipient = await models.VnUser.findById(recipientId, null);
// Prevent sending messages to yourself // Prevent sending messages to yourself
if (recipientId == userId) return false; if (recipientId == userId) return false;
if (!recipient) if (!recipient)
throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`); throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`);

View File

@ -0,0 +1,135 @@
module.exports = Self => {
Self.remoteMethodCtx('getTickets', {
description: 'Make a new collection of tickets',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'number',
description: 'The collection id',
required: true,
http: {source: 'path'}
}, {
arg: 'print',
type: 'boolean',
description: 'True if you want to print'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/getTickets`,
verb: 'POST'
}
});
Self.getTickets = async(ctx, id, print, options) => {
const userId = ctx.req.accessToken.userId;
const url = await Self.app.models.Url.getUrl();
const $t = ctx.req.__;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
myOptions.userId = userId;
const promises = [];
const [tickets] = await Self.rawSql(`CALL vn.collection_getTickets(?)`, [id], myOptions);
const sales = await Self.rawSql(`
SELECT s.ticketFk,
sgd.saleGroupFk,
s.id saleFk,
s.itemFk,
i.longName,
i.size,
ic.color,
o.code origin,
ish.packing,
ish.grouping,
s.isAdded,
s.originalQuantity,
s.quantity saleQuantity,
iss.quantity reservedQuantity,
SUM(iss.quantity) OVER (PARTITION BY s.id ORDER BY ish.id) accumulatedQuantity,
ROW_NUMBER () OVER (PARTITION BY s.id ORDER BY pickingOrder) currentItemShelving,
COUNT(*) OVER (PARTITION BY s.id ORDER BY s.id) totalItemShelving,
sh.code,
IFNULL(p2.code, p.code) parkingCode,
IFNULL(p2.pickingOrder, p.pickingOrder) pickingOrder,
iss.id itemShelvingSaleFk,
iss.isPicked
FROM ticketCollection tc
LEFT JOIN collection c ON c.id = tc.collectionFk
JOIN ticket t ON t.id = tc.ticketFk
JOIN sale s ON s.ticketFk = t.id
LEFT JOIN saleGroupDetail sgd ON sgd.saleFk = s.id
LEFT JOIN saleGroup sg ON sg.id = sgd.saleGroupFk
LEFT JOIN parking p2 ON p2.id = sg.parkingFk
JOIN item i ON i.id = s.itemFk
LEFT JOIN itemShelvingSale iss ON iss.saleFk = s.id
LEFT JOIN itemShelving ish ON ish.id = iss.itemShelvingFk
LEFT JOIN shelving sh ON sh.code = ish.shelvingFk
LEFT JOIN parking p ON p.id = sh.parkingFk
LEFT JOIN itemColor ic ON ic.itemFk = s.itemFk
LEFT JOIN origin o ON o.id = i.originFk
WHERE tc.collectionFk = ?
GROUP BY ish.id, p.code, p2.code
ORDER BY pickingOrder;`, [id], myOptions);
if (print)
await Self.rawSql(`CALL vn.collection_printSticker(?, ?)`, [id, null], myOptions);
const collection = {collectionFk: id, tickets: []};
if (tickets && tickets.length) {
for (const ticket of tickets) {
const ticketId = ticket.ticketFk;
if (ticket.observaciones != '') {
for (observation of ticket.observaciones.split(' ')) {
if (['#', '@'].includes(observation.charAt(0))) {
promises.push(Self.app.models.Chat.send(ctx, observation,
$t('The ticket is in preparation', {
ticketId: ticketId,
ticketUrl: `${url}ticket/${ticketId}/summary`,
salesPersonId: ticket.salesPersonFk
})));
}
}
}
if (sales && sales.length) {
const barcodes = await Self.rawSql(`
SELECT s.id saleFk, b.code, c.id
FROM vn.sale s
LEFT JOIN vn.itemBarcode b ON b.itemFk = s.itemFk
LEFT JOIN vn.buy c ON c.itemFk = s.itemFk
LEFT JOIN vn.entry e ON e.id = c.entryFk
LEFT JOIN vn.travel tr ON tr.id = e.travelFk
WHERE s.ticketFk = ?
AND tr.landed >= util.VN_CURDATE() - INTERVAL 1 YEAR`,
[ticketId], myOptions);
ticket.sales = [];
for (const sale of sales) {
if (sale.ticketFk === ticketId) {
sale.Barcodes = [];
if (barcodes && barcodes.length) {
for (const barcode of barcodes) {
if (barcode.saleFk === sale.saleFk) {
for (const prop in barcode) {
if (['id', 'code'].includes(prop) && barcode[prop])
sale.Barcodes.push(barcode[prop].toString(), '0' + barcode[prop]);
}
}
}
}
ticket.sales.push(sale);
}
}
}
collection.tickets.push(ticket);
}
}
await Promise.all(promises);
return collection;
};
};

View File

@ -0,0 +1,39 @@
const models = require('vn-loopback/server/server').models;
describe('collection getTickets()', () => {
let ctx;
beforeAll(async() => {
ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'}
}
};
});
it('should get tickets, sales and barcodes from collection', async() => {
const tx = await models.Collection.beginTransaction({});
try {
const options = {transaction: tx};
const collectionId = 1;
const collectionTickets = await models.Collection.getTickets(ctx, collectionId, null, options);
expect(collectionTickets.collectionFk).toEqual(collectionId);
expect(collectionTickets.tickets.length).toEqual(3);
expect(collectionTickets.tickets[0].ticketFk).toEqual(1);
expect(collectionTickets.tickets[1].ticketFk).toEqual(2);
expect(collectionTickets.tickets[2].ticketFk).toEqual(23);
expect(collectionTickets.tickets[0].sales[0].ticketFk).toEqual(1);
expect(collectionTickets.tickets[0].sales[1].ticketFk).toEqual(1);
expect(collectionTickets.tickets[0].sales[2].ticketFk).toEqual(1);
expect(collectionTickets.tickets[0].sales[0].Barcodes.length).toBeTruthy();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -18,6 +18,14 @@ describe('setSaleQuantity()', () => {
it('should change quantity sale', async() => { it('should change quantity sale', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
if (sqlStatement.includes('catalog_calcFromItem')) {
sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY
SELECT 100 as available;`;
params = null;
}
return models.Ticket.rawSql(sqlStatement, params, options);
});
try { try {
const options = {transaction: tx}; const options = {transaction: tx};

View File

@ -0,0 +1,54 @@
module.exports = Self => {
Self.remoteMethod('getList', {
description: 'Get list of the available and active notification subscriptions',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
description: 'User to modify',
http: {source: 'path'}
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/getList`,
verb: 'GET'
}
});
Self.getList = async(id, options) => {
const activeNotificationsMap = new Map();
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const availableNotificationsMap = await Self.getAvailable(id, myOptions);
const activeNotifications = await Self.app.models.NotificationSubscription.find({
fields: ['id', 'notificationFk'],
include: {relation: 'notification'},
where: {userFk: id}
}, myOptions);
for (active of activeNotifications) {
activeNotificationsMap.set(active.notificationFk, {
id: active.id,
notificationFk: active.notificationFk,
name: active.notification().name,
description: active.notification().description,
active: true
});
availableNotificationsMap.delete(active.notificationFk);
}
return {
active: [...activeNotificationsMap.entries()],
available: [...availableNotificationsMap.entries()]
};
};
};

View File

@ -0,0 +1,13 @@
const models = require('vn-loopback/server/server').models;
describe('NotificationSubscription getList()', () => {
it('should return a list of available and active notifications of a user', async() => {
const userId = 9;
const {active, available} = await models.NotificationSubscription.getList(userId);
const notifications = await models.Notification.find({});
const totalAvailable = notifications.length - active.length;
expect(active.length).toEqual(2);
expect(available.length).toEqual(totalAvailable);
});
});

View File

@ -0,0 +1,40 @@
module.exports = function(Self) {
Self.remoteMethod('getByUser', {
description: 'returns the starred modules for the current user',
accessType: 'READ',
accepts: [{
arg: 'userId',
type: 'number',
description: 'The user id',
required: true,
http: {source: 'path'}
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/:userId/get-by-user`,
verb: 'GET'
}
});
Self.getByUser = async userId => {
const models = Self.app.models;
const appNames = ['hedera'];
const filter = {
fields: ['appName', 'url'],
where: {
appName: {inq: appNames},
environment: process.env.NODE_ENV ?? 'development',
}
};
const isWorker = await models.Account.findById(userId, {fields: ['id']});
if (!isWorker)
return models.Url.find(filter);
appNames.push('salix');
return models.Url.find(filter);
};
};

View File

@ -0,0 +1,30 @@
module.exports = Self => {
Self.remoteMethod('getUrl', {
description: 'Returns the colling app name',
accessType: 'READ',
accepts: [
{
arg: 'app',
type: 'string',
required: false
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/getUrl`,
verb: 'get'
}
});
Self.getUrl = async(appName = 'salix') => {
const {url} = await Self.app.models.Url.findOne({
where: {
appName,
enviroment: process.env.NODE_ENV || 'development'
}
});
return url;
};
};

View File

@ -0,0 +1,19 @@
const {models} = require('vn-loopback/server/server');
describe('getByUser()', () => {
const worker = 1;
const notWorker = 2;
it(`should return only hedera url if not is worker`, async() => {
const urls = await models.Url.getByUser(notWorker);
expect(urls.length).toEqual(1);
expect(urls[0].appName).toEqual('hedera');
});
it(`should return more than hedera url`, async() => {
const urls = await models.Url.getByUser(worker);
expect(urls.length).toBeGreaterThan(1);
expect(urls.find(url => url.appName == 'salix').appName).toEqual('salix');
});
});

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethodCtx('updateUser', {
description: 'Update user data',
accepts: [
{
arg: 'id',
type: 'integer',
description: 'The user id',
required: true,
http: {source: 'path'}
}, {
arg: 'name',
type: 'string',
description: 'The user name',
}, {
arg: 'nickname',
type: 'string',
description: 'The user nickname',
}, {
arg: 'email',
type: 'string',
description: 'The user email'
}, {
arg: 'lang',
type: 'string',
description: 'The user lang'
}
],
http: {
path: `/:id/update-user`,
verb: 'PATCH'
}
});
Self.updateUser = async(ctx, id, name, nickname, email, lang) => {
await Self.userSecurity(ctx, id);
await Self.upsertWithWhere({id}, {name, nickname, email, lang});
};
};

View File

@ -7,17 +7,14 @@ module.exports = Self => {
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
if (!ctx.isNewInstance) return; if (!ctx.isNewInstance) return;
let {message} = ctx.instance; let {message} = ctx.instance;
if (!message) return; if (!message) return;
const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g); const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g);
if (!parts) return; if (!parts) return;
const replacedParts = parts.map(part => { const replacedParts = parts.map(part => {
return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, ''); return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, '');
}); });
for (const [index, part] of parts.entries()) for (const [index, part] of parts.entries())
message = message.replace(part, replacedParts[index]); message = message.replace(part, replacedParts[index]);

View File

@ -4,4 +4,5 @@ module.exports = Self => {
require('../methods/collection/getSectors')(Self); require('../methods/collection/getSectors')(Self);
require('../methods/collection/setSaleQuantity')(Self); require('../methods/collection/setSaleQuantity')(Self);
require('../methods/collection/previousLabel')(Self); require('../methods/collection/previousLabel')(Self);
require('../methods/collection/getTickets')(Self);
}; };

View File

@ -1,62 +1,74 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
require('../methods/notification/getList')(Self);
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
await checkModifyPermission(ctx);
});
Self.observe('before delete', async function(ctx) {
await checkModifyPermission(ctx);
});
async function checkModifyPermission(ctx) {
const models = Self.app.models; const models = Self.app.models;
const instance = ctx.instance;
const userId = ctx.options.accessToken.userId; const userId = ctx.options.accessToken.userId;
const user = await ctx.instance.userFk;
const modifiedUser = await getUserToModify(null, user, models);
if (userId != modifiedUser.id && userId != modifiedUser.bossFk) let notificationFk;
throw new UserError('You dont have permission to modify this user'); let workerId;
});
Self.remoteMethod('deleteNotification', { if (instance) {
description: 'Deletes a notification subscription', notificationFk = instance.notificationFk;
accepts: [ workerId = instance.userFk;
{ } else {
arg: 'ctx', const notificationSubscription = await models.NotificationSubscription.findById(ctx.where.id);
type: 'object', notificationFk = notificationSubscription.notificationFk;
http: {source: 'context'} workerId = notificationSubscription.userFk;
},
{
arg: 'notificationId',
type: 'number',
required: true
},
],
returns: {
type: 'object',
root: true
},
http: {
verb: 'POST',
path: '/deleteNotification'
} }
});
Self.deleteNotification = async function(ctx, notificationId) { const worker = await models.Worker.findById(workerId, {fields: ['id', 'bossFk']});
const available = await Self.getAvailable(workerId);
const hasAcl = available.has(notificationFk);
if (!hasAcl || (userId != worker.id && userId != worker.bossFk))
throw new UserError('The notification subscription of this worker cant be modified');
}
Self.getAvailable = async function(userId, options) {
const availableNotificationsMap = new Map();
const models = Self.app.models; const models = Self.app.models;
const user = ctx.req.accessToken.userId;
const modifiedUser = await getUserToModify(notificationId, null, models);
if (user != modifiedUser.id && user != modifiedUser.bossFk) const myOptions = {};
throw new UserError('You dont have permission to modify this user');
await models.NotificationSubscription.destroyById(notificationId); if (typeof options == 'object')
}; Object.assign(myOptions, options);
async function getUserToModify(notificationId, userFk, models) { const roles = await models.RoleMapping.find({
let userToModify = userFk; fields: ['roleId'],
if (notificationId) { where: {principalId: userId}
const subscription = await models.NotificationSubscription.findById(notificationId); }, myOptions);
userToModify = subscription.userFk;
} const availableNotifications = await models.NotificationAcl.find({
return await models.Worker.findOne({ fields: ['notificationFk', 'roleFk'],
fields: ['id', 'bossFk'], include: {relation: 'notification'},
where: { where: {
id: userToModify roleFk: {
inq: roles.map(role => role.roleId),
},
} }
}, myOptions);
for (available of availableNotifications) {
availableNotificationsMap.set(available.notificationFk, {
id: null,
notificationFk: available.notificationFk,
name: available.notification().name,
description: available.notification().description,
active: false
}); });
} }
return availableNotificationsMap;
};
}; };

View File

@ -1,74 +1,126 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
describe('loopback model NotificationSubscription', () => { describe('loopback model NotificationSubscription', () => {
it('Should fail to delete a notification if the user is not editing itself or a subordinate', async() => { it('should fail to add a notification subscription if the worker doesnt have ACLs', async() => {
const tx = await models.NotificationSubscription.beginTransaction({}); const tx = await models.NotificationSubscription.beginTransaction({});
try {
const options = {transaction: tx};
const user = 9;
const notificationSubscriptionId = 2;
const ctx = {req: {accessToken: {userId: user}}};
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
let error; let error;
try { try {
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options); const options = {transaction: tx, accessToken: {userId: 9}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 62}, options);
await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback();
error = e; error = e;
} }
expect(error.message).toContain('You dont have permission to modify this user'); expect(error.message).toEqual('The notification subscription of this worker cant be modified');
});
it('should fail to add a notification subscription if the user isnt editing itself or subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 1}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 9}, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; error = e;
} }
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
}); });
it('Should delete a notification if the user is editing itself', async() => { it('should fail to delete a notification subscription if the user isnt editing itself or subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({}); const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx, accessToken: {userId: 9}};
const user = 9; const notificationSubscriptionId = 2;
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
});
it('should add a notification subscription if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
await models.NotificationSubscription.create({notificationFk: 2, userFk: 9}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a notification subscription if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
const notificationSubscriptionId = 6;
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should add a notification subscription if the user is editing a subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 5}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a notification subscription if the user is editing a subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 19}};
const notificationSubscriptionId = 4; const notificationSubscriptionId = 4;
const ctx = {req: {accessToken: {userId: user}}}; await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
expect(deletedNotification).toBeNull();
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; error = e;
} }
});
it('Should delete a notification if the user is editing a subordinate', async() => { expect(error).toBeUndefined();
const tx = await models.NotificationSubscription.beginTransaction({});
try {
const options = {transaction: tx};
const user = 9;
const notificationSubscriptionId = 5;
const ctx = {req: {accessToken: {userId: user}}};
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
expect(deletedNotification).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const ForbiddenError = require('vn-loopback/util/forbiddenError');
describe('loopback model VnUser', () => { describe('loopback model VnUser', () => {
it('should return true if the user has the given role', async() => { it('should return true if the user has the given role', async() => {
@ -12,4 +13,42 @@ describe('loopback model VnUser', () => {
expect(result).toBeFalsy(); expect(result).toBeFalsy();
}); });
describe('userSecurity', () => {
const itManagementId = 115;
const hrId = 37;
const employeeId = 1;
it('should check if you are the same user', async() => {
const ctx = {options: {accessToken: {userId: employeeId}}};
await models.VnUser.userSecurity(ctx, employeeId);
});
it('should check for higher privileges', async() => {
const ctx = {options: {accessToken: {userId: itManagementId}}};
await models.VnUser.userSecurity(ctx, employeeId);
});
it('should check if you have medium privileges and the user email is not verified', async() => {
const ctx = {options: {accessToken: {userId: hrId}}};
await models.VnUser.userSecurity(ctx, employeeId);
});
it('should throw an error 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('emailVerified', 1, options);
await models.VnUser.userSecurity(ctx, employeeId, options);
await tx.rollback();
} catch (error) {
await tx.rollback();
expect(error).toEqual(new ForbiddenError());
}
});
});
}); });

4
back/models/url.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/url/getByUser')(Self);
require('../methods/url/getUrl')(Self);
};

View File

@ -1,6 +1,7 @@
const vnModel = require('vn-loopback/common/models/vn-model'); const vnModel = require('vn-loopback/common/models/vn-model');
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print'); const {Email} = require('vn-print');
const ForbiddenError = require('vn-loopback/util/forbiddenError');
const LoopBackContext = require('loopback-context');
module.exports = function(Self) { module.exports = function(Self) {
vnModel(Self); vnModel(Self);
@ -12,6 +13,7 @@ module.exports = function(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);
require('../methods/vn-user/update-user')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create'); Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
@ -90,11 +92,7 @@ module.exports = function(Self) {
}; };
Self.on('resetPasswordRequest', async function(info) { Self.on('resetPasswordRequest', async function(info) {
const loopBackContext = LoopBackContext.getCurrentContext(); const url = await Self.app.models.Url.getUrl();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const defaultHash = '/reset-password?access_token=$token$'; const defaultHash = '/reset-password?access_token=$token$';
const recoverHashes = { const recoverHashes = {
@ -110,7 +108,7 @@ module.exports = function(Self) {
const params = { const params = {
recipient: info.email, recipient: info.email,
lang: user.lang, lang: user.lang,
url: origin + '/#!' + recoverHash url: url.slice(0, -1) + recoverHash
}; };
const options = Object.assign({}, info.options); const options = Object.assign({}, info.options);
@ -178,45 +176,75 @@ module.exports = function(Self) {
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
.filter(acl => acl.property != 'changePassword'); .filter(acl => acl.property != 'changePassword');
// FIXME: https://redmine.verdnatura.es/issues/5761 Self.userSecurity = async(ctx, userId, options) => {
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => { const models = Self.app.models;
// if (!ctx.args || !ctx.args.data.email) return; const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken;
const ctxToken = {req: {accessToken}};
// const loopBackContext = LoopBackContext.getCurrentContext(); if (userId === accessToken.userId) return;
// const httpCtx = {req: loopBackContext.active};
// const httpRequest = httpCtx.req.http.req;
// const headers = httpRequest.headers;
// const origin = headers.origin;
// const url = origin.split(':');
// class Mailer { const myOptions = {};
// async send(verifyOptions, cb) { if (typeof options == 'object')
// const params = { Object.assign(myOptions, options);
// url: verifyOptions.verifyHref,
// recipient: verifyOptions.to,
// lang: ctx.req.getLocale()
// };
// const email = new Email('email-verify', params); const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions);
// email.send(); if (hasHigherPrivileges) return;
// cb(null, verifyOptions.to); const hasMediumPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'mediumPrivileges', myOptions);
// } const user = await models.VnUser.findById(userId, {fields: ['id', 'emailVerified']}, myOptions);
// } if (!user.emailVerified && hasMediumPrivileges) return;
// const options = { throw new ForbiddenError();
// type: 'email', };
// to: instance.email,
// from: {},
// redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`,
// template: false,
// mailer: new Mailer,
// host: url[1].split('/')[2],
// port: url[2],
// protocol: url[0],
// user: Self
// };
// await instance.verify(options); Self.observe('after save', async ctx => {
// }); const instance = ctx?.instance;
const newEmail = instance?.email;
const oldEmail = ctx?.hookState?.oldInstance?.email;
if (!ctx.isNewInstance && (!newEmail || !oldEmail || newEmail == oldEmail)) return;
const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const url = origin.split(':');
const env = process.env.NODE_ENV;
const liliumUrl = await Self.app.models.Url.findOne({
where: {and: [
{appName: 'lilium'},
{environment: env}
]}
});
class Mailer {
async send(verifyOptions, cb) {
const params = {
url: verifyOptions.verifyHref,
recipient: verifyOptions.to
};
const email = new Email('email-verify', params);
email.send();
cb(null, verifyOptions.to);
}
}
const options = {
type: 'email',
to: newEmail,
from: {},
redirect: `${liliumUrl.url}verifyEmail?userId=${instance.id}`,
template: false,
mailer: new Mailer,
host: url[1].split('/')[2],
port: url[2],
protocol: url[0],
user: Self
};
await instance.verify(options, ctx.options);
});
}; };

View File

@ -18,14 +18,7 @@
"required": true "required": true
}, },
"username": { "username": {
"type": "string", "type": "string"
"mysql": {
"columnName": "name"
}
},
"password": {
"type": "string",
"required": true
}, },
"roleFk": { "roleFk": {
"type": "number", "type": "number",
@ -45,6 +38,9 @@
"email": { "email": {
"type": "string" "type": "string"
}, },
"emailVerified": {
"type": "boolean"
},
"created": { "created": {
"type": "date" "type": "date"
}, },
@ -84,7 +80,7 @@
"worker": { "worker": {
"type": "hasOne", "type": "hasOne",
"model": "Worker", "model": "Worker",
"foreignKey": "userFk" "foreignKey": "id"
}, },
"userConfig": { "userConfig": {
"type": "hasOne", "type": "hasOne",
@ -144,7 +140,8 @@
"image", "image",
"hasGrant", "hasGrant",
"realm", "realm",
"email" "email",
"emailVerified"
] ]
} }
} }

View File

@ -34,7 +34,7 @@ BEGIN
isAllowedToWork isAllowedToWork
FROM(SELECT t.dated, FROM(SELECT t.dated,
b.id businessFk, b.id businessFk,
w.userFk, w.id,
b.departmentFk, b.departmentFk,
IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.start,5) ORDER BY j.start ASC SEPARATOR ' - ')) hourStart , IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.start,5) ORDER BY j.start ASC SEPARATOR ' - ')) hourStart ,
IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.end,5) ORDER BY j.end ASC SEPARATOR ' - ')) hourEnd, IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.end,5) ORDER BY j.end ASC SEPARATOR ' - ')) hourEnd,
@ -48,14 +48,14 @@ BEGIN
FROM time t FROM time t
LEFT JOIN business b ON t.dated BETWEEN b.started AND IFNULL(b.ended, vDatedTo) LEFT JOIN business b ON t.dated BETWEEN b.started AND IFNULL(b.ended, vDatedTo)
LEFT JOIN worker w ON w.id = b.workerFk LEFT JOIN worker w ON w.id = b.workerFk
JOIN tmp.`user` u ON u.userFK = w.userFK JOIN tmp.`user` u ON u.userFK = w.id
LEFT JOIN workCenter wc ON wc.id = b.workcenterFK LEFT JOIN workCenter wc ON wc.id = b.workcenterFK
LEFT JOIN postgresql.calendar_labour_type cl ON cl.calendar_labour_type_id = b.calendarTypeFk LEFT JOIN postgresql.calendar_labour_type cl ON cl.calendar_labour_type_id = b.calendarTypeFk
LEFT JOIN postgresql.journey j ON j.business_id = b.id AND j.day_id = WEEKDAY(t.dated) + 1 LEFT JOIN postgresql.journey j ON j.business_id = b.id AND j.day_id = WEEKDAY(t.dated) + 1
LEFT JOIN postgresql.calendar_employee ce ON ce.businessFk = b.id AND ce.date = t.dated LEFT JOIN postgresql.calendar_employee ce ON ce.businessFk = b.id AND ce.date = t.dated
LEFT JOIN absenceType at2 ON at2.id = ce.calendar_state_id LEFT JOIN absenceType at2 ON at2.id = ce.calendar_state_id
WHERE t.dated BETWEEN vDatedFrom AND vDatedTo WHERE t.dated BETWEEN vDatedFrom AND vDatedTo
GROUP BY w.userFk, t.dated GROUP BY w.id, t.dated
)sub; )sub;
UPDATE tmp.timeBusinessCalculate t UPDATE tmp.timeBusinessCalculate t

View File

@ -46,7 +46,7 @@ BEGIN
CONCAT('Cliente ', NEW.id), CONCAT('Cliente ', NEW.id),
CONCAT('Recibida la documentación: ', vText) CONCAT('Recibida la documentación: ', vText)
FROM worker w FROM worker w
LEFT JOIN account.user u ON w.userFk = u.id AND u.active LEFT JOIN account.user u ON w.id = u.id AND u.active
LEFT JOIN account.account ac ON ac.id = u.id LEFT JOIN account.account ac ON ac.id = u.id
WHERE w.id = NEW.salesPersonFk; WHERE w.id = NEW.salesPersonFk;
END IF; END IF;

View File

@ -3,11 +3,11 @@ INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `pri
('Ticket', 'editDiscount', 'WRITE', 'ALLOW', 'ROLE', 'claimManager'), ('Ticket', 'editDiscount', 'WRITE', 'ALLOW', 'ROLE', 'claimManager'),
('Ticket', 'editDiscount', 'WRITE', 'ALLOW', 'ROLE', 'salesPerson'), ('Ticket', 'editDiscount', 'WRITE', 'ALLOW', 'ROLE', 'salesPerson'),
('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'salesAssistant'), ('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'salesAssistant'),
('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'deliveryBoss'), ('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'deliveryAssistant'),
('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'buyer'), ('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'buyer'),
('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'claimManager'), ('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'claimManager'),
('Ticket', 'deleteTicketWithPartPrepared', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant'), ('Ticket', 'deleteTicketWithPartPrepared', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant'),
('Ticket', 'editZone', 'WRITE', 'ALLOW', 'ROLE', 'deliveryBoss'), ('Ticket', 'editZone', 'WRITE', 'ALLOW', 'ROLE', 'deliveryAssistant'),
('State', 'editableStates', 'READ', 'ALLOW', 'ROLE', 'employee'), ('State', 'editableStates', 'READ', 'ALLOW', 'ROLE', 'employee'),
('State', 'seeEditableStates', 'READ', 'ALLOW', 'ROLE', 'administrative'), ('State', 'seeEditableStates', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('State', 'seeEditableStates', 'READ', 'ALLOW', 'ROLE', 'production'), ('State', 'seeEditableStates', 'READ', 'ALLOW', 'ROLE', 'production'),

View File

@ -1,6 +1,6 @@
ALTER TABLE `vn`.`deviceLog` ADD serialNumber varchar(45) DEFAULT NULL NULL; -- ALTER TABLE `vn`.`deviceLog` ADD serialNumber varchar(45) DEFAULT NULL NULL;
INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId) -- INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId)
VALUES( 'DeviceLog', 'create', 'WRITE', 'ALLOW', 'ROLE', 'employee'); -- VALUES( 'DeviceLog', 'create', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL`(model, property, accessType, permission, principalType, principalId)
VALUES
('Collection', 'getTickets', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,42 @@
-- No encuentro este back
DELETE FROM `salix`.`ACL` WHERE property = 'activeWorkersWithRole';
DELETE FROM `salix`.`ACL` WHERE model = 'Client' AND property = '*';
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('Client','findOne','READ','ALLOW','ROLE','employee');
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('Client','findById','READ','ALLOW','ROLE','employee');
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('Client','find','READ','ALLOW','ROLE','employee');
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('Client','exists','READ','ALLOW','ROLE','employee');
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('Client','__get__addresses','READ','ALLOW','ROLE','employee');
DELETE FROM `salix`.`ACL` WHERE model = 'Client' AND property = '*' AND accessType IN (
'campaignMetricsEmail',
'campaignMetricsPdf',
'clientDebtStatementEmail',
'clientDebtStatementHtml',
'clientDebtStatementPdf',
'clientWelcomeEmail',
'clientWelcomeHtml',
'consumptionSendQueued',
'creditRequestEmail',
'creditRequestHtml',
'creditRequestPdf',
'getClientOrSupplierReference',
'incotermsAuthorizationEmail',
'incotermsAuthorizationHtml',
'incotermsAuthorizationPdf',
'letterDebtorNdEmail',
'letterDebtorNdHtml',
'letterDebtorPdf',
'letterDebtorStEmail',
'letterDebtorStHtml',
'printerSetupEmail',
'printerSetupHtml',
'sepaCoreEmail',
'setPassword',
'updateUser',
'uploadFile');

View File

@ -0,0 +1,4 @@
ALTER TABLE `vn`.`worker` DROP KEY `user_id_UNIQUE`;
ALTER TABLE `vn`.`worker` DROP COLUMN `userFk`;

Some files were not shown because too many files have changed in this diff Show More