Merge pull request '6067-vnUser_privileges_and_verifyEmail' (!1764) from 6067-vnUser_privileges_and_verifyEmail into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #1764
Reviewed-by: Juan Ferrer <juan@verdnatura.es>
This commit is contained in:
Alex Moreno 2023-10-19 12:13:16 +00:00
commit 44b2155c2b
14 changed files with 248 additions and 48 deletions

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const ForbiddenError = require('vn-loopback/util/forbiddenError');
describe('loopback model VnUser', () => { describe('loopback model VnUser', () => {
it('should return true if the user has the given role', async() => { it('should return true if the user has the given role', async() => {
@ -12,4 +13,42 @@ describe('loopback model VnUser', () => {
expect(result).toBeFalsy(); 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());
}
});
});
}); });

3
back/models/url.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/url/getByUser')(Self);
};

View File

@ -1,6 +1,7 @@
const vnModel = require('vn-loopback/common/models/vn-model'); const vnModel = require('vn-loopback/common/models/vn-model');
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print'); const {Email} = require('vn-print');
const ForbiddenError = require('vn-loopback/util/forbiddenError');
module.exports = function(Self) { module.exports = function(Self) {
vnModel(Self); vnModel(Self);
@ -12,6 +13,7 @@ module.exports = function(Self) {
require('../methods/vn-user/privileges')(Self); require('../methods/vn-user/privileges')(Self);
require('../methods/vn-user/validate-auth')(Self); require('../methods/vn-user/validate-auth')(Self);
require('../methods/vn-user/renew-token')(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'); 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 Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
.filter(acl => acl.property != 'changePassword'); .filter(acl => acl.property != 'changePassword');
// FIXME: https://redmine.verdnatura.es/issues/5761 Self.userSecurity = async(ctx, userId, options) => {
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => { const models = Self.app.models;
// if (!ctx.args || !ctx.args.data.email) return; const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken;
const ctxToken = {req: {accessToken}};
// const loopBackContext = LoopBackContext.getCurrentContext(); if (userId === accessToken.userId) return;
// const httpCtx = {req: loopBackContext.active};
// const httpRequest = httpCtx.req.http.req;
// const headers = httpRequest.headers;
// const origin = headers.origin;
// const url = origin.split(':');
// class Mailer { const myOptions = {};
// async send(verifyOptions, cb) { if (typeof options == 'object')
// const params = { Object.assign(myOptions, options);
// url: verifyOptions.verifyHref,
// recipient: verifyOptions.to,
// lang: ctx.req.getLocale()
// };
// const email = new Email('email-verify', params); const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions);
// email.send(); 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 = { throw new ForbiddenError();
// 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
// };
// 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);
});
}; };

View File

@ -13,15 +13,12 @@
"type": "number", "type": "number",
"id": true "id": true
}, },
"name": { "name": {
"type": "string", "type": "string",
"required": true "required": true
}, },
"username": { "username": {
"type": "string", "type": "string"
"mysql": {
"columnName": "name"
}
}, },
"roleFk": { "roleFk": {
"type": "number", "type": "number",

View File

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

View File

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

View File

@ -2867,6 +2867,7 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES VALUES
('lilium', 'development', 'http://localhost:9000/#/'), ('lilium', 'development', 'http://localhost:9000/#/'),
('hedera', 'development', 'http://localhost:9090/'),
('salix', 'development', 'http://localhost:5000/#!/'); ('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`) INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)

View File

@ -21,5 +21,16 @@
"model": "VnUser", "model": "VnUser",
"foreignKey": "account" "foreignKey": "account"
} }
} },
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
}, {
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
}]
} }

View File

@ -1,9 +1,9 @@
<mg-ajax path="VnUsers/{{patch.params.id}}/update-user" options="vnPatch"></mg-ajax>
<vn-watcher <vn-watcher
vn-id="watcher" vn-id="watcher"
url="VnUsers"
data="$ctrl.user" data="$ctrl.user"
id-value="$ctrl.$params.id" form="form"
form="form"> save="patch">
</vn-watcher> </vn-watcher>
<form <form
name="form" name="form"
@ -12,18 +12,18 @@
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-vertical> <vn-vertical>
<vn-textfield <vn-textfield
label="User" label="User"
ng-model="$ctrl.user.name" ng-model="$ctrl.user.name"
rule="VnUser" rule="VnUser"
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Nickname" label="Nickname"
ng-model="$ctrl.user.nickname" ng-model="$ctrl.user.nickname"
rule="VnUser"> rule="VnUser">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Personal email" label="Personal email"
ng-model="$ctrl.user.email" ng-model="$ctrl.user.email"
rule="VnUser"> rule="VnUser">
</vn-textfield> </vn-textfield>

View File

@ -18,5 +18,8 @@ ngModule.component('vnUserBasicData', {
controller: Controller, controller: Controller,
require: { require: {
card: '^vnUserCard' card: '^vnUserCard'
},
bindings: {
user: '<'
} }
}); });

View File

@ -78,7 +78,9 @@
"state": "account.card.basicData", "state": "account.card.basicData",
"component": "vn-user-basic-data", "component": "vn-user-basic-data",
"description": "Basic data", "description": "Basic data",
"acl": ["itManagement"] "params": {
"user": "$ctrl.user"
}
}, },
{ {
"url" : "/log", "url" : "/log",