Merge branch 'dev' into 5576-reestructurar-directorio-imagenes
gitea/salix/pipeline/head There was a failure building this commit
Details
gitea/salix/pipeline/head There was a failure building this commit
Details
This commit is contained in:
commit
d0ff8ba57f
|
@ -17,7 +17,7 @@ rules:
|
|||
camelcase: 0
|
||||
default-case: 0
|
||||
no-eq-null: 0
|
||||
no-console: ["error"]
|
||||
no-console: ["warn"]
|
||||
no-warning-comments: 0
|
||||
no-empty: [error, allowEmptyCatch: true]
|
||||
complexity: 0
|
||||
|
|
|
@ -10,5 +10,9 @@
|
|||
"eslint.format.enable": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
}
|
||||
},
|
||||
"cSpell.words": [
|
||||
"salix",
|
||||
"fdescribe"
|
||||
]
|
||||
}
|
||||
|
|
101
CHANGELOG.md
101
CHANGELOG.md
|
@ -5,16 +5,111 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2326.01] - 2023-06-29
|
||||
## [2348.01] - 2023-11-30
|
||||
|
||||
### Added
|
||||
- (Ticket -> Adelantar) Permite mover lineas sin generar negativos
|
||||
- (Ticket -> Adelantar) Permite modificar la fecha de los tickets
|
||||
|
||||
### Changed
|
||||
### Fixed
|
||||
- (Ticket -> RocketChat) Arreglada detección de cambios
|
||||
|
||||
|
||||
## [2346.01] - 2023-11-16
|
||||
|
||||
### Added
|
||||
### Changed
|
||||
### Fixed
|
||||
|
||||
|
||||
## [2342.01] - 2023-11-02
|
||||
|
||||
### Added
|
||||
- (Usuarios -> Foto) Se muestra la foto del trabajador
|
||||
### Fixed
|
||||
- (Usuarios -> Historial) Abre el descriptor del usuario correctamente
|
||||
|
||||
|
||||
## [2340.01] - 2023-10-05
|
||||
|
||||
## [2338.01] - 2023-09-21
|
||||
|
||||
### Added
|
||||
- (Ticket -> Servicios) Se pueden abonar servicios
|
||||
- (Facturas -> Datos básicos) Muestra valores por defecto
|
||||
- (Facturas -> Borrado) Notificación al borrar un asiento ya enlazado en Sage
|
||||
### Changed
|
||||
- (Trabajadores -> Calendario) Icono de check arreglado cuando pulsas un tipo de dia
|
||||
|
||||
## [2336.01] - 2023-09-07
|
||||
|
||||
## [2334.01] - 2023-08-24
|
||||
|
||||
### Added
|
||||
- (General -> Errores) Botón para enviar cau con los datos del error
|
||||
|
||||
|
||||
## [2332.01] - 2023-08-10
|
||||
|
||||
### Added
|
||||
- (Trabajadores -> Gestión documental) Soporte para Docuware
|
||||
- (General -> Agencia) Soporte para Viaexpress
|
||||
- (Tickets -> SMS) Nueva sección en Lilium
|
||||
|
||||
### Changed
|
||||
- (General -> Tickets) Devuelve el motivo por el cual no es editable
|
||||
- (Desplegables -> Trabajadores) Mejorados
|
||||
- (General -> Clientes) Razón social y dirección en mayúsculas
|
||||
|
||||
### Fixed
|
||||
- (Clientes -> SMS) Al pasar el ratón por encima muestra el mensaje completo
|
||||
|
||||
|
||||
## [2330.01] - 2023-07-27
|
||||
|
||||
### Added
|
||||
- (Artículos -> Vista Previa) Añadido campo "Plástico reciclado"
|
||||
- (Rutas -> Troncales) Nueva sección
|
||||
- (Tickets -> Opciones) Opción establecer peso
|
||||
- (Clientes -> SMS) Nueva sección
|
||||
|
||||
### Changed
|
||||
- (General -> Iconos) Añadidos nuevos iconos
|
||||
- (Clientes -> Razón social) Permite crear clientes con la misma razón social según el país
|
||||
|
||||
|
||||
## [2328.01] - 2023-07-13
|
||||
|
||||
### Added
|
||||
- (Clientes -> Morosos) Añadida columna "es trabajador"
|
||||
- (Trabajadores -> Departamentos) Nueva sección
|
||||
- (Trabajadores -> Departamentos) Añadido listado de Trabajadores por departamento
|
||||
- (Trabajadores -> Departamentos) Añadido características de departamento e información
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
-
|
||||
- (Trabajadores -> Departamentos) Arreglado búscador
|
||||
|
||||
## [2324.01] - 2023-06-08
|
||||
|
||||
## [2326.01] - 2023-06-29
|
||||
|
||||
### Added
|
||||
- (Entradas -> Correo) Al cambiar el tipo de cambio enviará un correo a las personas designadas
|
||||
- (General -> Históricos) Botón para ver el estado del registro en cada punto
|
||||
- (General -> Históricos) Al filtar por registro se muestra todo el histórial desde que fue creado
|
||||
- (Tickets -> Índice) Permite enviar varios albaranes a Docuware
|
||||
|
||||
### Changed
|
||||
- (General -> Históricos) Los registros se muestran agrupados por usuario y entidad
|
||||
- (Facturas -> Facturación global) Optimizada, generación de PDFs y notificaciones en paralelo
|
||||
|
||||
### Fixed
|
||||
- (General -> Históricos) Duplicidades eliminadas
|
||||
- (Facturas -> Facturación global) Solucionados fallos que paran el proceso
|
||||
|
||||
## [2324.01] - 2023-06-15
|
||||
|
||||
### Added
|
||||
- (Tickets -> Abono) Al abonar permite crear el ticket abono con almacén o sin almmacén
|
||||
|
|
10
Dockerfile
10
Dockerfile
|
@ -1,4 +1,4 @@
|
|||
FROM debian:bullseye-slim
|
||||
FROM debian:bookworm-slim
|
||||
ENV TZ Europe/Madrid
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
@ -25,7 +25,13 @@ RUN apt-get update \
|
|||
libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
|
||||
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
|
||||
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
|
||||
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
|
||||
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||
|
||||
# Extra dependencies
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
samba-common-bin samba-dsdb-modules\
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& npm -g install pm2
|
||||
|
||||
|
|
42
README.md
42
README.md
|
@ -8,7 +8,7 @@ Salix is also the scientific name of a beautifull tree! :)
|
|||
|
||||
Required applications.
|
||||
|
||||
* Node.js = 14.x LTS
|
||||
* Node.js
|
||||
* Docker
|
||||
* Git
|
||||
|
||||
|
@ -17,20 +17,7 @@ You will need to install globally the following items.
|
|||
$ sudo npm install -g jest gulp-cli
|
||||
```
|
||||
|
||||
For the usage of jest --watch on macOs.
|
||||
```
|
||||
$ brew install watchman
|
||||
```
|
||||
* [watchman](https://facebook.github.io/watchman/)
|
||||
|
||||
## Linux Only Prerequisites
|
||||
|
||||
Your user must be on the docker group to use it so you will need to run this command:
|
||||
```
|
||||
$ sudo usermod -a -G docker yourusername
|
||||
```
|
||||
|
||||
## Getting Started // Installing
|
||||
## Installing dependencies and launching
|
||||
|
||||
Pull from repository.
|
||||
|
||||
|
@ -71,34 +58,11 @@ $ npm run test:e2e
|
|||
|
||||
Open Visual Studio Code, press Ctrl+P and paste the following commands.
|
||||
|
||||
In Visual Studio Code we use the ESLint extension.
|
||||
In Visual Studio Code we use the ESLint extension.
|
||||
```
|
||||
ext install dbaeumer.vscode-eslint
|
||||
```
|
||||
|
||||
Gitlens for visualization of code authorship
|
||||
```
|
||||
ext install eamodio.gitlens
|
||||
```
|
||||
|
||||
Spanish language pack
|
||||
```
|
||||
ext install ms-ceintl.vscode-language-pack-es
|
||||
```
|
||||
|
||||
### Recommended extensions
|
||||
|
||||
Material icon Theme
|
||||
```
|
||||
ext install pkief.material-icon-theme
|
||||
```
|
||||
|
||||
Material UI Themes
|
||||
```
|
||||
ext install equinusocio.vsc-material-theme
|
||||
```
|
||||
|
||||
|
||||
## Built With
|
||||
|
||||
* [angularjs](https://angularjs.org/)
|
||||
|
|
|
@ -26,15 +26,14 @@ module.exports = Self => {
|
|||
|
||||
Self.sendCheckingPresence = async(ctx, recipientId, message) => {
|
||||
if (!recipientId) return false;
|
||||
|
||||
const models = Self.app.models;
|
||||
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
const sender = await models.VnUser.findById(userId, {fields: ['id']});
|
||||
const recipient = await models.VnUser.findById(recipientId, null);
|
||||
|
||||
// Prevent sending messages to yourself
|
||||
if (recipientId == userId) return false;
|
||||
|
||||
if (!recipient)
|
||||
throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`);
|
||||
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('getTickets', {
|
||||
description: 'Make a new collection of tickets',
|
||||
accessType: 'WRITE',
|
||||
accepts: [{
|
||||
arg: 'id',
|
||||
type: 'number',
|
||||
description: 'The collection id',
|
||||
required: true,
|
||||
http: {source: 'path'}
|
||||
}, {
|
||||
arg: 'print',
|
||||
type: 'boolean',
|
||||
description: 'True if you want to print'
|
||||
}],
|
||||
returns: {
|
||||
type: ['object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/:id/getTickets`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.getTickets = async(ctx, id, print, options) => {
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
const url = await Self.app.models.Url.getUrl();
|
||||
const $t = ctx.req.__;
|
||||
const myOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
myOptions.userId = userId;
|
||||
|
||||
const promises = [];
|
||||
const [tickets] = await Self.rawSql(`CALL vn.collection_getTickets(?)`, [id], myOptions);
|
||||
const sales = await Self.rawSql(`
|
||||
SELECT s.ticketFk,
|
||||
sgd.saleGroupFk,
|
||||
s.id saleFk,
|
||||
s.itemFk,
|
||||
i.longName,
|
||||
i.size,
|
||||
ic.color,
|
||||
o.code origin,
|
||||
ish.packing,
|
||||
ish.grouping,
|
||||
s.isAdded,
|
||||
s.originalQuantity,
|
||||
s.quantity saleQuantity,
|
||||
iss.quantity reservedQuantity,
|
||||
SUM(iss.quantity) OVER (PARTITION BY s.id ORDER BY ish.id) accumulatedQuantity,
|
||||
ROW_NUMBER () OVER (PARTITION BY s.id ORDER BY pickingOrder) currentItemShelving,
|
||||
COUNT(*) OVER (PARTITION BY s.id ORDER BY s.id) totalItemShelving,
|
||||
sh.code,
|
||||
IFNULL(p2.code, p.code) parkingCode,
|
||||
IFNULL(p2.pickingOrder, p.pickingOrder) pickingOrder,
|
||||
iss.id itemShelvingSaleFk,
|
||||
iss.isPicked
|
||||
FROM ticketCollection tc
|
||||
LEFT JOIN collection c ON c.id = tc.collectionFk
|
||||
JOIN ticket t ON t.id = tc.ticketFk
|
||||
JOIN sale s ON s.ticketFk = t.id
|
||||
LEFT JOIN saleGroupDetail sgd ON sgd.saleFk = s.id
|
||||
LEFT JOIN saleGroup sg ON sg.id = sgd.saleGroupFk
|
||||
LEFT JOIN parking p2 ON p2.id = sg.parkingFk
|
||||
JOIN item i ON i.id = s.itemFk
|
||||
LEFT JOIN itemShelvingSale iss ON iss.saleFk = s.id
|
||||
LEFT JOIN itemShelving ish ON ish.id = iss.itemShelvingFk
|
||||
LEFT JOIN shelving sh ON sh.code = ish.shelvingFk
|
||||
LEFT JOIN parking p ON p.id = sh.parkingFk
|
||||
LEFT JOIN itemColor ic ON ic.itemFk = s.itemFk
|
||||
LEFT JOIN origin o ON o.id = i.originFk
|
||||
WHERE tc.collectionFk = ?
|
||||
GROUP BY ish.id, p.code, p2.code
|
||||
ORDER BY pickingOrder;`, [id], myOptions);
|
||||
|
||||
if (print)
|
||||
await Self.rawSql(`CALL vn.collection_printSticker(?, ?)`, [id, null], myOptions);
|
||||
|
||||
const collection = {collectionFk: id, tickets: []};
|
||||
if (tickets && tickets.length) {
|
||||
for (const ticket of tickets) {
|
||||
const ticketId = ticket.ticketFk;
|
||||
if (ticket.observaciones != '') {
|
||||
for (observation of ticket.observaciones.split(' ')) {
|
||||
if (['#', '@'].includes(observation.charAt(0))) {
|
||||
promises.push(Self.app.models.Chat.send(ctx, observation,
|
||||
$t('The ticket is in preparation', {
|
||||
ticketId: ticketId,
|
||||
ticketUrl: `${url}ticket/${ticketId}/summary`,
|
||||
salesPersonId: ticket.salesPersonFk
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sales && sales.length) {
|
||||
const barcodes = await Self.rawSql(`
|
||||
SELECT s.id saleFk, b.code, c.id
|
||||
FROM vn.sale s
|
||||
LEFT JOIN vn.itemBarcode b ON b.itemFk = s.itemFk
|
||||
LEFT JOIN vn.buy c ON c.itemFk = s.itemFk
|
||||
LEFT JOIN vn.entry e ON e.id = c.entryFk
|
||||
LEFT JOIN vn.travel tr ON tr.id = e.travelFk
|
||||
WHERE s.ticketFk = ?
|
||||
AND tr.landed >= util.VN_CURDATE() - INTERVAL 1 YEAR`,
|
||||
[ticketId], myOptions);
|
||||
ticket.sales = [];
|
||||
for (const sale of sales) {
|
||||
if (sale.ticketFk === ticketId) {
|
||||
sale.Barcodes = [];
|
||||
if (barcodes && barcodes.length) {
|
||||
for (const barcode of barcodes) {
|
||||
if (barcode.saleFk === sale.saleFk) {
|
||||
for (const prop in barcode) {
|
||||
if (['id', 'code'].includes(prop) && barcode[prop])
|
||||
sale.Barcodes.push(barcode[prop].toString(), '0' + barcode[prop]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ticket.sales.push(sale);
|
||||
}
|
||||
}
|
||||
}
|
||||
collection.tickets.push(ticket);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
return collection;
|
||||
};
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
|
||||
describe('collection getTickets()', () => {
|
||||
let ctx;
|
||||
beforeAll(async() => {
|
||||
ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should get tickets, sales and barcodes from collection', async() => {
|
||||
const tx = await models.Collection.beginTransaction({});
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
const collectionId = 1;
|
||||
|
||||
const collectionTickets = await models.Collection.getTickets(ctx, collectionId, null, options);
|
||||
|
||||
expect(collectionTickets.collectionFk).toEqual(collectionId);
|
||||
expect(collectionTickets.tickets.length).toEqual(3);
|
||||
expect(collectionTickets.tickets[0].ticketFk).toEqual(1);
|
||||
expect(collectionTickets.tickets[1].ticketFk).toEqual(2);
|
||||
expect(collectionTickets.tickets[2].ticketFk).toEqual(23);
|
||||
expect(collectionTickets.tickets[0].sales[0].ticketFk).toEqual(1);
|
||||
expect(collectionTickets.tickets[0].sales[1].ticketFk).toEqual(1);
|
||||
expect(collectionTickets.tickets[0].sales[2].ticketFk).toEqual(1);
|
||||
expect(collectionTickets.tickets[0].sales[0].Barcodes.length).toBeTruthy();
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -18,6 +18,14 @@ describe('setSaleQuantity()', () => {
|
|||
|
||||
it('should change quantity sale', async() => {
|
||||
const tx = await models.Ticket.beginTransaction({});
|
||||
spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
|
||||
if (sqlStatement.includes('catalog_calcFromItem')) {
|
||||
sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY
|
||||
SELECT 100 as available;`;
|
||||
params = null;
|
||||
}
|
||||
return models.Ticket.rawSql(sqlStatement, params, options);
|
||||
});
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
const axios = require('axios');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('checkFile', {
|
||||
Self.remoteMethod('checkFile', {
|
||||
description: 'Check if exist docuware file',
|
||||
accessType: 'READ',
|
||||
accepts: [
|
||||
|
@ -17,12 +15,16 @@ module.exports = Self => {
|
|||
required: true,
|
||||
description: 'The fileCabinet name'
|
||||
},
|
||||
{
|
||||
arg: 'filter',
|
||||
type: 'object',
|
||||
description: 'The filter'
|
||||
},
|
||||
{
|
||||
arg: 'signed',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
description: 'If pdf is necessary to be signed'
|
||||
}
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
|
@ -34,7 +36,7 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.checkFile = async function(ctx, id, fileCabinet, signed) {
|
||||
Self.checkFile = async function(id, fileCabinet, filter, signed) {
|
||||
const models = Self.app.models;
|
||||
const action = 'find';
|
||||
|
||||
|
@ -45,40 +47,34 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
const searchFilter = {
|
||||
condition: [
|
||||
{
|
||||
DBName: docuwareInfo.findById,
|
||||
|
||||
Value: [id]
|
||||
}
|
||||
],
|
||||
sortOrder: [
|
||||
{
|
||||
Field: 'FILENAME',
|
||||
Direction: 'Desc'
|
||||
}
|
||||
]
|
||||
};
|
||||
if (!filter) {
|
||||
filter = {
|
||||
condition: [
|
||||
{
|
||||
DBName: docuwareInfo.findById,
|
||||
Value: [id]
|
||||
}
|
||||
],
|
||||
sortOrder: [
|
||||
{
|
||||
Field: 'FILENAME',
|
||||
Direction: 'Desc'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
if (signed) {
|
||||
filter.condition.push({
|
||||
DBName: 'ESTADO',
|
||||
Value: ['Firmado']
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const options = await Self.getOptions();
|
||||
const [response] = await Self.get(fileCabinet, filter);
|
||||
if (!response) return false;
|
||||
|
||||
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
|
||||
const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId);
|
||||
|
||||
const response = await axios.post(
|
||||
`${options.url}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`,
|
||||
searchFilter,
|
||||
options.headers
|
||||
);
|
||||
const [documents] = response.data.Items;
|
||||
if (!documents) return false;
|
||||
|
||||
const state = documents.Fields.find(field => field.FieldName == 'ESTADO');
|
||||
if (signed && state.Item != 'Firmado') return false;
|
||||
|
||||
return {id: documents.Id};
|
||||
return {id: response['Document ID']};
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,59 +1,6 @@
|
|||
const axios = require('axios');
|
||||
|
||||
module.exports = Self => {
|
||||
/**
|
||||
* Returns the dialog id
|
||||
*
|
||||
* @param {string} code - The fileCabinet name
|
||||
* @param {string} action - The fileCabinet name
|
||||
* @param {string} fileCabinetId - Optional The fileCabinet name
|
||||
* @return {number} - The fileCabinet id
|
||||
*/
|
||||
Self.getDialog = async(code, action, fileCabinetId) => {
|
||||
const docuwareInfo = await Self.app.models.Docuware.findOne({
|
||||
where: {
|
||||
code: code,
|
||||
action: action
|
||||
}
|
||||
});
|
||||
if (!fileCabinetId) fileCabinetId = await Self.getFileCabinet(code);
|
||||
|
||||
const options = await Self.getOptions();
|
||||
|
||||
if (!process.env.NODE_ENV)
|
||||
return Math.round();
|
||||
|
||||
const response = await axios.get(`${options.url}/FileCabinets/${fileCabinetId}/dialogs`, options.headers);
|
||||
const dialogs = response.data.Dialog;
|
||||
const dialogId = dialogs.find(dialogs => dialogs.DisplayName === docuwareInfo.dialogName).Id;
|
||||
|
||||
return dialogId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the fileCabinetId
|
||||
*
|
||||
* @param {string} code - The fileCabinet code
|
||||
* @return {number} - The fileCabinet id
|
||||
*/
|
||||
Self.getFileCabinet = async code => {
|
||||
const options = await Self.getOptions();
|
||||
const docuwareInfo = await Self.app.models.Docuware.findOne({
|
||||
where: {
|
||||
code: code
|
||||
}
|
||||
});
|
||||
|
||||
if (!process.env.NODE_ENV)
|
||||
return Math.round();
|
||||
|
||||
const fileCabinetResponse = await axios.get(`${options.url}/FileCabinets`, options.headers);
|
||||
const fileCabinets = fileCabinetResponse.data.FileCabinet;
|
||||
const fileCabinetId = fileCabinets.find(fileCabinet => fileCabinet.Name === docuwareInfo.fileCabinetName).Id;
|
||||
|
||||
return fileCabinetId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns basic headers
|
||||
*
|
||||
|
@ -75,4 +22,139 @@ module.exports = Self => {
|
|||
headers
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the dialog id
|
||||
*
|
||||
* @param {string} code - The fileCabinet name
|
||||
* @param {string} action - The fileCabinet name
|
||||
* @param {string} fileCabinetId - Optional The fileCabinet name
|
||||
* @return {number} - The fileCabinet id
|
||||
*/
|
||||
Self.getDialog = async(code, action, fileCabinetId) => {
|
||||
if (!process.env.NODE_ENV)
|
||||
return Math.floor(Math.random() + 100);
|
||||
|
||||
const docuwareInfo = await Self.app.models.Docuware.findOne({
|
||||
where: {
|
||||
code,
|
||||
action
|
||||
}
|
||||
});
|
||||
if (!fileCabinetId) fileCabinetId = await Self.getFileCabinet(code);
|
||||
|
||||
const options = await Self.getOptions();
|
||||
|
||||
const response = await axios.get(`${options.url}/FileCabinets/${fileCabinetId}/dialogs`, options.headers);
|
||||
const dialogs = response.data.Dialog;
|
||||
const dialogId = dialogs.find(dialogs => dialogs.DisplayName === docuwareInfo.dialogName).Id;
|
||||
|
||||
return dialogId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the fileCabinetId
|
||||
*
|
||||
* @param {string} code - The fileCabinet code
|
||||
* @return {number} - The fileCabinet id
|
||||
*/
|
||||
Self.getFileCabinet = async code => {
|
||||
if (!process.env.NODE_ENV)
|
||||
return Math.floor(Math.random() + 100);
|
||||
|
||||
const options = await Self.getOptions();
|
||||
const docuwareInfo = await Self.app.models.Docuware.findOne({
|
||||
where: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
const fileCabinetResponse = await axios.get(`${options.url}/FileCabinets`, options.headers);
|
||||
const fileCabinets = fileCabinetResponse.data.FileCabinet;
|
||||
const fileCabinetId = fileCabinets.find(fileCabinet => fileCabinet.Name === docuwareInfo.fileCabinetName).Id;
|
||||
|
||||
return fileCabinetId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns docuware data
|
||||
*
|
||||
* @param {string} code - The fileCabinet code
|
||||
* @param {object} filter - The filter for docuware
|
||||
* @param {object} parse - The fields parsed
|
||||
* @return {object} - The data
|
||||
*/
|
||||
Self.get = async(code, filter, parse) => {
|
||||
if (!process.env.NODE_ENV) return;
|
||||
|
||||
const options = await Self.getOptions();
|
||||
const fileCabinetId = await Self.getFileCabinet(code);
|
||||
const dialogId = await Self.getDialog(code, 'find', fileCabinetId);
|
||||
|
||||
const data = await axios.post(
|
||||
`${options.url}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`,
|
||||
filter,
|
||||
options.headers
|
||||
);
|
||||
return parser(data.data, parse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns docuware data
|
||||
*
|
||||
* @param {string} code - The fileCabinet code
|
||||
* @param {any} id - The id of docuware
|
||||
* @param {object} parse - The fields parsed
|
||||
* @return {object} - The data
|
||||
*/
|
||||
Self.getById = async(code, id, parse) => {
|
||||
if (!process.env.NODE_ENV) return;
|
||||
|
||||
const docuwareInfo = await Self.app.models.Docuware.findOne({
|
||||
fields: ['findById'],
|
||||
where: {
|
||||
code,
|
||||
action: 'find'
|
||||
}
|
||||
});
|
||||
const filter = {
|
||||
condition: [
|
||||
{
|
||||
DBName: docuwareInfo.findById,
|
||||
Value: [id]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return Self.get(code, filter, parse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns docuware data filtered
|
||||
*
|
||||
* @param {array} data - The data
|
||||
* @param {object} parse - The fields parsed
|
||||
* @return {object} - The data parsed
|
||||
*/
|
||||
function parser(data, parse) {
|
||||
if (!(data && data.Items)) return data;
|
||||
|
||||
const parsed = [];
|
||||
for (item of data.Items) {
|
||||
const itemParsed = {};
|
||||
item.Fields.map(field => {
|
||||
if (field.ItemElementName.includes('Date')) field.Item = toDate(field.Item);
|
||||
if (!parse) return itemParsed[field.FieldLabel] = field.Item;
|
||||
if (parse[field.FieldLabel])
|
||||
itemParsed[parse[field.FieldLabel]] = field.Item;
|
||||
});
|
||||
parsed.push(itemParsed);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function toDate(value) {
|
||||
if (!value) return;
|
||||
return new Date(Number(value.substring(6, 19)));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -65,7 +65,7 @@ module.exports = Self => {
|
|||
|
||||
const email = new Email('delivery-note', params);
|
||||
|
||||
const docuwareFile = await models.Docuware.download(ctx, id, 'deliveryNote');
|
||||
const docuwareFile = await models.Docuware.download(id, 'deliveryNote');
|
||||
|
||||
return email.send({
|
||||
overrideAttachments: true,
|
||||
|
|
|
@ -3,7 +3,7 @@ const axios = require('axios');
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('download', {
|
||||
Self.remoteMethod('download', {
|
||||
description: 'Download an docuware PDF',
|
||||
accessType: 'READ',
|
||||
accepts: [
|
||||
|
@ -16,8 +16,12 @@ module.exports = Self => {
|
|||
{
|
||||
arg: 'fileCabinet',
|
||||
type: 'string',
|
||||
description: 'The file cabinet',
|
||||
http: {source: 'path'}
|
||||
description: 'The file cabinet'
|
||||
},
|
||||
{
|
||||
arg: 'filter',
|
||||
type: 'object',
|
||||
description: 'The filter'
|
||||
}
|
||||
],
|
||||
returns: [
|
||||
|
@ -36,14 +40,15 @@ module.exports = Self => {
|
|||
}
|
||||
],
|
||||
http: {
|
||||
path: `/:id/download/:fileCabinet`,
|
||||
path: `/:id/download`,
|
||||
verb: 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
Self.download = async function(ctx, id, fileCabinet) {
|
||||
Self.download = async function(id, fileCabinet, filter) {
|
||||
const models = Self.app.models;
|
||||
const docuwareFile = await models.Docuware.checkFile(ctx, id, fileCabinet, true);
|
||||
|
||||
const docuwareFile = await models.Docuware.checkFile(id, fileCabinet, filter);
|
||||
if (!docuwareFile) throw new UserError('The DOCUWARE PDF document does not exists');
|
||||
|
||||
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
|
||||
|
|
|
@ -1,81 +1,27 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const axios = require('axios');
|
||||
|
||||
describe('docuware download()', () => {
|
||||
const ticketId = 1;
|
||||
const userId = 9;
|
||||
const ctx = {
|
||||
req: {
|
||||
|
||||
accessToken: {userId: userId},
|
||||
headers: {origin: 'http://localhost:5000'},
|
||||
}
|
||||
};
|
||||
|
||||
const docuwareModel = models.Docuware;
|
||||
const fileCabinetName = 'deliveryNote';
|
||||
|
||||
beforeAll(() => {
|
||||
spyOn(docuwareModel, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||
spyOn(docuwareModel, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||
});
|
||||
|
||||
it('should return false if there are no documents', async() => {
|
||||
const response = {
|
||||
data: {
|
||||
Items: []
|
||||
}
|
||||
};
|
||||
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
|
||||
spyOn(docuwareModel, 'get').and.returnValue((new Promise(resolve => resolve({Items: []}))));
|
||||
|
||||
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false if the document is unsigned', async() => {
|
||||
const response = {
|
||||
data: {
|
||||
Items: [
|
||||
{
|
||||
Id: 1,
|
||||
Fields: [
|
||||
{
|
||||
FieldName: 'ESTADO',
|
||||
Item: 'Unsigned'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
|
||||
|
||||
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
|
||||
const result = await models.Docuware.checkFile(ticketId, fileCabinetName, null, true);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return the document data', async() => {
|
||||
const docuwareId = 1;
|
||||
const response = {
|
||||
data: {
|
||||
Items: [
|
||||
{
|
||||
Id: docuwareId,
|
||||
Fields: [
|
||||
{
|
||||
FieldName: 'ESTADO',
|
||||
Item: 'Firmado'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
|
||||
const response = [{
|
||||
'Document ID': docuwareId
|
||||
}];
|
||||
spyOn(docuwareModel, 'get').and.returnValue((new Promise(resolve => resolve(response))));
|
||||
|
||||
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
|
||||
const result = await models.Docuware.checkFile(ticketId, fileCabinetName, null, true);
|
||||
|
||||
expect(result.id).toEqual(docuwareId);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
const axios = require('axios');
|
||||
const models = require('vn-loopback/server/server').models;
|
||||
|
||||
describe('Docuware core', () => {
|
||||
beforeAll(() => {
|
||||
process.env.NODE_ENV = 'testing';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
describe('getOptions()', () => {
|
||||
it('should return url and headers', async() => {
|
||||
const result = await models.Docuware.getOptions();
|
||||
|
||||
expect(result.url).toBeDefined();
|
||||
expect(result.headers).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDialog()', () => {
|
||||
it('should return dialogId', async() => {
|
||||
const dialogs = {
|
||||
data: {
|
||||
Dialog: [
|
||||
{
|
||||
DisplayName: 'find',
|
||||
Id: 'getDialogTest'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(dialogs)));
|
||||
const result = await models.Docuware.getDialog('deliveryNote', 'find', 'randomFileCabinetId');
|
||||
|
||||
expect(result).toEqual('getDialogTest');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileCabinet()', () => {
|
||||
it('should return fileCabinetId', async() => {
|
||||
const code = 'deliveryNote';
|
||||
const docuwareInfo = await models.Docuware.findOne({
|
||||
where: {
|
||||
code
|
||||
}
|
||||
});
|
||||
const dialogs = {
|
||||
data: {
|
||||
FileCabinet: [
|
||||
{
|
||||
Name: docuwareInfo.fileCabinetName,
|
||||
Id: 'getFileCabinetTest'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(dialogs)));
|
||||
const result = await models.Docuware.getFileCabinet(code);
|
||||
|
||||
expect(result).toEqual('getFileCabinetTest');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
it('should return data without parse', async() => {
|
||||
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||
const data = {
|
||||
data: {
|
||||
id: 1
|
||||
}
|
||||
};
|
||||
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
|
||||
const result = await models.Docuware.get('deliveryNote');
|
||||
|
||||
expect(result.id).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return data with parse', async() => {
|
||||
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||
const data = {
|
||||
data: {
|
||||
Items: [{
|
||||
Fields: [
|
||||
{
|
||||
ItemElementName: 'integer',
|
||||
FieldLabel: 'firstRequiredField',
|
||||
Item: 1
|
||||
},
|
||||
{
|
||||
ItemElementName: 'string',
|
||||
FieldLabel: 'secondRequiredField',
|
||||
Item: 'myName'
|
||||
},
|
||||
{
|
||||
ItemElementName: 'integer',
|
||||
FieldLabel: 'notRequiredField',
|
||||
Item: 2
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
};
|
||||
const parse = {
|
||||
'firstRequiredField': 'id',
|
||||
'secondRequiredField': 'name',
|
||||
};
|
||||
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
|
||||
const [result] = await models.Docuware.get('deliveryNote', null, parse);
|
||||
|
||||
expect(result.id).toEqual(1);
|
||||
expect(result.name).toEqual('myName');
|
||||
expect(result.notRequiredField).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getById()', () => {
|
||||
it('should return data', async() => {
|
||||
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
|
||||
const data = {
|
||||
data: {
|
||||
id: 1
|
||||
}
|
||||
};
|
||||
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
|
||||
const result = await models.Docuware.getById('deliveryNote', 1);
|
||||
|
||||
expect(result.id).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -39,7 +39,7 @@ describe('docuware download()', () => {
|
|||
spyOn(docuwareModel, 'checkFile').and.returnValue({});
|
||||
spyOn(axios, 'get').and.returnValue(new stream.PassThrough({objectMode: true}));
|
||||
|
||||
const result = await models.Docuware.download(ctx, ticketId, fileCabinetName);
|
||||
const result = await models.Docuware.download(ticketId, fileCabinetName);
|
||||
|
||||
expect(result[1]).toEqual('application/pdf');
|
||||
expect(result[2]).toEqual(`filename="${ticketId}.pdf"`);
|
||||
|
|
|
@ -111,7 +111,7 @@ module.exports = Self => {
|
|||
throw new UserError('Action not allowed on the test environment');
|
||||
|
||||
// delete old
|
||||
const docuwareFile = await models.Docuware.checkFile(ctx, id, fileCabinet, false);
|
||||
const docuwareFile = await models.Docuware.checkFile(id, fileCabinet, false);
|
||||
if (docuwareFile) {
|
||||
const deleteJson = {
|
||||
'Field': [{'FieldName': 'ESTADO', 'Item': 'Pendiente eliminar', 'ItemElementName': 'String'}]
|
||||
|
|
|
@ -139,7 +139,7 @@ module.exports = Self => {
|
|||
ftpClient.exec((err, response) => {
|
||||
if (err || response.error) {
|
||||
console.debug(`Error downloading checksum file... ${response.error}`);
|
||||
return reject(err);
|
||||
return reject(response.error || err);
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethod('getList', {
|
||||
description: 'Get list of the available and active notification subscriptions',
|
||||
accessType: 'READ',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'id',
|
||||
type: 'number',
|
||||
description: 'User to modify',
|
||||
http: {source: 'path'}
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/:id/getList`,
|
||||
verb: 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
Self.getList = async(id, options) => {
|
||||
const activeNotificationsMap = new Map();
|
||||
|
||||
const myOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const availableNotificationsMap = await Self.getAvailable(id, myOptions);
|
||||
const activeNotifications = await Self.app.models.NotificationSubscription.find({
|
||||
fields: ['id', 'notificationFk'],
|
||||
include: {relation: 'notification'},
|
||||
where: {userFk: id}
|
||||
}, myOptions);
|
||||
|
||||
for (active of activeNotifications) {
|
||||
activeNotificationsMap.set(active.notificationFk, {
|
||||
id: active.id,
|
||||
notificationFk: active.notificationFk,
|
||||
name: active.notification().name,
|
||||
description: active.notification().description,
|
||||
active: true
|
||||
});
|
||||
availableNotificationsMap.delete(active.notificationFk);
|
||||
}
|
||||
|
||||
return {
|
||||
active: [...activeNotificationsMap.entries()],
|
||||
available: [...availableNotificationsMap.entries()]
|
||||
};
|
||||
};
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
|
||||
describe('NotificationSubscription getList()', () => {
|
||||
it('should return a list of available and active notifications of a user', async() => {
|
||||
const userId = 9;
|
||||
const {active, available} = await models.NotificationSubscription.getList(userId);
|
||||
const notifications = await models.Notification.find({});
|
||||
const totalAvailable = notifications.length - active.length;
|
||||
|
||||
expect(active.length).toEqual(2);
|
||||
expect(available.length).toEqual(totalAvailable);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
const smtp = require('vn-print/core/smtp');
|
||||
const config = require('vn-print/core/config');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('sendToSupport', {
|
||||
description: 'Send mail to support',
|
||||
accessType: 'WRITE',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'reason',
|
||||
type: 'string',
|
||||
description: 'The reason'
|
||||
},
|
||||
{
|
||||
arg: 'additionalData',
|
||||
type: 'object',
|
||||
required: true,
|
||||
description: 'The additional data'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/send-to-support`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.sendToSupport = async(ctx, reason, additionalData) => {
|
||||
const emailUser =
|
||||
await Self.app.models.EmailUser.findById(ctx.req.accessToken.userId, {fields: ['email']});
|
||||
|
||||
let html = `<strong>Motivo</strong>:<br/>${reason}<br/>`;
|
||||
html += `<strong>Usuario</strong>:<br/>${ctx.req.accessToken.userId} ${emailUser.email}<br/>`;
|
||||
|
||||
for (const data in additionalData)
|
||||
html += `<strong>${data}</strong>:<br/>${tryParse(additionalData[data])}<br/>`;
|
||||
|
||||
const subjectReason = JSON.parse(additionalData?.httpRequest)?.data?.error;
|
||||
smtp.send({
|
||||
to: `${config.app.reportEmail}, ${emailUser.email}`,
|
||||
subject:
|
||||
'[Support-Salix] ' +
|
||||
additionalData?.frontPath + ' ' +
|
||||
subjectReason?.name + ':' +
|
||||
subjectReason?.message,
|
||||
html
|
||||
});
|
||||
};
|
||||
|
||||
function tryParse(value) {
|
||||
try {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch {}
|
||||
return JSON.stringify(value, null, ' ').split('\n').join('<br>');
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethod('getUrl', {
|
||||
description: 'Returns the colling app name',
|
||||
accessType: 'READ',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'app',
|
||||
type: 'string',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/getUrl`,
|
||||
verb: 'get'
|
||||
}
|
||||
});
|
||||
Self.getUrl = async(appName = 'salix') => {
|
||||
const {url} = await Self.app.models.Url.findOne({
|
||||
where: {
|
||||
appName,
|
||||
enviroment: process.env.NODE_ENV || 'development'
|
||||
}
|
||||
});
|
||||
return url;
|
||||
};
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
const axios = require('axios');
|
||||
const {DOMParser} = require('xmldom');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('internationalExpedition', {
|
||||
description: 'Create an expedition and return a label',
|
||||
accessType: 'WRITE',
|
||||
accepts: [{
|
||||
arg: 'expeditionFk',
|
||||
type: 'number',
|
||||
required: true
|
||||
}],
|
||||
returns: {
|
||||
type: ['object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/internationalExpedition`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.internationalExpedition = async expeditionFk => {
|
||||
const models = Self.app.models;
|
||||
|
||||
const viaexpressConfig = await models.ViaexpressConfig.findOne({
|
||||
fields: ['url']
|
||||
});
|
||||
|
||||
const renderedXml = await models.ViaexpressConfig.renderer(expeditionFk);
|
||||
const response = await axios.post(`${viaexpressConfig.url}ServicioVxClientes.asmx`, renderedXml, {
|
||||
headers: {
|
||||
'Content-Type': 'application/soap+xml; charset=utf-8'
|
||||
}
|
||||
});
|
||||
|
||||
const xmlString = response.data;
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
|
||||
const referenciaVxElement = xmlDoc.getElementsByTagName('ReferenciaVx')[0];
|
||||
const referenciaVx = referenciaVxElement.textContent;
|
||||
|
||||
return referenciaVx;
|
||||
};
|
||||
};
|
|
@ -0,0 +1,126 @@
|
|||
const fs = require('fs');
|
||||
const ejs = require('ejs');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('renderer', {
|
||||
description: 'Renders the data from an XML',
|
||||
accessType: 'READ',
|
||||
accepts: [{
|
||||
arg: 'expeditionFk',
|
||||
type: 'number',
|
||||
required: true
|
||||
}],
|
||||
returns: {
|
||||
type: ['object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/renderer`,
|
||||
verb: 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
Self.renderer = async expeditionFk => {
|
||||
const models = Self.app.models;
|
||||
|
||||
const viaexpressConfig = await models.ViaexpressConfig.findOne({
|
||||
fields: ['client', 'user', 'password', 'defaultWeight', 'deliveryType']
|
||||
});
|
||||
|
||||
const expedition = await models.Expedition.findOne({
|
||||
fields: ['id', 'ticketFk'],
|
||||
where: {id: expeditionFk},
|
||||
include: [
|
||||
{
|
||||
relation: 'ticket',
|
||||
scope: {
|
||||
fields: ['shipped', 'addressFk', 'clientFk', 'companyFk'],
|
||||
include: [
|
||||
{
|
||||
relation: 'client',
|
||||
scope: {
|
||||
fields: ['mobile', 'phone', 'email']
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'address',
|
||||
scope: {
|
||||
fields: [
|
||||
'nickname',
|
||||
'street',
|
||||
'postalCode',
|
||||
'city',
|
||||
'mobile',
|
||||
'phone',
|
||||
'provinceFk'
|
||||
],
|
||||
include: {
|
||||
relation: 'province',
|
||||
scope: {
|
||||
fields: ['name', 'countryFk'],
|
||||
include: {
|
||||
relation: 'country',
|
||||
scope: {
|
||||
fields: ['code'],
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'company',
|
||||
scope: {
|
||||
fields: ['clientFk'],
|
||||
include: {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
fields: ['socialName', 'mobile', 'phone', 'email', 'defaultAddressFk'],
|
||||
include: {
|
||||
relation: 'defaultAddress',
|
||||
scope: {
|
||||
fields: [
|
||||
'street',
|
||||
'postalCode',
|
||||
'city',
|
||||
'mobile',
|
||||
'phone',
|
||||
'provinceFk'
|
||||
],
|
||||
include: {
|
||||
relation: 'province',
|
||||
scope: {
|
||||
fields: ['name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const ticket = expedition.ticket();
|
||||
const sender = ticket.company().client();
|
||||
const shipped = ticket.shipped.toISOString();
|
||||
const data = {
|
||||
viaexpressConfig,
|
||||
sender,
|
||||
senderAddress: sender.defaultAddress(),
|
||||
client: ticket.client(),
|
||||
address: ticket.address(),
|
||||
shipped
|
||||
};
|
||||
|
||||
const template = fs.readFileSync(__dirname + '/template.ejs', 'utf-8');
|
||||
const renderedXml = ejs.render(template, data);
|
||||
return renderedXml;
|
||||
};
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
|
||||
<soap12:Body>
|
||||
<PutExpedicionInternacional xmlns="http://82.223.6.71:82">
|
||||
<ObjetoEnvio>
|
||||
<Peso><%= viaexpressConfig.defaultWeight %></Peso>
|
||||
<Bultos>1</Bultos>
|
||||
<Reembolso>0</Reembolso>
|
||||
<Fecha><%= shipped %></Fecha>
|
||||
<ConRetorno>0</ConRetorno>
|
||||
<Tipo><%= viaexpressConfig.deliveryType %></Tipo>
|
||||
<Debidos>0</Debidos>
|
||||
<Asegurado>0</Asegurado>
|
||||
<Imprimir>0</Imprimir>
|
||||
<ConDevolucionAlbaran>0</ConDevolucionAlbaran>
|
||||
<Intradia>0</Intradia>
|
||||
<Observaciones></Observaciones>
|
||||
<AlbaranRemitente></AlbaranRemitente>
|
||||
<Modo>0</Modo>
|
||||
<TextoAgencia></TextoAgencia>
|
||||
<Terminal></Terminal>
|
||||
<ObjetoRemitente>
|
||||
<RazonSocial><%= sender.socialName %></RazonSocial>
|
||||
<Domicilio><%= senderAddress.street %></Domicilio>
|
||||
<Cpostal><%= senderAddress.postalCode %></Cpostal>
|
||||
<Poblacion><%= senderAddress.city %></Poblacion>
|
||||
<Provincia><%= senderAddress.province().name %></Provincia>
|
||||
<Contacto></Contacto>
|
||||
<Telefono><%= senderAddress.mobile || senderAddress.phone || sender.mobile || sender.phone %></Telefono>
|
||||
<Email><%= sender.email %></Email>
|
||||
</ObjetoRemitente>
|
||||
<ObjetoDestinatario>
|
||||
<RazonSocial><%= address.nickname %></RazonSocial>
|
||||
<Domicilio><%= address.street %></Domicilio>
|
||||
<Cpostal><%= address.postalCode %></Cpostal>
|
||||
<Poblacion><%= address.city %></Poblacion>
|
||||
<Municipio></Municipio>
|
||||
<Provincia><%= address.province().name %></Provincia>
|
||||
<Contacto></Contacto>
|
||||
<Telefono><%= address.mobile || address.phone || client.mobile || client.phone %></Telefono>
|
||||
<Email><%= client.email %></Email>
|
||||
<Pais><%= address.province().country().code %></Pais>
|
||||
</ObjetoDestinatario>
|
||||
<ObjetoLogin>
|
||||
<IdCliente><%= viaexpressConfig.client %></IdCliente>
|
||||
<Usuario><%= viaexpressConfig.user %></Usuario>
|
||||
<Password><%= viaexpressConfig.password %></Password>
|
||||
</ObjetoLogin>
|
||||
</ObjetoEnvio>
|
||||
</PutExpedicionInternacional>
|
||||
</soap12:Body>
|
||||
</soap12:Envelope>
|
|
@ -47,7 +47,7 @@ module.exports = Self => {
|
|||
const user = await Self.findById(userId, {fields: ['hasGrant']}, myOptions);
|
||||
|
||||
const userToUpdate = await Self.findById(id, {
|
||||
fields: ['id', 'name', 'hasGrant', 'roleFk', 'password'],
|
||||
fields: ['id', 'name', 'hasGrant', 'roleFk', 'password', 'email'],
|
||||
include: {
|
||||
relation: 'role',
|
||||
scope: {
|
||||
|
|
|
@ -7,6 +7,11 @@ module.exports = Self => {
|
|||
type: 'string',
|
||||
description: 'The user name or email',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
arg: 'app',
|
||||
type: 'string',
|
||||
description: 'The directory for mail'
|
||||
}
|
||||
],
|
||||
http: {
|
||||
|
@ -15,7 +20,7 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.recoverPassword = async function(user) {
|
||||
Self.recoverPassword = async function(user, app) {
|
||||
const models = Self.app.models;
|
||||
|
||||
const usesEmail = user.indexOf('@') !== -1;
|
||||
|
@ -29,7 +34,7 @@ module.exports = Self => {
|
|||
}
|
||||
|
||||
try {
|
||||
await Self.resetPassword({email: user, emailTemplate: 'recover-password'});
|
||||
await Self.resetPassword({email: user, emailTemplate: 'recover-password', app});
|
||||
} catch (err) {
|
||||
if (err.code === 'EMAIL_NOT_FOUND')
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('renewToken', {
|
||||
description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it',
|
||||
accessType: 'WRITE',
|
||||
accepts: [],
|
||||
returns: {
|
||||
type: 'Object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/renewToken`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.renewToken = async function(ctx) {
|
||||
const models = Self.app.models;
|
||||
const token = ctx.req.accessToken;
|
||||
|
||||
const now = new Date();
|
||||
const differenceMilliseconds = now - token.created;
|
||||
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
|
||||
|
||||
const fields = ['renewPeriod', 'courtesyTime'];
|
||||
const accessTokenConfig = await models.AccessTokenConfig.findOne({fields});
|
||||
|
||||
if (differenceSeconds < accessTokenConfig.renewPeriod - accessTokenConfig.courtesyTime)
|
||||
throw new UserError(`The renew period has not been exceeded`, 'periodNotExceeded');
|
||||
|
||||
await Self.logout(token.id);
|
||||
const user = await Self.findById(token.userId);
|
||||
const accessToken = await user.createAccessToken();
|
||||
|
||||
return {id: accessToken.id, ttl: accessToken.ttl};
|
||||
};
|
||||
};
|
|
@ -0,0 +1,101 @@
|
|||
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('signIn', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'user',
|
||||
type: 'String',
|
||||
description: 'The user name or email',
|
||||
required: true
|
||||
}, {
|
||||
arg: 'password',
|
||||
type: 'String',
|
||||
description: 'The password'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/sign-in`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.signIn = async function(ctx, user, password, options) {
|
||||
const myOptions = {};
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const where = Self.userUses(user);
|
||||
const vnUser = await Self.findOne({
|
||||
fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'],
|
||||
where
|
||||
}, myOptions);
|
||||
|
||||
const validCredentials = vnUser
|
||||
&& await vnUser.hasPassword(password);
|
||||
|
||||
if (validCredentials) {
|
||||
if (!vnUser.active)
|
||||
throw new UserError('User disabled');
|
||||
await Self.sendTwoFactor(ctx, vnUser, myOptions);
|
||||
await Self.passExpired(vnUser, myOptions);
|
||||
|
||||
if (vnUser.twoFactor)
|
||||
throw new ForbiddenError(null, 'REQUIRES_2FA');
|
||||
}
|
||||
const validateLogin = await Self.validateLogin(user, password);
|
||||
await Self.app.models.SignInLog.create({
|
||||
token: validateLogin.token,
|
||||
userFk: vnUser.id,
|
||||
ip: ctx.req.ip
|
||||
});
|
||||
return validateLogin;
|
||||
};
|
||||
|
||||
Self.passExpired = async vnUser => {
|
||||
const today = Date.vnNew();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
|
||||
const err = new UserError('Pass expired', 'passExpired');
|
||||
err.details = {userId: vnUser.id, twoFactor: vnUser.twoFactor ? true : false};
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
Self.sendTwoFactor = async(ctx, vnUser, myOptions) => {
|
||||
if (vnUser.twoFactor === 'email') {
|
||||
const $ = Self.app.models;
|
||||
|
||||
const code = String(Math.floor(Math.random() * 999999));
|
||||
const maxTTL = ((60 * 1000) * 5); // 5 min
|
||||
await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
|
||||
userFk: vnUser.id,
|
||||
code: code,
|
||||
expires: Date.vnNow() + maxTTL
|
||||
}, myOptions);
|
||||
|
||||
const headers = ctx.req.headers;
|
||||
const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
|
||||
const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, '');
|
||||
const params = {
|
||||
args: {
|
||||
recipientId: vnUser.id,
|
||||
recipient: vnUser.email,
|
||||
code: code,
|
||||
ip: ctx.req?.connection?.remoteAddress,
|
||||
device: platform && browser ? platform + ', ' + browser : headers['user-agent'],
|
||||
},
|
||||
req: {getLocale: ctx.req.getLocale},
|
||||
};
|
||||
|
||||
await Self.sendTemplate(params, 'auth-code', true);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,82 +0,0 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('signIn', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'user',
|
||||
type: 'String',
|
||||
description: 'The user name or email',
|
||||
http: {source: 'form'},
|
||||
required: true
|
||||
}, {
|
||||
arg: 'password',
|
||||
type: 'String',
|
||||
description: 'The password'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/signIn`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.signIn = async function(user, password) {
|
||||
const models = Self.app.models;
|
||||
const usesEmail = user.indexOf('@') !== -1;
|
||||
let token;
|
||||
|
||||
const userInfo = usesEmail
|
||||
? {email: user}
|
||||
: {username: user};
|
||||
const instance = await Self.findOne({
|
||||
fields: ['username', 'password'],
|
||||
where: userInfo
|
||||
});
|
||||
|
||||
const where = usesEmail
|
||||
? {email: user}
|
||||
: {name: user};
|
||||
const vnUser = await Self.findOne({
|
||||
fields: ['id', 'active', 'passExpired'],
|
||||
where
|
||||
});
|
||||
|
||||
const today = Date.vnNew();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const validCredentials = instance
|
||||
&& await instance.hasPassword(password);
|
||||
|
||||
if (validCredentials) {
|
||||
if (!vnUser.active)
|
||||
throw new UserError('User disabled');
|
||||
|
||||
if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
|
||||
const changePasswordToken = await models.AccessToken.create({
|
||||
scopes: ['change-password'],
|
||||
userId: vnUser.id
|
||||
});
|
||||
throw new UserError('Pass expired', 'passExpired', {
|
||||
id: vnUser.id,
|
||||
token: changePasswordToken.id
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await models.Account.sync(instance.username, password);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
let loginInfo = Object.assign({password}, userInfo);
|
||||
token = await Self.login(loginInfo, 'user');
|
||||
return {token: token.id};
|
||||
};
|
||||
};
|
|
@ -0,0 +1,114 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('VnUser Sign-in()', () => {
|
||||
const employeeId = 1;
|
||||
const unauthCtx = {
|
||||
req: {
|
||||
headers: {},
|
||||
connection: {
|
||||
remoteAddress: '127.0.0.1'
|
||||
},
|
||||
getLocale: () => 'en'
|
||||
},
|
||||
args: {}
|
||||
};
|
||||
const {VnUser, AccessToken, SignInLog} = models;
|
||||
describe('when credentials are correct', () => {
|
||||
it('should return the token if user uses email', async() => {
|
||||
let login = await VnUser.signIn(unauthCtx, 'salesAssistant@mydomain.com', 'nightmare');
|
||||
let accessToken = await AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
let signInLog = await SignInLog.find({where: {token: accessToken.id}});
|
||||
|
||||
expect(signInLog.length).toEqual(1);
|
||||
expect(signInLog[0].userFk).toEqual(accessToken.userId);
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await VnUser.logout(ctx.req.accessToken.id);
|
||||
});
|
||||
|
||||
it('should return the token', async() => {
|
||||
let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
|
||||
let accessToken = await AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await VnUser.logout(ctx.req.accessToken.id);
|
||||
});
|
||||
|
||||
it('should return the token if the user doesnt exist but the client does', async() => {
|
||||
let login = await VnUser.signIn(unauthCtx, 'PetterParker', 'nightmare');
|
||||
let accessToken = await AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await VnUser.logout(ctx.req.accessToken.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when credentials are incorrect', () => {
|
||||
it('should throw a 401 error', async() => {
|
||||
let error;
|
||||
|
||||
try {
|
||||
await VnUser.signIn(unauthCtx, 'IDontExist', 'TotallyWrongPassword');
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(401);
|
||||
expect(error.code).toBe('LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when two-factor auth is required', () => {
|
||||
it('should throw a 403 error', async() => {
|
||||
const employee = await VnUser.findById(employeeId);
|
||||
const tx = await VnUser.beginTransaction({});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
await employee.updateAttribute('twoFactor', 'email', options);
|
||||
|
||||
await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(403);
|
||||
expect(error.code).toBe('REQUIRES_2FA');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passExpired', () => {
|
||||
it('should throw a passExpired error', async() => {
|
||||
const tx = await VnUser.beginTransaction({});
|
||||
const employee = await VnUser.findById(employeeId);
|
||||
const yesterday = Date.vnNew();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
await employee.updateAttribute('passExpired', yesterday, options);
|
||||
|
||||
await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.message).toBe('Pass expired');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('VnUser signIn()', () => {
|
||||
describe('when credentials are correct', () => {
|
||||
it('should return the token', async() => {
|
||||
let login = await models.VnUser.signIn('salesAssistant', 'nightmare');
|
||||
let accessToken = await models.AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await models.VnUser.signOut(ctx);
|
||||
});
|
||||
|
||||
it('should return the token if the user doesnt exist but the client does', async() => {
|
||||
let login = await models.VnUser.signIn('PetterParker', 'nightmare');
|
||||
let accessToken = await models.AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await models.VnUser.signOut(ctx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when credentials are incorrect', () => {
|
||||
it('should throw a 401 error', async() => {
|
||||
let error;
|
||||
|
||||
try {
|
||||
await models.VnUser.signIn('IDontExist', 'TotallyWrongPassword');
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(401);
|
||||
expect(error.code).toBe('LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('VnUser signOut()', () => {
|
||||
it('should logout and remove token after valid login', async() => {
|
||||
let loginResponse = await models.VnUser.signOut('buyer', 'nightmare');
|
||||
let accessToken = await models.AccessToken.findById(loginResponse.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
let logoutResponse = await models.VnUser.signOut(ctx);
|
||||
let tokenAfterLogout = await models.AccessToken.findById(loginResponse.token);
|
||||
|
||||
expect(logoutResponse).toBeTrue();
|
||||
expect(tokenAfterLogout).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw a 401 error when token is invalid', async() => {
|
||||
let error;
|
||||
let ctx = {req: {accessToken: {id: 'invalidToken'}}};
|
||||
|
||||
try {
|
||||
response = await models.VnUser.signOut(ctx);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should throw an error when no token is passed', async() => {
|
||||
let error;
|
||||
let ctx = {req: {accessToken: null}};
|
||||
|
||||
try {
|
||||
response = await models.VnUser.signOut(ctx);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('VnUser validate-auth()', () => {
|
||||
describe('validateAuth', () => {
|
||||
it('should signin if data is correct', async() => {
|
||||
await models.AuthCode.create({
|
||||
userFk: 9,
|
||||
code: '555555',
|
||||
expires: Date.vnNow() + (60 * 1000)
|
||||
});
|
||||
const token = await models.VnUser.validateAuth('developer', 'nightmare', '555555');
|
||||
|
||||
expect(token.token).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCode', () => {
|
||||
it('should throw an error for a non existent code', async() => {
|
||||
let error;
|
||||
try {
|
||||
await models.VnUser.validateCode('developer', '123456');
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.message).toEqual('Invalid or expired verification code');
|
||||
});
|
||||
|
||||
it('should throw an error when a code doesn`t match the login username', async() => {
|
||||
let error;
|
||||
let authCode;
|
||||
try {
|
||||
authCode = await models.AuthCode.create({
|
||||
userFk: 1,
|
||||
code: '555555',
|
||||
expires: Date.vnNow() + (60 * 1000)
|
||||
});
|
||||
|
||||
await models.VnUser.validateCode('developer', '555555');
|
||||
} catch (e) {
|
||||
authCode && await authCode.destroy();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.message).toEqual('Authentication failed');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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});
|
||||
};
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('validateAuth', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'user',
|
||||
type: 'String',
|
||||
description: 'The user name or email',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
arg: 'password',
|
||||
type: 'String',
|
||||
description: 'The password'
|
||||
},
|
||||
{
|
||||
arg: 'code',
|
||||
type: 'String',
|
||||
description: 'The auth code'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/validate-auth`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.validateAuth = async(username, password, code, options) => {
|
||||
const myOptions = {};
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const token = Self.validateLogin(username, password);
|
||||
await Self.validateCode(username, code, myOptions);
|
||||
return token;
|
||||
};
|
||||
|
||||
Self.validateCode = async(username, code, myOptions) => {
|
||||
const {AuthCode} = Self.app.models;
|
||||
|
||||
const authCode = await AuthCode.findOne({
|
||||
where: {
|
||||
code: code
|
||||
}
|
||||
}, myOptions);
|
||||
|
||||
const expired = authCode && Date.vnNow() > authCode.expires;
|
||||
if (!authCode || expired)
|
||||
throw new UserError('Invalid or expired verification code');
|
||||
|
||||
const user = await Self.findById(authCode.userFk, {
|
||||
fields: ['name', 'twoFactor']
|
||||
}, myOptions);
|
||||
|
||||
if (user.name !== username)
|
||||
throw new UserError('Authentication failed');
|
||||
|
||||
await authCode.destroy(myOptions);
|
||||
};
|
||||
};
|
|
@ -1,9 +1,23 @@
|
|||
{
|
||||
"AccessTokenConfig": {
|
||||
"dataSource": "vn",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "salix.accessTokenConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AccountingType": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"AuthCode": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"Bank": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"Buyer": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"Campaign": {
|
||||
"dataSource": "vn"
|
||||
|
@ -142,6 +156,9 @@
|
|||
},
|
||||
"PrintConfig": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"ViaexpressConfig": {
|
||||
"dataSource": "vn"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "AccessTokenConfig",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "accessTokenConfig"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"id": true,
|
||||
"description": "Identifier"
|
||||
},
|
||||
"renewPeriod": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
},
|
||||
"courtesyTime": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
},
|
||||
"renewInterval": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"acls": [{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "AuthCode",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "salix.authCode"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"userFk": {
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"id": true
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"expires": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "Account",
|
||||
"foreignKey": "userFk"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "Buyer",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "buyer"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"userFk": {
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"id": true
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "employee",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -7,17 +7,14 @@ module.exports = Self => {
|
|||
|
||||
Self.observe('before save', async function(ctx) {
|
||||
if (!ctx.isNewInstance) return;
|
||||
|
||||
let {message} = ctx.instance;
|
||||
if (!message) return;
|
||||
|
||||
const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g);
|
||||
if (!parts) return;
|
||||
|
||||
const replacedParts = parts.map(part => {
|
||||
return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, '');
|
||||
});
|
||||
|
||||
for (const [index, part] of parts.entries())
|
||||
message = message.replace(part, replacedParts[index]);
|
||||
|
||||
|
|
|
@ -4,4 +4,5 @@ module.exports = Self => {
|
|||
require('../methods/collection/getSectors')(Self);
|
||||
require('../methods/collection/setSaleQuantity')(Self);
|
||||
require('../methods/collection/previousLabel')(Self);
|
||||
require('../methods/collection/getTickets')(Self);
|
||||
};
|
||||
|
|
|
@ -18,11 +18,21 @@
|
|||
},
|
||||
"expired": {
|
||||
"type": "date"
|
||||
},
|
||||
"supplierAccountFk": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"where" :{
|
||||
"expired": null
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"client": {
|
||||
"type": "belongsTo",
|
||||
"model": "Client",
|
||||
"foreignKey": "clientFk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
},
|
||||
"isUeeMember": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isSocialNameUnique": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
|
@ -39,4 +42,4 @@
|
|||
"permission": "ALLOW"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,5 +28,12 @@
|
|||
"findById": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"dmsType": {
|
||||
"type": "belongsTo",
|
||||
"model": "DmsType",
|
||||
"foreignKey": "dmsTypeFk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,62 +1,74 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
require('../methods/notification/getList')(Self);
|
||||
|
||||
Self.observe('before save', async function(ctx) {
|
||||
await checkModifyPermission(ctx);
|
||||
});
|
||||
|
||||
Self.observe('before delete', async function(ctx) {
|
||||
await checkModifyPermission(ctx);
|
||||
});
|
||||
|
||||
async function checkModifyPermission(ctx) {
|
||||
const models = Self.app.models;
|
||||
const instance = ctx.instance;
|
||||
const userId = ctx.options.accessToken.userId;
|
||||
const user = await ctx.instance.userFk;
|
||||
const modifiedUser = await getUserToModify(null, user, models);
|
||||
|
||||
if (userId != modifiedUser.id && userId != modifiedUser.bossFk)
|
||||
throw new UserError('You dont have permission to modify this user');
|
||||
});
|
||||
let notificationFk;
|
||||
let workerId;
|
||||
|
||||
Self.remoteMethod('deleteNotification', {
|
||||
description: 'Deletes a notification subscription',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'ctx',
|
||||
type: 'object',
|
||||
http: {source: 'context'}
|
||||
},
|
||||
{
|
||||
arg: 'notificationId',
|
||||
type: 'number',
|
||||
required: true
|
||||
},
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
verb: 'POST',
|
||||
path: '/deleteNotification'
|
||||
if (instance) {
|
||||
notificationFk = instance.notificationFk;
|
||||
workerId = instance.userFk;
|
||||
} else {
|
||||
const notificationSubscription = await models.NotificationSubscription.findById(ctx.where.id);
|
||||
notificationFk = notificationSubscription.notificationFk;
|
||||
workerId = notificationSubscription.userFk;
|
||||
}
|
||||
});
|
||||
|
||||
Self.deleteNotification = async function(ctx, notificationId) {
|
||||
const models = Self.app.models;
|
||||
const user = ctx.req.accessToken.userId;
|
||||
const modifiedUser = await getUserToModify(notificationId, null, models);
|
||||
const worker = await models.Worker.findById(workerId, {fields: ['id', 'bossFk']});
|
||||
const available = await Self.getAvailable(workerId);
|
||||
const hasAcl = available.has(notificationFk);
|
||||
|
||||
if (user != modifiedUser.id && user != modifiedUser.bossFk)
|
||||
throw new UserError('You dont have permission to modify this user');
|
||||
|
||||
await models.NotificationSubscription.destroyById(notificationId);
|
||||
};
|
||||
|
||||
async function getUserToModify(notificationId, userFk, models) {
|
||||
let userToModify = userFk;
|
||||
if (notificationId) {
|
||||
const subscription = await models.NotificationSubscription.findById(notificationId);
|
||||
userToModify = subscription.userFk;
|
||||
}
|
||||
return await models.Worker.findOne({
|
||||
fields: ['id', 'bossFk'],
|
||||
where: {
|
||||
id: userToModify
|
||||
}
|
||||
});
|
||||
if (!hasAcl || (userId != worker.id && userId != worker.bossFk))
|
||||
throw new UserError('The notification subscription of this worker cant be modified');
|
||||
}
|
||||
|
||||
Self.getAvailable = async function(userId, options) {
|
||||
const availableNotificationsMap = new Map();
|
||||
const models = Self.app.models;
|
||||
|
||||
const myOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const roles = await models.RoleMapping.find({
|
||||
fields: ['roleId'],
|
||||
where: {principalId: userId}
|
||||
}, myOptions);
|
||||
|
||||
const availableNotifications = await models.NotificationAcl.find({
|
||||
fields: ['notificationFk', 'roleFk'],
|
||||
include: {relation: 'notification'},
|
||||
where: {
|
||||
roleFk: {
|
||||
inq: roles.map(role => role.roleId),
|
||||
},
|
||||
}
|
||||
}, myOptions);
|
||||
|
||||
for (available of availableNotifications) {
|
||||
availableNotificationsMap.set(available.notificationFk, {
|
||||
id: null,
|
||||
notificationFk: available.notificationFk,
|
||||
name: available.notification().name,
|
||||
description: available.notification().description,
|
||||
active: false
|
||||
});
|
||||
}
|
||||
return availableNotificationsMap;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/osticket/osTicketReportEmail')(Self);
|
||||
require('../methods/osticket/closeTicket')(Self);
|
||||
require('../methods/osticket/sendToSupport')(Self);
|
||||
};
|
||||
|
|
|
@ -1,74 +1,126 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
|
||||
describe('loopback model NotificationSubscription', () => {
|
||||
it('Should fail to delete a notification if the user is not editing itself or a subordinate', async() => {
|
||||
it('should fail to add a notification subscription if the worker doesnt have ACLs', async() => {
|
||||
const tx = await models.NotificationSubscription.beginTransaction({});
|
||||
let error;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
const user = 9;
|
||||
const options = {transaction: tx, accessToken: {userId: 9}};
|
||||
await models.NotificationSubscription.create({notificationFk: 1, userFk: 62}, options);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
|
||||
});
|
||||
|
||||
it('should fail to add a notification subscription if the user isnt editing itself or subordinate', async() => {
|
||||
const tx = await models.NotificationSubscription.beginTransaction({});
|
||||
let error;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx, accessToken: {userId: 1}};
|
||||
await models.NotificationSubscription.create({notificationFk: 1, userFk: 9}, options);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
|
||||
});
|
||||
|
||||
it('should fail to delete a notification subscription if the user isnt editing itself or subordinate', async() => {
|
||||
const tx = await models.NotificationSubscription.beginTransaction({});
|
||||
let error;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx, accessToken: {userId: 9}};
|
||||
const notificationSubscriptionId = 2;
|
||||
const ctx = {req: {accessToken: {userId: user}}};
|
||||
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
|
||||
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error.message).toContain('You dont have permission to modify this user');
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
|
||||
});
|
||||
|
||||
it('Should delete a notification if the user is editing itself', async() => {
|
||||
it('should add a notification subscription if the user is editing itself', async() => {
|
||||
const tx = await models.NotificationSubscription.beginTransaction({});
|
||||
let error;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
const user = 9;
|
||||
const options = {transaction: tx, accessToken: {userId: 9}};
|
||||
await models.NotificationSubscription.create({notificationFk: 2, userFk: 9}, options);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete a notification subscription if the user is editing itself', async() => {
|
||||
const tx = await models.NotificationSubscription.beginTransaction({});
|
||||
let error;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx, accessToken: {userId: 9}};
|
||||
const notificationSubscriptionId = 6;
|
||||
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should add a notification subscription if the user is editing a subordinate', async() => {
|
||||
const tx = await models.NotificationSubscription.beginTransaction({});
|
||||
let error;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx, accessToken: {userId: 9}};
|
||||
await models.NotificationSubscription.create({notificationFk: 1, userFk: 5}, options);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete a notification subscription if the user is editing a subordinate', async() => {
|
||||
const tx = await models.NotificationSubscription.beginTransaction({});
|
||||
let error;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx, accessToken: {userId: 19}};
|
||||
const notificationSubscriptionId = 4;
|
||||
const ctx = {req: {accessToken: {userId: user}}};
|
||||
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
|
||||
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
|
||||
|
||||
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
|
||||
|
||||
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
|
||||
|
||||
expect(deletedNotification).toBeNull();
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
error = e;
|
||||
}
|
||||
});
|
||||
|
||||
it('Should delete a notification if the user is editing a subordinate', async() => {
|
||||
const tx = await models.NotificationSubscription.beginTransaction({});
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
const user = 9;
|
||||
const notificationSubscriptionId = 5;
|
||||
const ctx = {req: {accessToken: {userId: user}}};
|
||||
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
|
||||
|
||||
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
|
||||
|
||||
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
|
||||
|
||||
expect(deletedNotification).toBeNull();
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/url/getByUser')(Self);
|
||||
require('../methods/url/getUrl')(Self);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/viaexpress-config/internationalExpedition')(Self);
|
||||
require('../methods/viaexpress-config/renderer')(Self);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "ViaexpressConfig",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "viaexpressConfig"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"client": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"defaultWeight": {
|
||||
"type": "number"
|
||||
},
|
||||
"deliveryType": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,29 @@
|
|||
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');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = function(Self) {
|
||||
vnModel(Self);
|
||||
|
||||
require('../methods/vn-user/signIn')(Self);
|
||||
require('../methods/vn-user/sign-in')(Self);
|
||||
require('../methods/vn-user/acl')(Self);
|
||||
require('../methods/vn-user/recover-password')(Self);
|
||||
require('../methods/vn-user/validate-token')(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');
|
||||
|
||||
// Validations
|
||||
|
||||
Self.validatesFormatOf('email', {
|
||||
message: 'Invalid email',
|
||||
allowNull: true,
|
||||
allowBlank: true,
|
||||
allowBlank: false,
|
||||
with: /^[\w|.|-]+@[\w|-]+(\.[\w|-]+)*(,[\w|.|-]+@[\w|-]+(\.[\w|-]+)*)*$/
|
||||
});
|
||||
|
||||
|
@ -92,11 +99,21 @@ module.exports = function(Self) {
|
|||
const headers = httpRequest.headers;
|
||||
const origin = headers.origin;
|
||||
|
||||
const defaultHash = '/reset-password?access_token=$token$';
|
||||
const recoverHashes = {
|
||||
hedera: 'verificationToken=$token$'
|
||||
};
|
||||
|
||||
const app = info.options?.app;
|
||||
let recoverHash = app ? recoverHashes[app] : defaultHash;
|
||||
recoverHash = recoverHash.replace('$token$', info.accessToken.id);
|
||||
|
||||
const user = await Self.app.models.VnUser.findById(info.user.id);
|
||||
|
||||
const params = {
|
||||
recipient: info.email,
|
||||
lang: user.lang,
|
||||
url: `${origin}/#!/reset-password?access_token=${info.accessToken.id}`
|
||||
url: origin + '/#!' + recoverHash
|
||||
};
|
||||
|
||||
const options = Object.assign({}, info.options);
|
||||
|
@ -107,6 +124,35 @@ module.exports = function(Self) {
|
|||
|
||||
return email.send();
|
||||
});
|
||||
Self.signInValidate = (user, userToken) => {
|
||||
const [[key, value]] = Object.entries(Self.userUses(user));
|
||||
if (userToken[key].toLowerCase().trim() !== value.toLowerCase().trim()) {
|
||||
console.error('ERROR!!! - Signin with other user', userToken, user);
|
||||
throw new UserError('Try again');
|
||||
}
|
||||
};
|
||||
|
||||
Self.validateLogin = async function(user, password) {
|
||||
const loginInfo = Object.assign({password}, Self.userUses(user));
|
||||
const token = await Self.login(loginInfo, 'user');
|
||||
|
||||
const userToken = await token.user.get();
|
||||
Self.signInValidate(user, userToken);
|
||||
|
||||
try {
|
||||
await Self.app.models.Account.sync(userToken.name, password);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
return {token: token.id, ttl: token.ttl};
|
||||
};
|
||||
|
||||
Self.userUses = function(user) {
|
||||
return user.indexOf('@') !== -1
|
||||
? {email: user}
|
||||
: {username: user};
|
||||
};
|
||||
|
||||
const _setPassword = Self.prototype.setPassword;
|
||||
Self.prototype.setPassword = async function(newPassword, options, cb) {
|
||||
|
@ -140,48 +186,82 @@ module.exports = function(Self) {
|
|||
}
|
||||
};
|
||||
|
||||
Self.sharedClass._methods.find(method => method.name == 'changePassword')
|
||||
.accessScopes = ['change-password'];
|
||||
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');
|
||||
|
||||
// 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 url = new URL(verifyOptions.verifyHref);
|
||||
if (process.env.NODE_ENV) url.port = '';
|
||||
|
||||
const params = {
|
||||
url: url.href,
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -13,19 +13,12 @@
|
|||
"type": "number",
|
||||
"id": true
|
||||
},
|
||||
"name": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"mysql": {
|
||||
"columnName": "name"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
"type": "string"
|
||||
},
|
||||
"roleFk": {
|
||||
"type": "number",
|
||||
|
@ -45,6 +38,9 @@
|
|||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailVerified": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"created": {
|
||||
"type": "date"
|
||||
},
|
||||
|
@ -59,7 +55,10 @@
|
|||
},
|
||||
"passExpired": {
|
||||
"type": "date"
|
||||
}
|
||||
},
|
||||
"twoFactor": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"role": {
|
||||
|
@ -81,7 +80,7 @@
|
|||
"worker": {
|
||||
"type": "hasOne",
|
||||
"model": "Worker",
|
||||
"foreignKey": "userFk"
|
||||
"foreignKey": "id"
|
||||
},
|
||||
"userConfig": {
|
||||
"type": "hasOne",
|
||||
|
@ -111,6 +110,13 @@
|
|||
"principalId": "$authenticated",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "validateAuth",
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "privileges",
|
||||
"accessType": "*",
|
||||
|
@ -118,5 +124,25 @@
|
|||
"principalId": "$authenticated",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
]
|
||||
],
|
||||
"scopes": {
|
||||
"preview": {
|
||||
"fields": [
|
||||
"id",
|
||||
"name",
|
||||
"username",
|
||||
"roleFk",
|
||||
"nickname",
|
||||
"lang",
|
||||
"active",
|
||||
"created",
|
||||
"updated",
|
||||
"image",
|
||||
"hasGrant",
|
||||
"realm",
|
||||
"email",
|
||||
"emailVerified"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,4 @@ apps:
|
|||
instances: 1
|
||||
max_restarts: 3
|
||||
restart_delay: 15000
|
||||
node_args: --tls-min-v1.0
|
||||
node_args: --tls-min-v1.0 --openssl-legacy-provider
|
||||
|
|
|
@ -34,7 +34,7 @@ BEGIN
|
|||
isAllowedToWork
|
||||
FROM(SELECT t.dated,
|
||||
b.id businessFk,
|
||||
w.userFk,
|
||||
w.id,
|
||||
b.departmentFk,
|
||||
IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.start,5) ORDER BY j.start ASC SEPARATOR ' - ')) hourStart ,
|
||||
IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.end,5) ORDER BY j.end ASC SEPARATOR ' - ')) hourEnd,
|
||||
|
@ -48,14 +48,14 @@ BEGIN
|
|||
FROM time t
|
||||
LEFT JOIN business b ON t.dated BETWEEN b.started AND IFNULL(b.ended, vDatedTo)
|
||||
LEFT JOIN worker w ON w.id = b.workerFk
|
||||
JOIN tmp.`user` u ON u.userFK = w.userFK
|
||||
JOIN tmp.`user` u ON u.userFK = w.id
|
||||
LEFT JOIN workCenter wc ON wc.id = b.workcenterFK
|
||||
LEFT JOIN postgresql.calendar_labour_type cl ON cl.calendar_labour_type_id = b.calendarTypeFk
|
||||
LEFT JOIN postgresql.journey j ON j.business_id = b.id AND j.day_id = WEEKDAY(t.dated) + 1
|
||||
LEFT JOIN postgresql.calendar_employee ce ON ce.businessFk = b.id AND ce.date = t.dated
|
||||
LEFT JOIN absenceType at2 ON at2.id = ce.calendar_state_id
|
||||
WHERE t.dated BETWEEN vDatedFrom AND vDatedTo
|
||||
GROUP BY w.userFk, t.dated
|
||||
GROUP BY w.id, t.dated
|
||||
)sub;
|
||||
|
||||
UPDATE tmp.timeBusinessCalculate t
|
||||
|
|
|
@ -74,7 +74,7 @@ BEGIN
|
|||
clientFk,
|
||||
dued,
|
||||
companyFk,
|
||||
cplusInvoiceType477Fk
|
||||
siiTypeInvoiceOutFk
|
||||
)
|
||||
SELECT
|
||||
1,
|
||||
|
|
|
@ -96,7 +96,7 @@ BEGIN
|
|||
clientFk,
|
||||
dued,
|
||||
companyFk,
|
||||
cplusInvoiceType477Fk
|
||||
siiTypeInvoiceOutFk
|
||||
)
|
||||
SELECT
|
||||
1,
|
||||
|
|
|
@ -46,7 +46,7 @@ BEGIN
|
|||
CONCAT('Cliente ', NEW.id),
|
||||
CONCAT('Recibida la documentación: ', vText)
|
||||
FROM worker w
|
||||
LEFT JOIN account.user u ON w.userFk = u.id AND u.active
|
||||
LEFT JOIN account.user u ON w.id = u.id AND u.active
|
||||
LEFT JOIN account.account ac ON ac.id = u.id
|
||||
WHERE w.id = NEW.salesPersonFk;
|
||||
END IF;
|
|
@ -1,6 +1,5 @@
|
|||
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
|
||||
VALUES
|
||||
('VnUser', '*', '*', 'ALLOW', 'ROLE', 'employee'),
|
||||
('VnUser','acl','READ','ALLOW','ROLE','account'),
|
||||
('VnUser','getCurrentUserData','READ','ALLOW','ROLE','account'),
|
||||
('VnUser','changePassword', 'WRITE', 'ALLOW', 'ROLE', 'account'),
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue