Merge branch 'dev' into 5914-warmfix-renameTable
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
2cd9c9c688
|
@ -10,5 +10,9 @@
|
||||||
"eslint.format.enable": true,
|
"eslint.format.enable": true,
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
}
|
},
|
||||||
|
"cSpell.words": [
|
||||||
|
"salix",
|
||||||
|
"fdescribe"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
|
||||||
|
VALUES
|
||||||
|
('Application', 'executeProc', '*', 'ALLOW', 'ROLE', 'employee'),
|
||||||
|
('Application', 'executeFunc', '*', 'ALLOW', 'ROLE', 'employee');
|
|
@ -0,0 +1,3 @@
|
||||||
|
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
|
||||||
|
VALUES
|
||||||
|
('NotificationSubscription', 'getList', 'READ', 'ALLOW', 'ROLE', 'employee');
|
|
@ -2788,6 +2788,11 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
|
||||||
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
|
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
|
||||||
VALUES
|
VALUES
|
||||||
(1, 9),
|
(1, 9),
|
||||||
|
(1, 1),
|
||||||
|
(2, 1),
|
||||||
|
(3, 9),
|
||||||
|
(4, 1),
|
||||||
|
(5, 9),
|
||||||
(6, 9);
|
(6, 9);
|
||||||
|
|
||||||
INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`)
|
INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`)
|
||||||
|
@ -2800,6 +2805,8 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`)
|
||||||
VALUES
|
VALUES
|
||||||
(1, 1109),
|
(1, 1109),
|
||||||
(1, 1110),
|
(1, 1110),
|
||||||
|
(2, 1110),
|
||||||
|
(4, 1110),
|
||||||
(2, 1109),
|
(2, 1109),
|
||||||
(1, 9),
|
(1, 9),
|
||||||
(1, 3),
|
(1, 3),
|
||||||
|
|
|
@ -2352,6 +2352,90 @@ BEGIN
|
||||||
END IF;
|
END IF;
|
||||||
END ;;
|
END ;;
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|
||||||
|
|
||||||
|
DELIMITER ;;
|
||||||
|
CREATE DEFINER=`root`@`localhost` FUNCTION `account`.`user_hasRoutinePriv`(vType ENUM('PROCEDURE', 'FUNCTION'),
|
||||||
|
vChain VARCHAR(100),
|
||||||
|
vUserFk INT
|
||||||
|
) RETURNS tinyint(1)
|
||||||
|
READS SQL DATA
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search if the user has privileges on routines.
|
||||||
|
*
|
||||||
|
* @param vType procedure or function
|
||||||
|
* @param vChain string passed with this syntax dbName.tableName
|
||||||
|
* @param vUserFk user to ckeck
|
||||||
|
* @return vHasPrivilege
|
||||||
|
*/
|
||||||
|
DECLARE vHasPrivilege BOOL DEFAULT FALSE;
|
||||||
|
DECLARE vDb VARCHAR(50);
|
||||||
|
DECLARE vObject VARCHAR(50);
|
||||||
|
DECLARE vChainExists BOOL;
|
||||||
|
DECLARE vExecutePriv INT DEFAULT 262144;
|
||||||
|
-- 262144 = CONV(1000000000000000000, 2, 10)
|
||||||
|
-- 1000000000000000000 execution permission expressed in binary base
|
||||||
|
|
||||||
|
SET vDb = SUBSTRING_INDEX(vChain, '.', 1);
|
||||||
|
SET vChain = SUBSTRING(vChain, LENGTH(vDb) + 2);
|
||||||
|
SET vObject = SUBSTRING_INDEX(vChain, '.', 1);
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO vChainExists
|
||||||
|
FROM mysql.proc
|
||||||
|
WHERE db = vDb
|
||||||
|
AND `name` = vObject
|
||||||
|
AND `type` = vType
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF NOT vChainExists THEN
|
||||||
|
RETURN FALSE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS tRole;
|
||||||
|
CREATE TEMPORARY TABLE tRole
|
||||||
|
(INDEX (`name`))
|
||||||
|
ENGINE = MEMORY
|
||||||
|
SELECT r.`name`
|
||||||
|
FROM user u
|
||||||
|
JOIN roleRole rr ON rr.role = u.role
|
||||||
|
JOIN `role` r ON r.id = rr.inheritsFrom
|
||||||
|
WHERE u.id = vUserFk;
|
||||||
|
|
||||||
|
SELECT TRUE INTO vHasPrivilege
|
||||||
|
FROM mysql.global_priv gp
|
||||||
|
JOIN tRole tr ON tr.name = gp.`User`
|
||||||
|
OR CONCAT('$', tr.name) = gp.`User`
|
||||||
|
WHERE JSON_VALUE(gp.Priv, '$.access') >= vExecutePriv
|
||||||
|
AND gp.Host = ''
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF NOT vHasPrivilege THEN
|
||||||
|
SELECT TRUE INTO vHasPrivilege
|
||||||
|
FROM mysql.db db
|
||||||
|
JOIN tRole tr ON tr.name = db.`User`
|
||||||
|
WHERE db.Db = vDb
|
||||||
|
AND db.Execute_priv = 'Y';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT vHasPrivilege THEN
|
||||||
|
SELECT TRUE INTO vHasPrivilege
|
||||||
|
FROM mysql.procs_priv pp
|
||||||
|
JOIN tRole tr ON tr.name = pp.`User`
|
||||||
|
WHERE pp.Db = vDb
|
||||||
|
AND pp.Routine_name = vObject
|
||||||
|
AND pp.Routine_type = vType
|
||||||
|
AND pp.Proc_priv = 'Execute'
|
||||||
|
LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE tRole;
|
||||||
|
RETURN vHasPrivilege;
|
||||||
|
END ;;
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
|
||||||
/*!50003 SET sql_mode = @saved_sql_mode */ ;
|
/*!50003 SET sql_mode = @saved_sql_mode */ ;
|
||||||
/*!50003 SET character_set_client = @saved_cs_client */ ;
|
/*!50003 SET character_set_client = @saved_cs_client */ ;
|
||||||
/*!50003 SET character_set_results = @saved_cs_results */ ;
|
/*!50003 SET character_set_results = @saved_cs_results */ ;
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.execute = async(ctx, type, query, params, options) => {
|
||||||
|
const userId = ctx.req.accessToken.userId;
|
||||||
|
const models = Self.app.models;
|
||||||
|
params = params ?? [];
|
||||||
|
|
||||||
|
const myOptions = {userId: ctx.req.accessToken.userId};
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
const chain = query.split(' ')[1];
|
||||||
|
|
||||||
|
const [canExecute] = await models.ProcsPriv.rawSql(
|
||||||
|
'SELECT account.user_hasRoutinePriv(?,?,?)',
|
||||||
|
[type, chain, userId],
|
||||||
|
myOptions);
|
||||||
|
|
||||||
|
if (!Object.values(canExecute)[0]) throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED');
|
||||||
|
|
||||||
|
const argString = params.map(() => '?').join(',');
|
||||||
|
|
||||||
|
const [response] = await models.ProcsPriv.rawSql(query + `(${argString})`, params, myOptions);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,41 @@
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('executeFunc', {
|
||||||
|
description: 'Return result of function',
|
||||||
|
accessType: '*',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'routine',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The routine name',
|
||||||
|
required: true,
|
||||||
|
http: {source: 'path'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'schema',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The routine schema',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'params',
|
||||||
|
type: ['any'],
|
||||||
|
description: 'The params array',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: 'any',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/:routine/execute-func`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.executeFunc = async(ctx, routine, schema, params, options) => {
|
||||||
|
const query = `SELECT ${schema}.${routine}`;
|
||||||
|
|
||||||
|
const response = await Self.execute(ctx, 'FUNCTION', query, params, options);
|
||||||
|
return Object.values(response)[0];
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('executeProc', {
|
||||||
|
description: 'Return result of procedure',
|
||||||
|
accessType: '*',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'routine',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The routine name',
|
||||||
|
required: true,
|
||||||
|
http: {source: 'path'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'schema',
|
||||||
|
type: 'string',
|
||||||
|
description: 'The routine schema',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'params',
|
||||||
|
type: ['any'],
|
||||||
|
description: 'The params array',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: 'any',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/:routine/execute-proc`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.executeProc = async(ctx, routine, schema, params, options) => {
|
||||||
|
const query = `CALL ${schema}.${routine}`;
|
||||||
|
return Self.execute(ctx, 'PROCEDURE', query, params, options);
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,161 @@
|
||||||
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
|
||||||
|
describe('Application execute()/executeProc()/executeFunc()', () => {
|
||||||
|
const userWithoutPrivileges = 1;
|
||||||
|
const userWithPrivileges = 9;
|
||||||
|
const userWithInheritedPrivileges = 120;
|
||||||
|
let tx;
|
||||||
|
|
||||||
|
function getCtx(userId) {
|
||||||
|
return {
|
||||||
|
req: {
|
||||||
|
accessToken: {userId},
|
||||||
|
headers: {origin: 'http://localhost'}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async() => {
|
||||||
|
tx = await models.Application.beginTransaction({});
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
await models.Application.rawSql(`
|
||||||
|
CREATE OR REPLACE PROCEDURE vn.myProcedure(vMyParam INT)
|
||||||
|
BEGIN
|
||||||
|
SELECT vMyParam myParam, t.*
|
||||||
|
FROM ticket t
|
||||||
|
LIMIT 2;
|
||||||
|
END
|
||||||
|
`, null, options);
|
||||||
|
|
||||||
|
await models.Application.rawSql(`
|
||||||
|
CREATE OR REPLACE FUNCTION bs.myFunction(vMyParam INT) RETURNS int(11)
|
||||||
|
BEGIN
|
||||||
|
RETURN vMyParam;
|
||||||
|
END
|
||||||
|
`, null, options);
|
||||||
|
|
||||||
|
await models.Application.rawSql(`
|
||||||
|
GRANT EXECUTE ON PROCEDURE vn.myProcedure TO developer;
|
||||||
|
GRANT EXECUTE ON FUNCTION bs.myFunction TO developer;
|
||||||
|
`, null, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when execute procedure and not have privileges', async() => {
|
||||||
|
const ctx = getCtx(userWithoutPrivileges);
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
await models.Application.execute(
|
||||||
|
ctx,
|
||||||
|
'PROCEDURE',
|
||||||
|
'CALL vn.myProcedure',
|
||||||
|
[1],
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.message).toEqual(`You don't have enough privileges`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute procedure and get data', async() => {
|
||||||
|
const ctx = getCtx(userWithPrivileges);
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const response = await models.Application.execute(
|
||||||
|
ctx,
|
||||||
|
'PROCEDURE',
|
||||||
|
'CALL vn.myProcedure',
|
||||||
|
[1],
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.length).toEqual(2);
|
||||||
|
expect(response[0].myParam).toEqual(1);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Application executeProc()', () => {
|
||||||
|
it('should execute procedure and get data (executeProc)', async() => {
|
||||||
|
const ctx = getCtx(userWithPrivileges);
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const response = await models.Application.executeProc(
|
||||||
|
ctx,
|
||||||
|
'myProcedure',
|
||||||
|
'vn',
|
||||||
|
[1],
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.length).toEqual(2);
|
||||||
|
expect(response[0].myParam).toEqual(1);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Application executeFunc()', () => {
|
||||||
|
it('should execute function and get data', async() => {
|
||||||
|
const ctx = getCtx(userWithPrivileges);
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const response = await models.Application.executeFunc(
|
||||||
|
ctx,
|
||||||
|
'myFunction',
|
||||||
|
'bs',
|
||||||
|
[1],
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual(1);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute function and get data with user with inherited privileges', async() => {
|
||||||
|
const ctx = getCtx(userWithInheritedPrivileges);
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const response = await models.Application.executeFunc(
|
||||||
|
ctx,
|
||||||
|
'myFunction',
|
||||||
|
'bs',
|
||||||
|
[1],
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual(1);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,4 +2,7 @@
|
||||||
module.exports = function(Self) {
|
module.exports = function(Self) {
|
||||||
require('../methods/application/status')(Self);
|
require('../methods/application/status')(Self);
|
||||||
require('../methods/application/post')(Self);
|
require('../methods/application/post')(Self);
|
||||||
|
require('../methods/application/execute')(Self);
|
||||||
|
require('../methods/application/executeProc')(Self);
|
||||||
|
require('../methods/application/executeFunc')(Self);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "ProcsPriv",
|
||||||
|
"base": "VnModel",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "mysql.procs_priv"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"id": 1,
|
||||||
|
"type": "string",
|
||||||
|
"mysql": {
|
||||||
|
"columnName": "Routine_name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"id": 3,
|
||||||
|
"type": "string",
|
||||||
|
"mysql": {
|
||||||
|
"columnName": "Db"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"mysql": {
|
||||||
|
"columnName": "user"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"id": 2,
|
||||||
|
"type": "string",
|
||||||
|
"mysql": {
|
||||||
|
"columnName": "Routine_type"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"type": "string",
|
||||||
|
"mysql": {
|
||||||
|
"columnName": "Host"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -321,9 +321,9 @@
|
||||||
"Select a different client": "Seleccione un cliente distinto",
|
"Select a different client": "Seleccione un cliente distinto",
|
||||||
"Fill all the fields": "Rellene todos los campos",
|
"Fill all the fields": "Rellene todos los campos",
|
||||||
"The response is not a PDF": "La respuesta no es un PDF",
|
"The response is not a PDF": "La respuesta no es un PDF",
|
||||||
"Ticket without Route": "Ticket sin ruta",
|
|
||||||
"Booking completed": "Reserva completada",
|
"Booking completed": "Reserva completada",
|
||||||
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
|
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
|
||||||
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina",
|
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina",
|
||||||
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina"
|
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina",
|
||||||
|
"The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada"
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,5 +49,13 @@
|
||||||
},
|
},
|
||||||
"Container": {
|
"Container": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
|
"ProcsPriv": {
|
||||||
|
"dataSource": "vn",
|
||||||
|
"options": {
|
||||||
|
"mysql": {
|
||||||
|
"table": "mysql.procs_priv"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -158,7 +158,7 @@
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"type": "belongsTo",
|
"type": "belongsTo",
|
||||||
"model": "Account",
|
"model": "VnUser",
|
||||||
"foreignKey": "id"
|
"foreignKey": "id"
|
||||||
},
|
},
|
||||||
"payMethod": {
|
"payMethod": {
|
||||||
|
|
|
@ -120,7 +120,7 @@ module.exports = async function(ctx, Self, tickets, reqArgs = {}) {
|
||||||
await Self.rawSql(`
|
await Self.rawSql(`
|
||||||
INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
|
INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
|
||||||
`, [ticket.clientFk, sample.id, ticket.companyFk], {userId});
|
`, [ticket.clientFk, sample.id, ticket.companyFk], {userId});
|
||||||
};
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Domain not found
|
// Domain not found
|
||||||
if (error.responseCode == 450)
|
if (error.responseCode == 450)
|
||||||
|
|
|
@ -20,4 +20,5 @@ import './dms/create';
|
||||||
import './dms/edit';
|
import './dms/edit';
|
||||||
import './note/index';
|
import './note/index';
|
||||||
import './note/create';
|
import './note/create';
|
||||||
|
import './notifications';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<vn-card>
|
||||||
|
</vn-card>
|
|
@ -0,0 +1,21 @@
|
||||||
|
import ngModule from '../module';
|
||||||
|
import Section from 'salix/components/section';
|
||||||
|
|
||||||
|
class Controller extends Section {
|
||||||
|
constructor($element, $) {
|
||||||
|
super($element, $);
|
||||||
|
}
|
||||||
|
|
||||||
|
async $onInit() {
|
||||||
|
const url = await this.vnApp.getUrl(`worker/${this.$params.id}/notifications`);
|
||||||
|
window.open(url).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngModule.vnComponent('vnWorkerNotifications', {
|
||||||
|
template: require('./index.html'),
|
||||||
|
controller: Controller,
|
||||||
|
bindings: {
|
||||||
|
ticket: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -15,6 +15,7 @@
|
||||||
{"state": "worker.card.timeControl", "icon": "access_time"},
|
{"state": "worker.card.timeControl", "icon": "access_time"},
|
||||||
{"state": "worker.card.calendar", "icon": "icon-calendar"},
|
{"state": "worker.card.calendar", "icon": "icon-calendar"},
|
||||||
{"state": "worker.card.pda", "icon": "phone_android"},
|
{"state": "worker.card.pda", "icon": "phone_android"},
|
||||||
|
{"state": "worker.card.notifications", "icon": "notifications"},
|
||||||
{"state": "worker.card.pbx", "icon": "icon-pbx"},
|
{"state": "worker.card.pbx", "icon": "icon-pbx"},
|
||||||
{"state": "worker.card.dms.index", "icon": "cloud_upload"},
|
{"state": "worker.card.dms.index", "icon": "cloud_upload"},
|
||||||
{
|
{
|
||||||
|
@ -112,6 +113,14 @@
|
||||||
"params": {
|
"params": {
|
||||||
"worker": "$ctrl.worker"
|
"worker": "$ctrl.worker"
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
"url": "/notifications",
|
||||||
|
"state": "worker.card.notifications",
|
||||||
|
"component": "vn-worker-notifications",
|
||||||
|
"description": "Notifications",
|
||||||
|
"params": {
|
||||||
|
"worker": "$ctrl.worker"
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
"url": "/time-control?timestamp",
|
"url": "/time-control?timestamp",
|
||||||
"state": "worker.card.timeControl",
|
"state": "worker.card.timeControl",
|
||||||
|
|
Loading…
Reference in New Issue