Merge branch 'dev' into #2219-Añadir-menu-wikipedia
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2020-04-06 12:41:07 +02:00
commit dad77cfa08
18 changed files with 260 additions and 50 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ e2e/dms/*/
!e2e/dms/c4c !e2e/dms/c4c
!e2e/dms/c81 !e2e/dms/c81
!e2e/dms/ecc !e2e/dms/ecc
!e2e/dms/a87
npm-debug.log npm-debug.log
.eslintcache .eslintcache
datasources.*.json datasources.*.json

View File

@ -1,5 +1,4 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('downloadFile', { Self.remoteMethodCtx('downloadFile', {
description: 'Download a document', description: 'Download a document',
@ -34,29 +33,8 @@ module.exports = Self => {
}); });
Self.downloadFile = async function(ctx, id) { Self.downloadFile = async function(ctx, id) {
const storageConnector = Self.app.dataSources.storage.connector; if (!await Self.checkRole(ctx, id))
const models = Self.app.models;
const dms = await Self.findById(id);
const hasReadRole = await models.DmsType.hasReadRole(ctx, dms.dmsTypeFk);
if (!hasReadRole)
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
return await Self.getFile(id);
const pathHash = storageConnector.getPathHash(dms.id);
try {
await models.Container.getFile(pathHash, dms.file);
} catch (e) {
if (e.code != 'ENOENT')
throw e;
const error = new UserError(`File doesn't exists`);
error.statusCode = 404;
throw error;
}
const stream = models.Container.downloadStream(pathHash, dms.file);
return [stream, dms.contentType, `filename="${dms.file}"`];
}; };
}; };

View File

@ -11,7 +11,7 @@ describe('dms downloadFile()', () => {
expect(result[1]).toEqual('text/plain'); expect(result[1]).toEqual('text/plain');
}); });
it(`should return an error for a user without enough privileges`, async() => { it('should return an error for a user without enough privileges', async() => {
let clientId = 101; let clientId = 101;
let ctx = {req: {accessToken: {userId: clientId}}}; let ctx = {req: {accessToken: {userId: clientId}}};

View File

@ -1,6 +1,37 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
require('../methods/dms/downloadFile')(Self); require('../methods/dms/downloadFile')(Self);
require('../methods/dms/uploadFile')(Self); require('../methods/dms/uploadFile')(Self);
require('../methods/dms/removeFile')(Self); require('../methods/dms/removeFile')(Self);
require('../methods/dms/updateFile')(Self); require('../methods/dms/updateFile')(Self);
Self.checkRole = async function(ctx, id) {
const models = Self.app.models;
const dms = await Self.findById(id);
return await models.DmsType.hasReadRole(ctx, dms.dmsTypeFk);
};
Self.getFile = async function(id) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const dms = await Self.findById(id);
const pathHash = storageConnector.getPathHash(dms.id);
try {
await models.Container.getFile(pathHash, dms.file);
} catch (e) {
if (e.code != 'ENOENT')
throw e;
const error = new UserError(`File doesn't exists`);
error.statusCode = 404;
throw error;
}
const stream = models.Container.downloadStream(pathHash, dms.file);
return [stream, dms.contentType, `filename="${dms.file}"`];
};
}; };

View File

@ -0,0 +1,53 @@
const app = require('vn-loopback/server/server');
describe('Dms', () => {
const Dms = app.models.Dms;
describe('getFile()', () => {
it('should return a response with text content-type', async() => {
const result = await Dms.getFile(1);
expect(result[1]).toEqual('text/plain');
});
it('should return an error for a file does not exists', async() => {
let error = {};
try {
await Dms.getFile(6);
} catch (e) {
error = e;
}
expect(error.statusCode).toBe(404);
});
it('should return an error for a record does not exists', async() => {
let error = {};
try {
await app.models.Dms.getFile('NotExistentId');
} catch (e) {
error = e;
}
expect(error.statusCode).not.toBe(404);
expect(error).toEqual(jasmine.any(Error));
});
});
describe('checkRole()', () => {
const dmsId = 1;
it('should return a true for an employee with permission', async() => {
let ctx = {req: {accessToken: {userId: 107}}};
const result = await Dms.checkRole(ctx, dmsId);
expect(result).toBeTruthy();
});
it('should return false for an employee without permission', async() => {
let ctx = {req: {accessToken: {userId: 101}}};
const result = await Dms.checkRole(ctx, dmsId);
expect(result).toBeFalsy();
});
});
});

View File

@ -0,0 +1,4 @@
ALTER TABLE `vn`.`workerDocument`
ADD COLUMN `isReadableByWorker` TINYINT(1) NOT NULL DEFAULT 0 AFTER `document`;
UPDATE `vn`.`workerDocument` SET `isReadableByWorker` = '1' WHERE (`id` = '1');

View File

@ -0,0 +1,5 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('WorkerDms', 'filter', 'READ', 'ALLOW', 'ROLE', 'employee'),
('WorkerDms', 'downloadFile', 'READ', 'ALLOW', 'ROLE', 'employee');
DELETE FROM `salix`.`ACL` WHERE (`id` = '205');

View File

@ -1953,11 +1953,13 @@ INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `c
INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`) INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`)
VALUES VALUES
(1, 14, '1.txt', 'text/plain', 5, 1, 442, NULL, FALSE, 'Ticket:11', 'Ticket:11 dms for the ticket', CURDATE()), (1, 14, '1.txt', 'text/plain', 5, 1, 442, NULL, FALSE, 'Ticket:11', 'Ticket:11 dms for the ticket', CURDATE()),
(2, 5, '2.txt', 'text/plain', 5, 1, 442, 1, TRUE, 'Client:104', 'Client:104 dms for the client', CURDATE()), (2, 5, '2.txt', 'text/plain', 5, 1, 442, 1, TRUE, 'Client:104', 'Client:104 dms for the client', CURDATE()),
(3, 5, '3.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Client: 104', 'Client:104 readme', CURDATE()), (3, 5, '3.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Client: 104', 'Client:104 readme', CURDATE()),
(4, 3, '4.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Worker: 106', 'Worker:106 readme', CURDATE()), (4, 3, '4.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Worker: 106', 'Worker:106 readme', CURDATE()),
(5, 5, '5.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'travel: 1', 'dmsForThermograph', CURDATE()); (5, 5, '5.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'travel: 1', 'dmsForThermograph', CURDATE()),
(6, 5, '6.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'NotExists', 'DoesNotExists', CURDATE());
INSERT INTO `vn`.`ticketDms`(`ticketFk`, `dmsFk`) INSERT INTO `vn`.`ticketDms`(`ticketFk`, `dmsFk`)
VALUES VALUES
@ -1968,9 +1970,10 @@ INSERT INTO `vn`.`clientDms`(`clientFk`, `dmsFk`)
(104, 2), (104, 2),
(104, 3); (104, 3);
INSERT INTO `vn`.`workerDocument`(`id`, `worker`, `document`) INSERT INTO `vn`.`workerDocument`(`id`, `worker`, `document`,`isReadableByWorker`)
VALUES VALUES
(1, 106, 4); (1, 106, 4, TRUE),
(2, 107, 3, FALSE);
INSERT INTO `vn`.`device` (`sn`, `model`, `userFk`) INSERT INTO `vn`.`device` (`sn`, `model`, `userFk`)
VALUES VALUES

1
e2e/dms/a87/4.txt Normal file
View File

@ -0,0 +1 @@
File: 4.txt. It works!

View File

@ -0,0 +1,40 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('downloadFile', {
description: 'Download a worker document',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: `/:id/downloadFile`,
verb: 'GET'
}
});
Self.downloadFile = async function(ctx, id) {
if (!await Self.app.models.Dms.checkRole(ctx, id) && !await Self.isMine(ctx, id))
throw new UserError(`You don't have enough privileges`);
return await Self.app.models.Dms.getFile(id);
};
};

View File

@ -0,0 +1,53 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethodCtx('filter', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'Object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
}
],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/filter`,
verb: 'GET'
}
});
Self.filter = async(ctx, filter) => {
const conn = Self.dataSource.connector;
const userId = ctx.req.accessToken.userId;
const account = await Self.app.models.Account.findById(userId);
const stmt = new ParameterizedSQL(
`SELECT d.id dmsFk, d.reference, d.description, d.file, d.created
FROM workerDocument wd
JOIN dms d ON d.id = wd.document
JOIN dmsType dt ON dt.id = d.dmsTypeFk
LEFT JOIN account.roleRole rr ON rr.inheritsFrom = dt.readRoleFk AND rr.role = ?
`, [account.roleFk]
);
const oldWhere = filter.where;
const yourOwnDms = {and: [{isReadableByWorker: true}, {worker: userId}]};
filter.where = {
and: [{
or: [yourOwnDms, {
role: {
neq: null
}
}]
}, oldWhere]};
stmt.merge(conn.makeSuffix(filter));
return await conn.executeStmt(stmt);
};
};

View File

@ -0,0 +1,27 @@
const app = require('vn-loopback/server/server');
describe('worker-dms downloadFile()', () => {
let dmsId = 4;
it('should return a response for an employee with text content-type', async() => {
let workerId = 106;
let ctx = {req: {accessToken: {userId: workerId}}};
const result = await app.models.WorkerDms.downloadFile(ctx, dmsId);
expect(result[1]).toEqual('text/plain');
});
it('should return an error for a user without enough privileges', async() => {
let clientId = 1;
let ctx = {req: {accessToken: {userId: clientId}}};
let error;
try {
await app.models.WorkerDms.downloadFile(ctx, dmsId);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
});

View File

@ -125,7 +125,6 @@ module.exports = Self => {
LEFT JOIN account.emailUser mu ON mu.userFk = u.id` LEFT JOIN account.emailUser mu ON mu.userFk = u.id`
); );
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));
let itemsIndex = stmts.push(stmt) - 1; let itemsIndex = stmts.push(stmt) - 1;

View File

@ -1,4 +1,17 @@
module.exports = Self => { module.exports = Self => {
require('../methods/worker-dms/downloadFile')(Self);
require('../methods/worker-dms/removeFile')(Self); require('../methods/worker-dms/removeFile')(Self);
require('../methods/worker-dms/allowedContentTypes')(Self); require('../methods/worker-dms/allowedContentTypes')(Self);
require('../methods/worker-dms/filter')(Self);
Self.isMine = async function(ctx, dmsId) {
const myUserId = ctx.req.accessToken.userId;
let workerDms = await Self.findOne({
where: {
workerFk: myUserId,
dmsFk: dmsId
}
});
return workerDms !== null;
};
}; };

View File

@ -29,6 +29,9 @@
"mysql": { "mysql": {
"columnName": "worker" "columnName": "worker"
} }
},
"isReadableByWorker": {
"type": "Boolean"
} }
}, },
"relations": { "relations": {

View File

@ -1,8 +1,7 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="WorkerDms" url="WorkerDms/filter"
link="{workerFk: $ctrl.$params.id}" link="{worker: $ctrl.$params.id}"
filter="::$ctrl.filter"
limit="20" limit="20"
data="$ctrl.workerDms" data="$ctrl.workerDms"
order="dmsFk DESC" order="dmsFk DESC"
@ -28,28 +27,28 @@
<vn-tbody> <vn-tbody>
<vn-tr ng-repeat="document in $ctrl.workerDms"> <vn-tr ng-repeat="document in $ctrl.workerDms">
<vn-td number shrink>{{::document.dmsFk}}</vn-td> <vn-td number shrink>{{::document.dmsFk}}</vn-td>
<vn-td shrink> <vn-td expand>
<span title="{{::document.dms.reference}}"> <span title="{{::document.reference}}">
{{::document.dms.reference}} {{::document.reference}}
</span> </span>
</vn-td> </vn-td>
<vn-td expand> <vn-td expand>
<span title="{{::document.dms.description}}"> <span title="{{::document.description}}">
{{::document.dms.description}} {{::document.description}}
</span> </span>
</vn-td> </vn-td >
<vn-td shrink> <vn-td shrink>
<a target="_blank" <a target="_blank"
title="{{'Download file' | translate}}" title="{{'Download file' | translate}}"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">{{::document.dms.file}} href="api/workerDms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">{{::document.file}}
</a> </a>
</vn-td> </vn-td>
<vn-td> <vn-td>
{{::document.dms.created | date:'dd/MM/yyyy HH:mm'}} {{::document.created | date:'dd/MM/yyyy HH:mm'}}
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<a target="_blank" <a target="_blank"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}"> href="api/workerDms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.vnToken.token}}">
<vn-icon-button <vn-icon-button
icon="cloud_download" icon="cloud_download"
title="{{'Download file' | translate}}"> title="{{'Download file' | translate}}">
@ -57,7 +56,7 @@
</a> </a>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon-button ui-sref="worker.card.dms.edit({dmsId: {{::document.dmsFk}}})" <vn-icon-button ui-sref="worker.card.edit({dmsId: {{::document.dmsFk}}})"
icon="edit" icon="edit"
title="{{'Edit file' | translate}}"> title="{{'Edit file' | translate}}">
</vn-icon-button> </vn-icon-button>
@ -78,7 +77,7 @@
<vn-worker-descriptor-popover <vn-worker-descriptor-popover
vn-id="workerDescriptor"> vn-id="workerDescriptor">
</vn-worker-descriptor-popover> </vn-worker-descriptor-popover>
<a ui-sref="worker.card.dms.create" <a ui-sref="worker.card.create"
vn-tooltip="Upload file" vn-tooltip="Upload file"
vn-bind="+" vn-bind="+"
fixed-bottom-right> fixed-bottom-right>

View File

@ -104,8 +104,8 @@
"url": "/index", "url": "/index",
"state": "worker.card.dms.index", "state": "worker.card.dms.index",
"component": "vn-worker-dms-index", "component": "vn-worker-dms-index",
"description": "File management", "description": "My documentation",
"acl": ["hr"] "acl": ["employee"]
}, },
{ {
"url": "/create", "url": "/create",

View File

@ -41,7 +41,7 @@
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
vn-one vn-one
label="Fiscal Identifier" label="Fiscal identifier"
ng-model="filter.fi"> ng-model="filter.fi">
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>