diff --git a/back/methods/account/privileges.js b/back/methods/account/privileges.js
new file mode 100644
index 000000000..df421125e
--- /dev/null
+++ b/back/methods/account/privileges.js
@@ -0,0 +1,58 @@
+const UserError = require('vn-loopback/util/user-error');
+
+module.exports = Self => {
+ Self.remoteMethodCtx('privileges', {
+ description: 'Change role and hasGrant if user has privileges',
+ accepts: [
+ {
+ arg: 'id',
+ type: 'number',
+ required: true,
+ description: 'The user id',
+ http: {source: 'path'}
+ },
+ {
+ arg: 'roleFk',
+ type: 'number',
+ description: 'The new role for user',
+ },
+ {
+ arg: 'hasGrant',
+ type: 'boolean',
+ description: 'Whether to has grant'
+ }
+ ],
+ http: {
+ path: `/:id/privileges`,
+ verb: 'POST'
+ }
+ });
+
+ Self.privileges = async function(ctx, id, roleFk, hasGrant, options) {
+ const models = Self.app.models;
+ const userId = ctx.req.accessToken.userId;
+
+ const myOptions = {};
+
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ const user = await models.Account.findById(userId, null, myOptions);
+
+ if (!user.hasGrant)
+ throw new UserError(`You don't have enough privileges`);
+
+ const userToUpdate = await models.Account.findById(id);
+ if (hasGrant != null)
+ return await userToUpdate.updateAttribute('hasGrant', hasGrant, myOptions);
+ if (!roleFk) return;
+
+ const role = await models.Role.findById(roleFk, null, myOptions);
+ const hasRole = await models.Account.hasRole(userId, role.name, myOptions);
+
+ if (!hasRole)
+ throw new UserError(`You don't have enough privileges`);
+
+ await userToUpdate.updateAttribute('roleFk', roleFk, myOptions);
+ };
+};
diff --git a/back/methods/account/specs/privileges.spec.js b/back/methods/account/specs/privileges.spec.js
new file mode 100644
index 000000000..137c08671
--- /dev/null
+++ b/back/methods/account/specs/privileges.spec.js
@@ -0,0 +1,99 @@
+const models = require('vn-loopback/server/server').models;
+
+describe('account privileges()', () => {
+ const employeeId = 1;
+ const developerId = 9;
+ const sysadminId = 66;
+ const bruceWayneId = 1101;
+
+ it('should throw an error when user not has privileges', async() => {
+ const ctx = {req: {accessToken: {userId: developerId}}};
+ const tx = await models.Account.beginTransaction({});
+
+ let error;
+ try {
+ const options = {transaction: tx};
+
+ await models.Account.privileges(ctx, employeeId, null, true, options);
+
+ await tx.rollback();
+ } catch (e) {
+ error = e;
+ await tx.rollback();
+ }
+
+ expect(error.message).toContain(`You don't have enough privileges`);
+ });
+
+ it('should throw an error when user has privileges but not has the role', async() => {
+ const ctx = {req: {accessToken: {userId: sysadminId}}};
+ const tx = await models.Account.beginTransaction({});
+
+ let error;
+ try {
+ const options = {transaction: tx};
+
+ const root = await models.Role.findOne({
+ where: {
+ name: 'root'
+ }
+ }, options);
+ await models.Account.privileges(ctx, employeeId, root.id, null, options);
+
+ await tx.rollback();
+ } catch (e) {
+ error = e;
+ await tx.rollback();
+ }
+
+ expect(error.message).toContain(`You don't have enough privileges`);
+ });
+
+ it('should change role', async() => {
+ const ctx = {req: {accessToken: {userId: sysadminId}}};
+ const tx = await models.Account.beginTransaction({});
+
+ const options = {transaction: tx};
+ const agency = await models.Role.findOne({
+ where: {
+ name: 'agency'
+ }
+ }, options);
+
+ let error;
+ let result;
+ try {
+ await models.Account.privileges(ctx, bruceWayneId, agency.id, null, options);
+ result = await models.Account.findById(bruceWayneId, null, options);
+
+ await tx.rollback();
+ } catch (e) {
+ error = e;
+ await tx.rollback();
+ }
+
+ expect(error).not.toBeDefined();
+ expect(result.roleFk).toEqual(agency.id);
+ });
+
+ it('should change hasGrant', async() => {
+ const ctx = {req: {accessToken: {userId: sysadminId}}};
+ const tx = await models.Account.beginTransaction({});
+
+ let error;
+ let result;
+ try {
+ const options = {transaction: tx};
+ await models.Account.privileges(ctx, bruceWayneId, null, true, options);
+ result = await models.Account.findById(bruceWayneId, null, options);
+
+ await tx.rollback();
+ } catch (e) {
+ error = e;
+ await tx.rollback();
+ }
+
+ expect(error).not.toBeDefined();
+ expect(result.hasGrant).toBeTruthy();
+ });
+});
diff --git a/back/models/account.js b/back/models/account.js
index ba703c68d..f74052b5c 100644
--- a/back/models/account.js
+++ b/back/models/account.js
@@ -7,6 +7,7 @@ module.exports = Self => {
require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self);
require('../methods/account/validate-token')(Self);
+ require('../methods/account/privileges')(Self);
// Validations
@@ -77,7 +78,7 @@ module.exports = Self => {
`SELECT r.name
FROM account.user u
JOIN account.roleRole rr ON rr.role = u.role
- JOIN account.role r ON r.id = rr.inheritsFrom
+ JOIN account.role r ON r.id = rr.inheritsFrom
WHERE u.id = ?`, [userId], options);
let roles = [];
diff --git a/back/models/account.json b/back/models/account.json
index 5f0b05f9b..c25cd532d 100644
--- a/back/models/account.json
+++ b/back/models/account.json
@@ -48,6 +48,9 @@
},
"image": {
"type": "string"
+ },
+ "hasGrant": {
+ "type": "boolean"
}
},
"relations": {
diff --git a/db/changes/10490-august/00-user_hasGrant.sql b/db/changes/10490-august/00-user_hasGrant.sql
new file mode 100644
index 000000000..60d1273d8
--- /dev/null
+++ b/db/changes/10490-august/00-user_hasGrant.sql
@@ -0,0 +1 @@
+ALTER TABLE `account`.`user` ADD hasGrant TINYINT(1) NOT NULL;
diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index 73a6ef687..7e59c1a54 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -2663,3 +2663,7 @@ INSERT INTO `vn`.`collection` (`id`, `created`, `workerFk`, `stateFk`, `itemPack
INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `level`, `wagon`, `smartTagFk`, `usedShelves`, `itemCount`, `liters`)
VALUES
(9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL);
+
+UPDATE `account`.`user`
+ SET `hasGrant` = 1
+ WHERE `id` = 66;
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index df9088140..fedcaf5f9 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -51,14 +51,12 @@ export default {
accountDescriptor: {
menuButton: 'vn-user-descriptor vn-icon-button[icon="more_vert"]',
deleteAccount: '.vn-menu [name="deleteUser"]',
- changeRole: '.vn-menu [name="changeRole"]',
setPassword: '.vn-menu [name="setPassword"]',
activateAccount: '.vn-menu [name="enableAccount"]',
activateUser: '.vn-menu [name="activateUser"]',
deactivateUser: '.vn-menu [name="deactivateUser"]',
newPassword: 'vn-textfield[ng-model="$ctrl.newPassword"]',
repeatPassword: 'vn-textfield[ng-model="$ctrl.repeatPassword"]',
- newRole: 'vn-autocomplete[ng-model="$ctrl.newRole"]',
activeAccountIcon: 'vn-icon[icon="contact_mail"]',
activeUserIcon: 'vn-icon[icon="icon-disabled"]',
acceptButton: 'button[response="accept"]',
@@ -143,6 +141,11 @@ export default {
verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]',
save: 'vn-account-samba vn-submit'
},
+ accountPrivileges: {
+ checkHasGrant: 'vn-user-privileges vn-check[ng-model="$ctrl.user.hasGrant"]',
+ role: 'vn-user-privileges vn-autocomplete[ng-model="$ctrl.user.roleFk"]',
+ save: 'vn-user-privileges vn-submit'
+ },
clientsIndex: {
createClientButton: `vn-float-button`
},
diff --git a/e2e/paths/14-account/01_create_and_basic_data.spec.js b/e2e/paths/14-account/01_create_and_basic_data.spec.js
index 0400fb99e..54e4d1f12 100644
--- a/e2e/paths/14-account/01_create_and_basic_data.spec.js
+++ b/e2e/paths/14-account/01_create_and_basic_data.spec.js
@@ -62,27 +62,6 @@ describe('Account create and basic data path', () => {
});
describe('Descriptor option', () => {
- describe('Edit role', () => {
- it('should edit the role using the descriptor menu', async() => {
- await page.waitToClick(selectors.accountDescriptor.menuButton);
- await page.waitToClick(selectors.accountDescriptor.changeRole);
- await page.autocompleteSearch(selectors.accountDescriptor.newRole, 'adminBoss');
- await page.waitToClick(selectors.accountDescriptor.acceptButton);
- const message = await page.waitForSnackbar();
-
- expect(message.text).toContain('Role changed succesfully!');
- });
-
- it('should reload the roles section to see now there are more roles', async() => {
- // when role updates db takes time to return changes, without this timeout the result would have been 3
- await page.waitForTimeout(1000);
- await page.reloadSection('account.card.roles');
- const rolesCount = await page.countElement(selectors.accountRoles.anyResult);
-
- expect(rolesCount).toEqual(61);
- });
- });
-
describe('activate account', () => {
it(`should check the active account icon isn't present in the descriptor`, async() => {
await page.waitForNumberOfElements(selectors.accountDescriptor.activeAccountIcon, 0);
diff --git a/e2e/paths/14-account/09_privileges.spec.js b/e2e/paths/14-account/09_privileges.spec.js
new file mode 100644
index 000000000..71e9345a8
--- /dev/null
+++ b/e2e/paths/14-account/09_privileges.spec.js
@@ -0,0 +1,86 @@
+import selectors from '../../helpers/selectors.js';
+import getBrowser from '../../helpers/puppeteer';
+
+describe('Account privileges path', () => {
+ let browser;
+ let page;
+
+ beforeAll(async() => {
+ browser = await getBrowser();
+ page = browser.page;
+ await page.loginAndModule('developer', 'account');
+ await page.accessToSearchResult('1101');
+ await page.accessToSection('account.card.privileges');
+ });
+
+ afterAll(async() => {
+ await browser.close();
+ });
+
+ describe('as developer', () => {
+ it('should throw error when give privileges', async() => {
+ await page.waitToClick(selectors.accountPrivileges.checkHasGrant);
+ await page.waitToClick(selectors.accountPrivileges.save);
+
+ const message = await page.waitForSnackbar();
+
+ expect(message.text).toContain(`You don't have enough privileges`);
+ });
+
+ it('should throw error when change role', async() => {
+ await page.autocompleteSearch(selectors.accountPrivileges.role, 'employee');
+ await page.waitToClick(selectors.accountPrivileges.save);
+
+ const message = await page.waitForSnackbar();
+
+ expect(message.text).toContain(`You don't have enough privileges`);
+ });
+ });
+
+ describe('as sysadmin', () => {
+ beforeAll(async() => {
+ await page.loginAndModule('sysadmin', 'account');
+ await page.accessToSearchResult('9');
+ await page.accessToSection('account.card.privileges');
+ });
+
+ it('should give privileges', async() => {
+ await page.waitToClick(selectors.accountPrivileges.checkHasGrant);
+ await page.waitToClick(selectors.accountPrivileges.save);
+ const message = await page.waitForSnackbar();
+
+ await page.reloadSection('account.card.privileges');
+ const result = await page.checkboxState(selectors.accountPrivileges.checkHasGrant);
+
+ expect(message.text).toContain(`Data saved!`);
+ expect(result).toBe('checked');
+ });
+
+ it('should change role', async() => {
+ await page.autocompleteSearch(selectors.accountPrivileges.role, 'employee');
+ await page.waitToClick(selectors.accountPrivileges.save);
+ const message = await page.waitForSnackbar();
+
+ await page.reloadSection('account.card.privileges');
+ const result = await page.waitToGetProperty(selectors.accountPrivileges.role, 'value');
+
+ expect(message.text).toContain(`Data saved!`);
+ expect(result).toContain('employee');
+ });
+ });
+
+ describe('as developer again', () => {
+ it('should remove privileges', async() => {
+ await page.accessToSearchResult('9');
+ await page.accessToSection('account.card.privileges');
+
+ await page.waitToClick(selectors.accountPrivileges.checkHasGrant);
+ await page.waitToClick(selectors.accountPrivileges.save);
+
+ await page.reloadSection('account.card.privileges');
+ const result = await page.checkboxState(selectors.accountPrivileges.checkHasGrant);
+
+ expect(result).toBe('unchecked');
+ });
+ });
+});
diff --git a/modules/account/front/descriptor/index.html b/modules/account/front/descriptor/index.html
index c709c1ec0..7a7ba43f3 100644
--- a/modules/account/front/descriptor/index.html
+++ b/modules/account/front/descriptor/index.html
@@ -11,14 +11,6 @@
translate>
Delete
-
- Change role
-
-
@@ -128,22 +120,6 @@
question="Are you sure you want to continue?"
message="User will be deactivated">
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/modules/account/front/descriptor/index.js b/modules/account/front/descriptor/index.js
index 3f27b1f76..b802b2349 100644
--- a/modules/account/front/descriptor/index.js
+++ b/modules/account/front/descriptor/index.js
@@ -30,20 +30,6 @@ class Controller extends Descriptor {
.then(() => this.vnApp.showSuccess(this.$t('User removed')));
}
- onChangeRole() {
- this.newRole = this.user.role.id;
- this.$.changeRole.show();
- }
-
- onChangeRoleAccept() {
- const params = {roleFk: this.newRole};
- return this.$http.patch(`Accounts/${this.id}`, params)
- .then(() => {
- this.emit('change');
- this.vnApp.showSuccess(this.$t('Role changed succesfully!'));
- });
- }
-
onChangePassClick(askOldPass) {
this.$http.get('UserPasswords/findOne')
.then(res => {
diff --git a/modules/account/front/descriptor/index.spec.js b/modules/account/front/descriptor/index.spec.js
index 8ee67a304..f5e7aa7d4 100644
--- a/modules/account/front/descriptor/index.spec.js
+++ b/modules/account/front/descriptor/index.spec.js
@@ -30,17 +30,6 @@ describe('component vnUserDescriptor', () => {
});
});
- describe('onChangeRoleAccept()', () => {
- it('should call backend method to change role', () => {
- $httpBackend.expectPATCH('Accounts/1').respond();
- controller.onChangeRoleAccept();
- $httpBackend.flush();
-
- expect(controller.vnApp.showSuccess).toHaveBeenCalled();
- expect(controller.emit).toHaveBeenCalledWith('change');
- });
- });
-
describe('onPassChange()', () => {
it('should throw an error when password is empty', () => {
expect(() => {
diff --git a/modules/account/front/index.js b/modules/account/front/index.js
index 50e04c9fa..0cd0c4955 100644
--- a/modules/account/front/index.js
+++ b/modules/account/front/index.js
@@ -18,3 +18,4 @@ import './roles';
import './ldap';
import './samba';
import './accounts';
+import './privileges';
diff --git a/modules/account/front/privileges/index.html b/modules/account/front/privileges/index.html
new file mode 100644
index 000000000..e3e44898a
--- /dev/null
+++ b/modules/account/front/privileges/index.html
@@ -0,0 +1,42 @@
+
+
+
+
diff --git a/modules/account/front/privileges/index.js b/modules/account/front/privileges/index.js
new file mode 100644
index 000000000..00ba772df
--- /dev/null
+++ b/modules/account/front/privileges/index.js
@@ -0,0 +1,9 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {}
+
+ngModule.component('vnUserPrivileges', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/privileges/locale/es.yml b/modules/account/front/privileges/locale/es.yml
new file mode 100644
index 000000000..f7330e1be
--- /dev/null
+++ b/modules/account/front/privileges/locale/es.yml
@@ -0,0 +1,2 @@
+Privileges: Privilegios
+Has grant: Tiene privilegios
diff --git a/modules/account/front/routes.json b/modules/account/front/routes.json
index 66b26f427..b96c931c9 100644
--- a/modules/account/front/routes.json
+++ b/modules/account/front/routes.json
@@ -19,7 +19,8 @@
{"state": "account.card.basicData", "icon": "settings"},
{"state": "account.card.roles", "icon": "group"},
{"state": "account.card.mailForwarding", "icon": "forward"},
- {"state": "account.card.aliases", "icon": "email"}
+ {"state": "account.card.aliases", "icon": "email"},
+ {"state": "account.card.privileges", "icon": "badge"}
],
"role": [
{"state": "account.role.card.basicData", "icon": "settings"},
@@ -99,6 +100,13 @@
"description": "Mail aliases",
"acl": ["marketing", "hr"]
},
+ {
+ "url": "/privileges",
+ "state": "account.card.privileges",
+ "component": "vn-user-privileges",
+ "description": "Privileges",
+ "acl": ["hr"]
+ },
{
"url": "/role?q",
"state": "account.role",
@@ -249,4 +257,4 @@
"acl": ["developer"]
}
]
-}
\ No newline at end of file
+}