diff --git a/back/methods/collection/spec/setSaleQuantity.spec.js b/back/methods/collection/spec/setSaleQuantity.spec.js
index fdc1bce1a..8cd73205f 100644
--- a/back/methods/collection/spec/setSaleQuantity.spec.js
+++ b/back/methods/collection/spec/setSaleQuantity.spec.js
@@ -18,6 +18,7 @@ describe('setSaleQuantity()', () => {
it('should change quantity sale', async() => {
const tx = await models.Ticket.beginTransaction({});
+ spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: 100}))));
try {
const options = {transaction: tx};
diff --git a/back/methods/url/getByUser.js b/back/methods/url/getByUser.js
new file mode 100644
index 000000000..dd4805182
--- /dev/null
+++ b/back/methods/url/getByUser.js
@@ -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);
+ };
+};
diff --git a/back/methods/url/specs/getByUser.spec.js b/back/methods/url/specs/getByUser.spec.js
new file mode 100644
index 000000000..f6af6ec00
--- /dev/null
+++ b/back/methods/url/specs/getByUser.spec.js
@@ -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');
+ });
+});
diff --git a/back/methods/vn-user/update-user.js b/back/methods/vn-user/update-user.js
new file mode 100644
index 000000000..ddaae8548
--- /dev/null
+++ b/back/methods/vn-user/update-user.js
@@ -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});
+ };
+};
diff --git a/back/models/specs/vn-user.spec.js b/back/models/specs/vn-user.spec.js
index 3700b919a..8689a7854 100644
--- a/back/models/specs/vn-user.spec.js
+++ b/back/models/specs/vn-user.spec.js
@@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models;
+const ForbiddenError = require('vn-loopback/util/forbiddenError');
describe('loopback model VnUser', () => {
it('should return true if the user has the given role', async() => {
@@ -12,4 +13,42 @@ describe('loopback model VnUser', () => {
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());
+ }
+ });
+ });
});
diff --git a/back/models/url.js b/back/models/url.js
new file mode 100644
index 000000000..216d149ba
--- /dev/null
+++ b/back/models/url.js
@@ -0,0 +1,3 @@
+module.exports = Self => {
+ require('../methods/url/getByUser')(Self);
+};
diff --git a/back/models/vn-user.js b/back/models/vn-user.js
index cf210b61b..d7f54521f 100644
--- a/back/models/vn-user.js
+++ b/back/models/vn-user.js
@@ -1,6 +1,7 @@
const vnModel = require('vn-loopback/common/models/vn-model');
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
+const ForbiddenError = require('vn-loopback/util/forbiddenError');
module.exports = function(Self) {
vnModel(Self);
@@ -12,6 +13,7 @@ module.exports = function(Self) {
require('../methods/vn-user/privileges')(Self);
require('../methods/vn-user/validate-auth')(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');
@@ -178,45 +180,75 @@ module.exports = function(Self) {
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
.filter(acl => acl.property != 'changePassword');
- // FIXME: https://redmine.verdnatura.es/issues/5761
- // Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
- // if (!ctx.args || !ctx.args.data.email) return;
+ Self.userSecurity = async(ctx, userId, options) => {
+ const models = Self.app.models;
+ const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken;
+ const ctxToken = {req: {accessToken}};
- // 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(':');
+ if (userId === accessToken.userId) return;
- // class Mailer {
- // async send(verifyOptions, cb) {
- // const params = {
- // url: verifyOptions.verifyHref,
- // recipient: verifyOptions.to,
- // lang: ctx.req.getLocale()
- // };
+ const myOptions = {};
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
- // const email = new Email('email-verify', params);
- // email.send();
+ const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions);
+ 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 = {
- // 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
- // };
+ throw new ForbiddenError();
+ };
- // 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);
+ });
};
diff --git a/back/models/vn-user.json b/back/models/vn-user.json
index f5eb3ae0f..0f6daff5a 100644
--- a/back/models/vn-user.json
+++ b/back/models/vn-user.json
@@ -13,15 +13,12 @@
"type": "number",
"id": true
},
- "name": {
+ "name": {
"type": "string",
"required": true
},
"username": {
- "type": "string",
- "mysql": {
- "columnName": "name"
- }
+ "type": "string"
},
"roleFk": {
"type": "number",
@@ -38,6 +35,12 @@
"active": {
"type": "boolean"
},
+ "email": {
+ "type": "string"
+ },
+ "emailVerified": {
+ "type": "boolean"
+ },
"created": {
"type": "date"
},
@@ -137,7 +140,8 @@
"image",
"hasGrant",
"realm",
- "email"
+ "email",
+ "emailVerified"
]
}
}
diff --git a/db/changes/234201/00-account_acl.sql b/db/changes/234201/00-account_acl.sql
new file mode 100644
index 000000000..8dfe1d1ec
--- /dev/null
+++ b/db/changes/234201/00-account_acl.sql
@@ -0,0 +1,7 @@
+INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
+ VALUES
+ ('VnUser', 'higherPrivileges', '*', 'ALLOW', 'ROLE', 'itManagement'),
+ ('VnUser', 'mediumPrivileges', '*', 'ALLOW', 'ROLE', 'hr'),
+ ('VnUser', 'updateUser', '*', 'ALLOW', 'ROLE', 'employee');
+
+ALTER TABLE `account`.`user` ADD `username` varchar(30) AS (name) VIRTUAL;
diff --git a/db/changes/234201/00-aclSetPassword.sql b/db/changes/234201/00-aclSetPassword.sql
new file mode 100644
index 000000000..44b3e9de0
--- /dev/null
+++ b/db/changes/234201/00-aclSetPassword.sql
@@ -0,0 +1,4 @@
+INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
+ VALUES ('Worker','setPassword','*','ALLOW','ROLE','employee');
+
+
diff --git a/db/changes/234201/00-aclUrlHedera.sql b/db/changes/234201/00-aclUrlHedera.sql
new file mode 100644
index 000000000..79d9fb4c8
--- /dev/null
+++ b/db/changes/234201/00-aclUrlHedera.sql
@@ -0,0 +1,7 @@
+INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
+ VALUES
+ ('hedera', 'test', 'https://test-shop.verdnatura.es/'),
+ ('hedera', 'production', 'https://shop.verdnatura.es/');
+
+INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId)
+ VALUES('Url', 'getByUser', 'READ', 'ALLOW', 'ROLE', '$everyone');
diff --git a/db/changes/234201/00-zoneIncluded.sql b/db/changes/234201/00-zoneIncluded.sql
new file mode 100644
index 000000000..12d4058cf
--- /dev/null
+++ b/db/changes/234201/00-zoneIncluded.sql
@@ -0,0 +1,26 @@
+ALTER TABLE `vn`.`zoneIncluded`
+ ADD COLUMN `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
+ DROP PRIMARY KEY,
+ DROP FOREIGN KEY `zoneFk2`,
+ DROP FOREIGN KEY `zoneGeoFk2`,
+ DROP KEY `geoFk_idx`,
+ ADD PRIMARY KEY (`id`),
+ ADD CONSTRAINT `zoneIncluded_FK_1` FOREIGN KEY (zoneFk) REFERENCES `vn`.`zone`(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ ADD CONSTRAINT `zoneIncluded_FK_2` FOREIGN KEY (geoFk) REFERENCES `vn`.`zoneGeo`(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ ADD CONSTRAINT `unique_zone_geo` UNIQUE (`zoneFk`, `geoFk`);
+
+DROP TRIGGER IF EXISTS `vn`.`zoneIncluded_afterDelete`;
+USE `vn`;
+
+DELIMITER $$
+CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`zoneIncluded_afterDelete`
+ AFTER DELETE ON `zoneIncluded`
+ FOR EACH ROW
+BEGIN
+ INSERT INTO zoneLog
+ SET `action` = 'delete',
+ `changedModel` = 'zoneIncluded',
+ `changedModelId` = OLD.zoneFk,
+ `userFk` = account.myUser_getId();
+END$$
+DELIMITER ;
diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index 69c22013f..a062168c9 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -2867,6 +2867,7 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES
('lilium', 'development', 'http://localhost:9000/#/'),
+ ('hedera', 'development', 'http://localhost:9090/'),
('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
diff --git a/db/dump/structure.sql b/db/dump/structure.sql
index 08df0541c..b242821fc 100644
--- a/db/dump/structure.sql
+++ b/db/dump/structure.sql
@@ -30434,6 +30434,7 @@ CREATE TABLE `item` (
`editorFk` int(10) unsigned DEFAULT NULL,
`recycledPlastic` int(11) DEFAULT NULL,
`nonRecycledPlastic` int(11) DEFAULT NULL,
+ `minQuantity` int(10) unsigned DEFAULT NULL COMMENT 'Cantidad mínima para una línea de venta',
PRIMARY KEY (`id`),
UNIQUE KEY `item_supplyResponseFk_idx` (`supplyResponseFk`),
KEY `Color` (`inkFk`),
diff --git a/loopback/locale/en.json b/loopback/locale/en.json
index 645a874e8..f61226e9e 100644
--- a/loopback/locale/en.json
+++ b/loopback/locale/en.json
@@ -189,5 +189,6 @@
"The sales do not exists": "The sales do not exists",
"Ticket without Route": "Ticket without route",
"Booking completed": "Booking complete",
- "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation"
-}
+ "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation",
+ "You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets"
+}
\ No newline at end of file
diff --git a/loopback/locale/es.json b/loopback/locale/es.json
index 6e478c000..7fb7c51a0 100644
--- a/loopback/locale/es.json
+++ b/loopback/locale/es.json
@@ -320,5 +320,7 @@
"The response is not a PDF": "La respuesta no es un PDF",
"Ticket without Route": "Ticket sin ruta",
"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",
+ "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina"
}
diff --git a/modules/account/back/models/mail-forward.json b/modules/account/back/models/mail-forward.json
index edef1bf08..874810b7a 100644
--- a/modules/account/back/models/mail-forward.json
+++ b/modules/account/back/models/mail-forward.json
@@ -21,5 +21,16 @@
"model": "VnUser",
"foreignKey": "account"
}
- }
+ },
+ "acls": [{
+ "accessType": "READ",
+ "principalType": "ROLE",
+ "principalId": "$owner",
+ "permission": "ALLOW"
+ }, {
+ "accessType": "WRITE",
+ "principalType": "ROLE",
+ "principalId": "$owner",
+ "permission": "ALLOW"
+ }]
}
diff --git a/modules/account/front/basic-data/index.html b/modules/account/front/basic-data/index.html
index 6f757753e..9fd3506fe 100644
--- a/modules/account/front/basic-data/index.html
+++ b/modules/account/front/basic-data/index.html
@@ -1,9 +1,9 @@
+