diff --git a/back/models/notificationAcl.json b/back/models/notificationAcl.json index e3e97f52d..a20187961 100644 --- a/back/models/notificationAcl.json +++ b/back/models/notificationAcl.json @@ -6,6 +6,16 @@ "table": "util.notificationAcl" } }, + "properties":{ + "notificationFk": { + "id": true, + "type": "number" + }, + "roleFk":{ + "id": true, + "type": "number" + } + }, "relations": { "notification": { "type": "belongsTo", diff --git a/back/models/notificationSubscription.js b/back/models/notificationSubscription.js new file mode 100644 index 000000000..f1b2811fa --- /dev/null +++ b/back/models/notificationSubscription.js @@ -0,0 +1,62 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.observe('before save', async function(ctx) { + const models = Self.app.models; + 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) + throw new UserError('You dont have permission to modify this user'); + }); + + Self.remoteMethod('deleteNotification', { + description: 'Deletes a notification subscription', + accepts: [ + { + arg: 'ctx', + type: 'object', + http: {source: 'context'} + }, + { + arg: 'notificationId', + type: 'number', + required: true + }, + ], + returns: { + type: 'object', + root: true + }, + http: { + verb: 'POST', + path: '/deleteNotification' + } + }); + + Self.deleteNotification = async function(ctx, notificationId) { + 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) + throw new UserError('You dont have permission to modify this user'); + + await models.NotificationSubscription.destroyById(notificationId); + }; + + async function getUserToModify(notificationId, userFk, models) { + let userToModify = userFk; + if (notificationId) { + const subscription = await models.NotificationSubscription.findById(notificationId); + userToModify = subscription.userFk; + } + return await models.Worker.findOne({ + fields: ['id', 'bossFk'], + where: { + id: userToModify + } + }); + } +}; diff --git a/back/models/notificationSubscription.json b/back/models/notificationSubscription.json index 43fa6db27..a640e0917 100644 --- a/back/models/notificationSubscription.json +++ b/back/models/notificationSubscription.json @@ -7,15 +7,18 @@ } }, "properties": { - "notificationFk": { + "id": { "type": "number", "id": true, - "description": "Identifier" + "description": "Primary key" + }, + "notificationFk": { + "type": "number", + "description": "Foreign key to Notification" }, "userFk": { "type": "number", - "id": true, - "description": "Identifier" + "description": "Foreign key to Account" } }, "relations": { diff --git a/back/models/specs/notificationSubscription.spec.js b/back/models/specs/notificationSubscription.spec.js new file mode 100644 index 000000000..c7f37abed --- /dev/null +++ b/back/models/specs/notificationSubscription.spec.js @@ -0,0 +1,74 @@ +const models = require('vn-loopback/server/server').models; + +describe('loopback model NotificationSubscription', () => { + it('Should fail to delete a notification if the user is not editing itself or a subordinate', async() => { + 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; + + try { + await models.NotificationSubscription.deleteNotification(ctx, notification.id, options); + } catch (e) { + error = e; + } + + expect(error.message).toContain('You dont have permission to modify this user'); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('Should delete a notification if the user is editing itself', async() => { + const tx = await models.NotificationSubscription.beginTransaction({}); + + try { + const options = {transaction: tx}; + const user = 9; + const notificationSubscriptionId = 4; + 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; + } + }); + + it('Should delete a notification if the user is editing a subordinate', async() => { + 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; + } + }); +}); + diff --git a/db/changes/230401/00-acl_notifications.sql b/db/changes/230401/00-acl_notifications.sql new file mode 100644 index 000000000..ab40b16a5 --- /dev/null +++ b/db/changes/230401/00-acl_notifications.sql @@ -0,0 +1,4 @@ +INSERT INTO `salix`.`ACL` (model,property,accessType,principalId) + VALUES + ('NotificationSubscription','*','*','employee'), + ('NotificationAcl','*','READ','employee'); diff --git a/db/changes/230401/00-uniqueKeyNotificationSubscription.sql b/db/changes/230401/00-uniqueKeyNotificationSubscription.sql new file mode 100644 index 000000000..623ecf770 --- /dev/null +++ b/db/changes/230401/00-uniqueKeyNotificationSubscription.sql @@ -0,0 +1,4 @@ +ALTER TABLE + `util`.`notificationSubscription` +ADD + CONSTRAINT `notificationSubscription_UN` UNIQUE KEY (`notificationFk`, `userFk`); \ No newline at end of file diff --git a/db/changes/230401/01-alter_notSubs.sql b/db/changes/230401/01-alter_notSubs.sql new file mode 100644 index 000000000..07ea7c2bf --- /dev/null +++ b/db/changes/230401/01-alter_notSubs.sql @@ -0,0 +1,7 @@ +ALTER TABLE `util`.`notificationSubscription` +ADD `id` int(11) auto_increment NULL, +DROP PRIMARY KEY, +ADD CONSTRAINT PRIMARY KEY (`id`); + +ALTER TABLE `util`.`notificationSubscription` +ADD KEY `notificationSubscription_ibfk_1` (`notificationFk`); diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 218fcb9ca..bb4f00ff5 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -1954,10 +1954,6 @@ INSERT INTO `vn`.`workerBusinessType` (`id`, `name`, `isFullTime`, `isPermanent` (100, 'INDEFINIDO A TIEMPO COMPLETO', 1, 1, 1), (109, 'CONVERSION DE TEMPORAL EN INDEFINIDO T.COMPLETO', 1, 1, 1); -INSERT INTO `vn`.`businessCategory` (`id`, `description`, `rate`) - VALUES - (1, 'basic employee', 1); - UPDATE `vn`.`business` b SET `rate` = 7, `workerBusinessCategoryFk` = 1, @@ -2705,7 +2701,10 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`) VALUES (1, 1109), (1, 1110), - (3, 1109); + (3, 1109), + (1,9), + (1,3); + INSERT INTO `vn`.`routeConfig` (`id`, `defaultWorkCenterFk`) VALUES diff --git a/front/core/components/smart-table/index.js b/front/core/components/smart-table/index.js index 770dcdf32..ad9883950 100644 --- a/front/core/components/smart-table/index.js +++ b/front/core/components/smart-table/index.js @@ -478,8 +478,8 @@ export default class SmartTable extends Component { const params = {q: JSON.stringify(stateFilter)}; - this.$state.go(this.$state.current.name, params, {location: 'replace'}); - this.refresh(); + this.$state.go(this.$state.current.name, params, {location: 'replace'}) + .then(() => this.refresh()); } applySort() { @@ -499,8 +499,8 @@ export default class SmartTable extends Component { stateFilter.tableOrder = order; const params = {q: JSON.stringify(stateFilter)}; - this.$state.go(this.$state.current.name, params, {location: 'replace'}); - this.refresh(); + this.$state.go(this.$state.current.name, params, {location: 'replace'}) + .then(() => this.refresh()); } filterSanitizer(field) { @@ -589,7 +589,7 @@ export default class SmartTable extends Component { refresh() { this.isRefreshing = true; this.model.refresh() - .then(() => this.isRefreshing = false); + .finally(() => this.isRefreshing = false); } } diff --git a/front/core/components/smart-table/index.spec.js b/front/core/components/smart-table/index.spec.js index 5fd4c33b7..5a1be98e1 100644 --- a/front/core/components/smart-table/index.spec.js +++ b/front/core/components/smart-table/index.spec.js @@ -160,7 +160,7 @@ describe('Component smartTable', () => { describe('applySort()', () => { it('should call the $state go and model refresh without making changes on the model order', () => { controller.$state = { - go: jest.fn(), + go: jest.fn().mockReturnValue(new Promise(resolve => resolve())), current: { name: 'section' } @@ -171,13 +171,12 @@ describe('Component smartTable', () => { expect(controller.model.order).toBeUndefined(); expect(controller.$state.go).toHaveBeenCalled(); - expect(controller.refresh).toHaveBeenCalled(); }); it('should call the $state go and model refresh after setting model order according to the controller sortCriteria', () => { const orderBy = {field: 'myField', sortType: 'ASC'}; controller.$state = { - go: jest.fn(), + go: jest.fn().mockReturnValue(new Promise(resolve => resolve())), current: { name: 'section' } @@ -190,7 +189,6 @@ describe('Component smartTable', () => { expect(controller.model.order).toEqual(`${orderBy.field} ${orderBy.sortType}`); expect(controller.$state.go).toHaveBeenCalled(); - expect(controller.refresh).toHaveBeenCalled(); }); }); @@ -293,12 +291,10 @@ describe('Component smartTable', () => { controller.$inputsScope = { searchProps: {} }; - jest.spyOn(controller, 'refresh'); controller.defaultFilter(); expect(controller.model.addFilter).toHaveBeenCalled(); - expect(controller.refresh).toHaveBeenCalled(); }); });