Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4090-global_invoincing
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Vicent Llopis 2022-10-07 08:19:08 +02:00
commit 2e91698c1e
26 changed files with 408 additions and 84 deletions

View File

@ -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);
};
};

View File

@ -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();
});
});

View File

@ -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

View File

@ -48,6 +48,9 @@
},
"image": {
"type": "string"
},
"hasGrant": {
"type": "boolean"
}
},
"relations": {

View File

@ -0,0 +1 @@
ALTER TABLE `account`.`user` ADD hasGrant TINYINT(1) NOT NULL;

View File

View File

@ -2667,3 +2667,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;

View File

@ -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`
},

View File

@ -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);

View File

@ -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');
});
});
});

View File

@ -132,5 +132,6 @@
"Descanso diario 9h.": "Daily rest 9h.",
"Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.",
"Password does not meet requirements": "Password does not meet requirements",
"You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies",
"Not enough privileges to edit a client": "Not enough privileges to edit a client"
}

View File

@ -96,7 +96,7 @@
"This postcode already exists": "Este código postal ya existe",
"Concept cannot be blank": "El concepto no puede quedar en blanco",
"File doesn't exists": "El archivo no existe",
"You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies": "No tienes permisos para cambiar la zona o para esos parámetros hay más de una opción de envío, hable con las agencias",
"You don't have privileges to change the zone": "No tienes permisos para cambiar la zona o para esos parámetros hay más de una opción de envío, hable con las agencias",
"This ticket is already on weekly tickets": "Este ticket ya está en tickets programados",
"Ticket id cannot be blank": "El id de ticket no puede quedar en blanco",
"Weekday cannot be blank": "El día de la semana no puede quedar en blanco",

View File

@ -11,14 +11,6 @@
translate>
Delete
</vn-item>
<vn-item
ng-click="$ctrl.onChangeRole()"
name="changeRole"
vn-acl="hr"
vn-acl-action="remove"
translate>
Change role
</vn-item>
<vn-item
ng-if="::$root.user.id == $ctrl.id"
ng-click="$ctrl.onChangePassClick(true)"
@ -128,22 +120,6 @@
question="Are you sure you want to continue?"
message="User will be deactivated">
</vn-confirm>
<vn-dialog
vn-id="changeRole"
on-accept="$ctrl.onChangeRoleAccept()">
<tpl-body>
<vn-autocomplete
label="Role"
ng-model="$ctrl.newRole"
url="Roles"
vn-focus>
</vn-autocomplete>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Accept</button>
</tpl-buttons>
</vn-dialog>
<vn-dialog
vn-id="changePass"
on-accept="$ctrl.onPassChange()"

View File

@ -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 => {

View File

@ -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(() => {

View File

@ -18,3 +18,4 @@ import './roles';
import './ldap';
import './samba';
import './accounts';
import './privileges';

View File

@ -0,0 +1,42 @@
<mg-ajax path="Accounts/{{post.params.id}}/privileges" options="vnPost"></mg-ajax>
<vn-watcher
vn-id="watcher"
url="Accounts"
data="$ctrl.user"
id-value="$ctrl.$params.id"
form="form"
save="post">
</vn-watcher>
<form
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg" vn-focus>
<vn-vertical>
<vn-check
label="Has grant"
ng-model="$ctrl.user.hasGrant">
</vn-check>
</vn-vertical>
<vn-vertical
class="vn-mt-md">
<vn-autocomplete
label="Role"
ng-model="$ctrl.user.roleFk"
url="Roles">
</vn-autocomplete>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
<vn-button
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>

View File

@ -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
});

View File

@ -0,0 +1,2 @@
Privileges: Privilegios
Has grant: Tiene privilegios

View File

@ -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",

View File

@ -0,0 +1,57 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethodCtx('getItemTypeWorker', {
description: 'Returns the workers that appear in itemType',
accessType: 'READ',
accepts: [{
arg: 'filter',
type: 'Object',
description: 'Filter defining where and paginated data',
required: true
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/getItemTypeWorker`,
verb: 'GET'
}
});
Self.getItemTypeWorker = async(ctx, filter, options) => {
const myOptions = {};
const conn = Self.dataSource.connector;
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
const query =
`SELECT DISTINCT u.nickname
FROM itemType it
JOIN worker w ON w.id = it.workerFk
JOIN account.user u ON u.id = w.id`;
let stmt = new ParameterizedSQL(query);
if (filter.where) {
const value = filter.where.firstName;
const myFilter = {
where: {or: [
{'w.firstName': {like: `%${value}%`}},
{'w.lastName': {like: `%${value}%`}},
{'u.name': {like: `%${value}%`}},
{'u.nickname': {like: `%${value}%`}}
]}
};
stmt.merge(conn.makeSuffix(myFilter));
}
if (tx) await tx.commit();
return conn.executeStmt(stmt);
};
};

View File

@ -0,0 +1,21 @@
const models = require('vn-loopback/server/server').models;
describe('ticket-request getItemTypeWorker()', () => {
const ctx = {req: {accessToken: {userId: 18}}};
it('should return the buyer as result', async() => {
const filter = {where: {firstName: 'buyer'}};
const result = await models.TicketRequest.getItemTypeWorker(ctx, filter);
expect(result.length).toEqual(1);
});
it('should return the workers at itemType as result', async() => {
const filter = {};
const result = await models.TicketRequest.getItemTypeWorker(ctx, filter);
expect(result.length).toBeGreaterThan(1);
});
});

View File

@ -126,8 +126,7 @@ module.exports = Self => {
myOptions);
if (!zoneShipped || zoneShipped.zoneFk != args.zoneFk) {
const error = `You don't have privileges to change the zone or
for these parameters there are more than one shipping options, talk to agencies`;
const error = `You don't have privileges to change the zone`;
throw new UserError(error);
}

View File

@ -88,8 +88,7 @@ module.exports = Self => {
myOptions);
if (!zoneShipped || zoneShipped.zoneFk != args.zoneId) {
const error = `You don't have privileges to change the zone or
for these parameters there are more than one shipping options, talk to agencies`;
const error = `You don't have privileges to change the zone`;
throw new UserError(error);
}

View File

@ -5,6 +5,7 @@ module.exports = function(Self) {
require('../methods/ticket-request/filter')(Self);
require('../methods/ticket-request/deny')(Self);
require('../methods/ticket-request/confirm')(Self);
require('../methods/ticket-request/getItemTypeWorker')(Self);
Self.observe('before save', async function(ctx) {
if (ctx.isNewInstance) {

View File

@ -18,9 +18,8 @@
<vn-autocomplete
label="Buyer"
ng-model="$ctrl.ticketRequest.attenderFk"
url="Workers/activeWithRole"
url="TicketRequests/getItemTypeWorker"
show-field="nickname"
where="{role: {inq: ['logistic', 'buyer']}}"
search-function="{firstName: $search}">
</vn-autocomplete>
</vn-horizontal>