utn-cli 2.1.17 → 2.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,388 @@
1
+ const { ejecutarConsultaSIGU, crearObjetoConexionSIGU } = require('../db.js');
2
+ const { envioDeCorreo } = require('../EnvioDeCorreos.js');
3
+ const ManejadorDeErrores = require('../ManejadorDeErrores.js');
4
+
5
+ module.exports = {
6
+
7
+ NombresParalocalhost() {
8
+ return ["localhost", "::", "127.0.0.1", "::1"];
9
+ },
10
+
11
+ async googleClientId() {
12
+ const clientId = await ejecutarConsultaSIGU("SELECT `Valor` FROM `SIGU`.`SIGU_VariablesDeSistema` WHERE `Nombre` = 'Google-ClientId'");
13
+ return clientId[0]?.['Valor'] || '';
14
+ },
15
+
16
+ async palabraSecretaParaTokens() {
17
+ const palabraSecreta = await ejecutarConsultaSIGU("SELECT `Valor` FROM `SIGU`.`SIGU_VariablesDeSistema` WHERE `Nombre` = 'PalabraSecretaParaLaCreacionDeTokens'");
18
+ return palabraSecreta[0]['Valor'];
19
+ },
20
+
21
+ comparacionDeCadenas(cadena1, cadena2) {
22
+ const cadenaA = String(cadena1);
23
+ let cadenaB = String(cadena2);
24
+ const tamanioDeCadelaA = cadenaA.length;
25
+ let resultado = 0;
26
+ if (tamanioDeCadelaA !== cadenaB.length) {
27
+ cadenaB = cadenaA;
28
+ resultado = 1;
29
+ }
30
+ for (let contador = 0; contador < tamanioDeCadelaA; contador++) {
31
+ resultado |= (cadenaA.charCodeAt(contador) ^ cadenaB.charCodeAt(contador));
32
+ }
33
+ return resultado === 0;
34
+ },
35
+
36
+ async generarLastUser(Solicitud) {
37
+ const Resultado = await this.obtenerDatosDelUsuario(Solicitud.headers.authorization);
38
+ let LastUser = '';
39
+ if (Resultado) {
40
+ LastUser = Resultado.Identificador;
41
+ }
42
+ LastUser = LastUser + '@' + Solicitud.headers.host.trim()
43
+ + '@' + Solicitud.headers["user-agent"].trim()
44
+ + '@' + (Solicitud.ip || Solicitud.headers['x-forwarded-for'] || Solicitud.socket.remoteAddress);
45
+ return LastUser;
46
+ },
47
+
48
+ async obtenerDatosDelUsuario(encabezadoDeAutorizacion) {
49
+ const jwt = require('jsonwebtoken');
50
+ let token = undefined;
51
+ try {
52
+ token = encabezadoDeAutorizacion.split(" ")[1];
53
+ if (!token) {
54
+ throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorEncabezadoDeAutorizacion(), ManejadorDeErrores.obtenerNumeroDeLinea(), encabezadoDeAutorizacion);
55
+ }
56
+ } catch (error) {
57
+ console.error(error);
58
+ return false;
59
+ }
60
+ let Resultado = undefined;
61
+ try {
62
+ Resultado = await jwt.verify(token, await this.palabraSecretaParaTokens());
63
+ if (!Resultado) {
64
+ throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorVerificacionDeToken(), ManejadorDeErrores.obtenerNumeroDeLinea(), encabezadoDeAutorizacion);
65
+ }
66
+ Resultado.token = token;
67
+ const DatosDeLaPersona = await ejecutarConsultaSIGU("SELECT `Identificacion`, `Nombre`, `PrimerApellido`,\
68
+ `SegundoApellido`, CONCAT(`FechaNacimiento`) AS `FechaDeNacimiento`\
69
+ FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ?", [Resultado.Identificador]);
70
+ Resultado.Identificacion = DatosDeLaPersona[0]['Identificacion'];
71
+ Resultado.Nombre = DatosDeLaPersona[0]['Nombre'];
72
+ Resultado.PrimerApellido = DatosDeLaPersona[0]['PrimerApellido'];
73
+ Resultado.SegundoApellido = DatosDeLaPersona[0]['SegundoApellido'];
74
+ Resultado.FechaDeNacimiento = DatosDeLaPersona[0]['FechaDeNacimiento'];
75
+ return Resultado;
76
+ } catch (error) {
77
+ console.error(error);
78
+ return false;
79
+ }
80
+ },
81
+
82
+ async verificarToken(Token) {
83
+ let Resultado = undefined;
84
+ try {
85
+ Resultado = await this.obtenerDatosDelUsuario(`Bearer ${Token}`);
86
+ } catch (error) {
87
+ console.error(error);
88
+ }
89
+ return Resultado;
90
+ },
91
+
92
+ async validarTokenV2(encabezadoDeAutorizacion, permisoExtraId = undefined) {
93
+ let Resultado = undefined;
94
+ try {
95
+ Resultado = await this.obtenerDatosDelUsuario(encabezadoDeAutorizacion);
96
+ } catch (error) {
97
+ console.error(error);
98
+ }
99
+ if (Resultado) {
100
+ try {
101
+ const tokenAlmacenado = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_Sesiones`\
102
+ WHERE `Identificador` = ? AND `Token` = ?", [Resultado.Identificador, Resultado.token]);
103
+ if (tokenAlmacenado[0]['Total'] >= 1) {
104
+ const rolPermisoId = await this.permisoIdV2();
105
+ const permisos = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_PermisosPersonasV2`\
106
+ WHERE `PermisoId` = ? AND `Identificador` = ?", [rolPermisoId, Resultado.Identificador]);
107
+ if (permisos[0]['Total'] === 1) {
108
+ if (permisoExtraId) {
109
+ const ValidacionPermisoExtra = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_PermisosExtraPersonasV2`\
110
+ WHERE `PermisoExtraId` = ? AND `Identificador` = ?", [permisoExtraId, Resultado.Identificador]);
111
+ if (ValidacionPermisoExtra[0]['Total'] === 1) return true;
112
+ } else {
113
+ return true;
114
+ }
115
+ } else {
116
+ throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorPermisosInsuficientes(), ManejadorDeErrores.obtenerNumeroDeLinea());
117
+ }
118
+ } else {
119
+ throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorTokenInvalido(), ManejadorDeErrores.obtenerNumeroDeLinea());
120
+ }
121
+ } catch (error) {
122
+ console.error(error);
123
+ return false;
124
+ }
125
+ }
126
+ return false;
127
+ },
128
+
129
+ async validarToken(encabezadoDeAutorizacion) {
130
+ return await this.validarTokenV2(encabezadoDeAutorizacion);
131
+ },
132
+
133
+ async validarIdentificadorAPI(Encabezados) {
134
+ let Identificador = undefined;
135
+ try {
136
+ Identificador = Encabezados.authorization.split(" ")[1];
137
+ if (!Identificador) throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorEncabezadoDeAutorizacion(), ManejadorDeErrores.obtenerNumeroDeLinea());
138
+ } catch (error) {
139
+ console.error(error);
140
+ return false;
141
+ }
142
+ let Repositorio = undefined;
143
+ try {
144
+ Repositorio = Encabezados.referrer;
145
+ if (!Repositorio) throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorRepositorioOrigen(), ManejadorDeErrores.obtenerNumeroDeLinea());
146
+ } catch (error) {
147
+ console.error(error);
148
+ return false;
149
+ }
150
+ let ComprobacionDeRepositorio = undefined;
151
+ try {
152
+ ComprobacionDeRepositorio = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_Repositorios`\
153
+ WHERE `Repositorio` = ? AND `Identificador` = ?", [Repositorio, Identificador]);
154
+ if (ComprobacionDeRepositorio[0]['Total'] === 0) {
155
+ throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorRepositorioOrigen(), ManejadorDeErrores.obtenerNumeroDeLinea());
156
+ } else {
157
+ let ComprobacionDeAcceso = undefined;
158
+ try {
159
+ ComprobacionDeAcceso = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_RepositoriosAccesos`\
160
+ WHERE `RepositorioDestino` = ? AND `RepositorioOrigen` = ?", [this.NombreDelRepositorioDelBackend, Repositorio]);
161
+ if (ComprobacionDeAcceso[0]['Total'] === 1) return true;
162
+ else throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorPermisosInsuficientes(), ManejadorDeErrores.obtenerNumeroDeLinea());
163
+ } catch (error) {
164
+ console.error(error);
165
+ return false;
166
+ }
167
+ }
168
+ } catch (error) {
169
+ console.error(error);
170
+ return false;
171
+ }
172
+ },
173
+
174
+ async validarUsuarioYContrasenia(Cuerpo) {
175
+ const crypto = require('crypto');
176
+ const bcrypt = require('bcryptjs');
177
+ const resultados = await ejecutarConsultaSIGU("SELECT `Clave` FROM `SIGU`.`SIGU_Personas` WHERE `Activo` = TRUE AND `Identificacion` = ?", [Cuerpo.body.Identificacion]);
178
+ if (resultados.length != 1) {
179
+ throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorAccesoAAPI("El usuario " + Cuerpo.body.Identificacion + " no existe"), ManejadorDeErrores.obtenerNumeroDeLinea());
180
+ }
181
+ const Resultado = await bcrypt.compare(crypto.createHash('md5').update(Cuerpo.body.Clave).digest("hex"), resultados[0]['Clave']);
182
+ if (!Resultado) {
183
+ const Mensaje = "Desactivación del usario: " + Cuerpo.body.Identificacion + " por fallo de autenticación.";
184
+ console.log(Mensaje);
185
+ const solicitudCompleta = { metodo: Cuerpo.method, url: Cuerpo.originalUrl, ip: Cuerpo.ip, headers: Cuerpo.headers, query: Cuerpo.query, params: Cuerpo.params, body: Cuerpo.body };
186
+ const contenidoHTML = `<h2>Solicitud HTTP completa</h2><p><strong>Método:</strong> ${solicitudCompleta.metodo}</p><p><strong>URL:</strong> ${solicitudCompleta.url}</p><p><strong>IP:</strong> ${solicitudCompleta.ip}</p><h3>Headers:</h3><pre>${JSON.stringify(solicitudCompleta.headers, null, 2)}</pre><h3>Query:</h3><pre>${JSON.stringify(solicitudCompleta.query, null, 2)}</pre><h3>Params:</h3><pre>${JSON.stringify(solicitudCompleta.params, null, 2)}</pre><h3>Body:</h3><pre>${JSON.stringify(solicitudCompleta.body, null, 2)}</pre>`;
187
+ envioDeCorreo(process.env.DESTINATARIODEINFORMESDEERROR, Mensaje, contenidoHTML);
188
+ await ejecutarConsultaSIGU("UPDATE `SIGU`.`SIGU_Personas` SET `Activo` = FALSE WHERE `Identificacion` = ?", [Cuerpo.body.Identificacion]);
189
+ }
190
+ return Resultado;
191
+ },
192
+
193
+ async restablecimientoDeClave(Solicitud) {
194
+ let Clave = await ejecutarConsultaSIGU("SELECT MD5(NOW(4)) AS `Dato`");
195
+ Clave = Clave[0]['Dato'];
196
+ console.log("El usuario: " + Solicitud.body.Identificacion + " ha solicitado restablecer su clave de acceso.");
197
+ const CorreoElectronico = await ejecutarConsultaSIGU("SELECT `CorreoElectronico`, `Identificador` FROM `SIGU`.`SIGU_CorreosPersona` WHERE `Identificador` = (SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Identificacion` = ?) AND `Principal` = TRUE LIMIT 1", [Solicitud.body.Identificacion]);
198
+ const LastUser = Solicitud.body.Identificacion + '@' + Solicitud.headers.host.trim() + '@' + Solicitud.headers["user-agent"].trim() + '@' + (Solicitud.ip || Solicitud.headers['x-forwarded-for'] || Solicitud.socket.remoteAddress);
199
+ await ejecutarConsultaSIGU("REPLACE INTO `SIGU`.`SIGU_ClavesTemporalesDeLasPersonas` VALUES (?, ?, NOW(4), ?)", [CorreoElectronico[0]['Identificador'], Clave, LastUser]);
200
+ const SolicitudTextual = JSON.stringify({ timestamp: new Date().toISOString(), httpVersion: Solicitud.httpVersion, method: Solicitud.method, protocol: Solicitud.protocol, secure: Solicitud.secure, ip: Solicitud.ip, ips: Solicitud.ips, hostname: Solicitud.hostname, originalUrl: Solicitud.originalUrl, baseUrl: Solicitud.baseUrl, path: Solicitud.path, url: Solicitud.url, headers: Solicitud.headers, query: Solicitud.query, params: Solicitud.params, body: Solicitud.body, cookies: Solicitud.cookies, signedCookies: Solicitud.signedCookies, userAgent: Solicitud.get('User-Agent'), authUser: Solicitud.user ?? null });
201
+ await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_SolicitudesDeRestablecimientoDeClave` VALUES (?, ?, ?, NOW(4), ?)", [CorreoElectronico[0]['Identificador'], Solicitud.body.Identificacion, SolicitudTextual, LastUser]);
202
+ await envioDeCorreo(CorreoElectronico[0]['CorreoElectronico'], "Solicitud de restablecimiento de clave",
203
+ "<p>Estimada persona usuaria,<br /><br />Se ha realizado una solicitud de cambio de clave para su cuenta de acceso a SIGU.</p>"
204
+ + "<p>Para continuar con el cambio por favor presione <a href='" + this.EnlaceDeAcceso + "/?Identificacion=" + Solicitud.body.Identificacion + "'>aquí</a> de lo contrario omita este mensaje.</p>"
205
+ + "<p>Su nueva clave es: " + Clave + " y se le invita a cambiarla lo antes posible haciendo uso del sistema.</p>"
206
+ + "<p>La vigencia de la clave generada es de 10 minutos, luego de eso se eliminará la solicitud.</p>"
207
+ + "<p>Si usted no solicitó el cambio de contraseña por favor omita este correo.</p>");
208
+ return;
209
+ },
210
+
211
+ async AutenticarConGoogle(Solicitud) {
212
+ const { OAuth2Client } = require('google-auth-library');
213
+ const jwt = require('jsonwebtoken');
214
+ const clientId = await this.googleClientId();
215
+ const client = new OAuth2Client(clientId);
216
+ const ConexionSigu = await crearObjetoConexionSIGU();
217
+ const LastUser = await this.generarLastUser(Solicitud);
218
+ try {
219
+ const ticket = await client.verifyIdToken({ idToken: Solicitud.body.token, audience: clientId });
220
+ const payload = ticket.getPayload();
221
+ const email = payload['email'];
222
+ const resultadosEmail = await ConexionSigu.query("SELECT `Identificador` FROM `SIGU`.`SIGU_CorreosPersona` WHERE `CorreoElectronico` = ? AND `Principal` = TRUE LIMIT 1", [email]);
223
+ if (resultadosEmail[0].length === 0) {
224
+ console.log("El correo de Google", email, "no está registrado como principal en SIGU");
225
+ return { error: "Su cuenta de Google no está vinculada a ningún usuario de SIGU." };
226
+ }
227
+ const Identificador = resultadosEmail[0][0]['Identificador'];
228
+ const resultadosUsuario = await ConexionSigu.query("SELECT `Identificacion` FROM `SIGU`.`SIGU_Personas` WHERE `Identificador` = ? AND `Activo` = TRUE", [Identificador]);
229
+ if (resultadosUsuario[0].length === 0) return { error: "El usuario asociado a esta cuenta no está activo." };
230
+ const Token = await jwt.sign({ Identificador: Identificador, uid: Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '2h' });
231
+ await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_Sesiones` VALUES (?, ?, ?, NOW(4), ?) ON DUPLICATE KEY UPDATE `Token` = ?, `LastUser` = ?", [Identificador, Solicitud.headers.host.trim(), Token, LastUser, Token, LastUser]);
232
+ await ConexionSigu.query("DELETE FROM `SIGU`.`SIGU_SesionesFallidas` WHERE `Identificador` = ?", [Identificador]);
233
+ const ipUsuario = (Solicitud.headers['x-forwarded-for'] || '').split(',').shift() || Solicitud.socket?.remoteAddress || Solicitud.connection?.remoteAddress || '-';
234
+ await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_DireccionesUsadasPorLosUsuarios` (`DireccionUsadaPorElUsuario`, `Identificador`, `LastUpdate`, `LastUser`) VALUES (?, ?, NOW(4), ?) ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4), `LastUser` = ?;", [ipUsuario, Identificador, LastUser, LastUser]);
235
+ return { Token, Dominio: ((process.env.ENV || 'local') === 'production' ? '.sigu.utn.ac.cr' : '.181.193.85.44.nip.io') };
236
+ } catch (error) {
237
+ console.error("Error en AutenticarConGoogle:", error);
238
+ return { error: "Error al autenticar con Google" };
239
+ } finally {
240
+ if (ConexionSigu) await ConexionSigu.end();
241
+ }
242
+ },
243
+
244
+ async Autenticar(Solicitud) {
245
+ const crypto = require('crypto');
246
+ const bcrypt = require('bcryptjs');
247
+ const jwt = require('jsonwebtoken');
248
+ const LastUser = await this.generarLastUser(Solicitud);
249
+ const ConexionSigu = await crearObjetoConexionSIGU();
250
+ try {
251
+ const resultados = await ConexionSigu.query("SELECT `Identificador`, `Clave` FROM `SIGU`.`SIGU_Personas` WHERE `Activo` = TRUE AND `Identificacion` = ?", [Solicitud.body.Identificacion]);
252
+ const Identificador = resultados[0][0]['Identificador'];
253
+ const Resultado = await bcrypt.compare(crypto.createHash('md5').update(Solicitud.body.Clave).digest("hex"), resultados[0][0]['Clave']);
254
+ if (Resultado) {
255
+ console.log("La clave brindada para el usuario", Solicitud.body.Identificacion, "coincide");
256
+ const Token = await jwt.sign({ Identificador: Identificador, uid: Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '2h' });
257
+ await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_Sesiones` VALUES (?, ?, ?, NOW(4), ?) ON DUPLICATE KEY UPDATE `Token` = ?, `LastUser` = ?", [Identificador, Solicitud.headers.host.trim(), Token, LastUser, Token, LastUser]);
258
+ await ConexionSigu.query("DELETE FROM `SIGU`.`SIGU_SesionesFallidas` WHERE `Identificador` = ?", [Identificador]);
259
+ const permisos = await ConexionSigu.query("WITH RECURSIVE`ModulosJerarquia` AS( SELECT`Nombre`, `Padre` FROM`SIGU`.`SIGU_ModulosV2` WHERE`Nombre` COLLATE utf8mb4_spanish_ci IN( SELECT`Modulo` FROM`SIGU`.`SIGU_PermisosV2` WHERE`Nombre` LIKE '%Público%' ) UNION ALL SELECT`m`.`Nombre`, `m`.`Padre` FROM`SIGU`.`SIGU_ModulosV2` `m` INNER JOIN`ModulosJerarquia` `mj` ON`mj`.`Padre` COLLATE utf8mb4_spanish_ci = `m`.`Nombre` COLLATE utf8mb4_spanish_ci ) SELECT DISTINCT `p`.`PermisoId` FROM`ModulosJerarquia` `mj` JOIN`SIGU`.`SIGU_PermisosV2` `p` ON`p`.`Modulo` COLLATE utf8mb4_spanish_ci = `mj`.`Nombre` COLLATE utf8mb4_spanish_ci;");
260
+ for (const permiso of permisos[0]) {
261
+ await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_PermisosPersonasV2` (`PermisoId`, `Identificador`, `LastUpdate`, `LastUser`) VALUES (?, ?, NOW(4), USER()) ON DUPLICATE KEY UPDATE `LastUser` = USER(), `LastUpdate` = NOW(4);", [permiso.PermisoId, Identificador]);
262
+ }
263
+ const ipUsuario = (Solicitud.headers['x-forwarded-for'] || '').split(',').shift() || Solicitud.socket?.remoteAddress || Solicitud.connection?.remoteAddress || '-';
264
+ const ipExiste = await ConexionSigu.query("SELECT COUNT(*) AS Total FROM `SIGU`.`SIGU_DireccionesUsadasPorLosUsuarios` WHERE `Identificador` = ? AND `DireccionUsadaPorElUsuario` = ?", [Identificador, ipUsuario]);
265
+ const fechaUltimoCambio = await ConexionSigu.query("SELECT LastUpdate FROM `SIGU`.`SIGU_FechaDelUltimoCambioDeClave` WHERE `Identificador` = ?", [Identificador]);
266
+ let fechaBD = fechaUltimoCambio?.[0]?.[0]?.LastUpdate;
267
+ const fechaUltimo = new Date(fechaBD);
268
+ const fechaActual = new Date();
269
+ const diasPasados = (fechaActual - fechaUltimo) / (1000 * 60 * 60 * 24);
270
+ if (!fechaBD || diasPasados >= 60) {
271
+ await ConexionSigu.query("UPDATE`SIGU_Personas` SET`Clave` = '' where`Identificador` = ? ;", [Identificador]);
272
+ return { RequiereCambioContraseña: true };
273
+ }
274
+ if (ipExiste[0][0].Total === 0) {
275
+ let Codigo2FA = await ConexionSigu.query("SELECT UUID() AS `Dato`");
276
+ Codigo2FA = Codigo2FA[0][0].Dato;
277
+ await ConexionSigu.query("REPLACE INTO `SIGU`.`SIGU_CodigosDe2FAParaLosUsuarios` VALUES (?, ?, NOW(4), ?)", [Identificador, Codigo2FA, LastUser]);
278
+ const CorreoElectronico = await ConexionSigu.query("SELECT `CorreoElectronico` FROM `SIGU`.`SIGU_CorreosPersona` WHERE `Identificador` = ? AND `Principal` = TRUE", [Identificador]);
279
+ await envioDeCorreo(CorreoElectronico[0][0].CorreoElectronico, "Código de verificación 2FA", "<p>Hemos recibido su solicitud de acceso para 2FA.</p><p>Para continuar con el proceso, por favor utilice el siguiente código único de verificación:</p><p>" + Codigo2FA + "</p><p>Si usted no inició este proceso, puede ignorar este mensaje.</p>");
280
+ return { Requiere2FA: true };
281
+ }
282
+ await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_DireccionesUsadasPorLosUsuarios` (`DireccionUsadaPorElUsuario`, `Identificador`, `LastUpdate`, `LastUser`) VALUES (?, ?, NOW(4), ?) ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4), `LastUser` = ?;", [ipUsuario, Identificador, LastUser, LastUser]);
283
+ return { Token, Dominio: ((process.env.ENV || 'local') === 'production' ? '.sigu.utn.ac.cr' : '.svc.cluster.local') };
284
+ } else {
285
+ console.log("La clave brindada para el usuario", Solicitud.body.Identificacion, "no conincide");
286
+ const Resultados2 = await ConexionSigu.query("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_ClavesTemporalesDeLasPersonas` WHERE `Identificador` = ? AND `Clave` = ?", [Identificador, Solicitud.body.Clave]);
287
+ if (Resultados2[0][0]['Total'] > 0) {
288
+ console.log("La clave brindada para el usuario", Solicitud.body.Identificacion, "no conincide, pero coincide la clave temporal");
289
+ await ConexionSigu.query("UPDATE `SIGU`.`SIGU_Personas` SET `Clave` = ?, `LastUpdate` = NOW(4), `LastUser` = ? WHERE `Identificacion` = ?", [await bcrypt.hash(require('crypto').createHash('md5').update(Solicitud.body.Clave).digest("hex"), 10), LastUser, Solicitud.body.Identificacion]);
290
+ await ConexionSigu.query("REPLACE INTO `SIGU`.`SIGU_FechaDelUltimoCambioDeClave` VALUES (?, NOW(), ?);", [Identificador, LastUser]);
291
+ await ConexionSigu.query("DELETE FROM `SIGU`.`SIGU_ClavesTemporalesDeLasPersonas`WHERE `Identificador` = ?", [Identificador]);
292
+ return await this.Autenticar(Solicitud);
293
+ } else {
294
+ await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_SesionesFallidas` VALUES (?, ?, NOW(4))", [Identificador, Solicitud.headers.host.trim()]);
295
+ }
296
+ }
297
+ } catch (error) {
298
+ console.log(error);
299
+ return;
300
+ } finally {
301
+ if (ConexionSigu) await ConexionSigu.end();
302
+ }
303
+ return;
304
+ },
305
+
306
+ async Verificar2FA(Solicitud) {
307
+ const jwt = require('jsonwebtoken');
308
+ const ConexionSigu = await crearObjetoConexionSIGU();
309
+ const LastUser = await this.generarLastUser(Solicitud);
310
+ try {
311
+ const { Identificacion, Codigo } = Solicitud.body;
312
+ const resultados = await ConexionSigu.query("SELECT `Identificador` FROM `SIGU`.`SIGU_Personas` WHERE `Activo` = TRUE AND `Identificacion` = ?", [Identificacion]);
313
+ if (!resultados[0].length) return { error: "Usuario no encontrado" };
314
+ const Identificador = resultados[0][0]['Identificador'];
315
+ const codigoValido = await ConexionSigu.query("SELECT COUNT(*) AS Total FROM `SIGU`.`SIGU_CodigosDe2FAParaLosUsuarios` WHERE `Identificador` = ? AND `CodigoDe2FAParaElUsuario` = ?", [Identificador, Codigo]);
316
+ if (codigoValido[0][0].Total === 0) {
317
+ await ConexionSigu.query("DELETE FROM `SIGU`.`SIGU_CodigosDe2FAParaLosUsuarios` WHERE `Identificador` = ? ", [Identificador]);
318
+ return { error: "Código inválido" };
319
+ }
320
+ await ConexionSigu.query("DELETE FROM `SIGU`.`SIGU_CodigosDe2FAParaLosUsuarios` WHERE `Identificador` = ?", [Identificador]);
321
+ const ipUsuario = (Solicitud.headers['x-forwarded-for'] || '').split(',').shift() || Solicitud.socket?.remoteAddress || Solicitud.connection?.remoteAddress || '-';
322
+ await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_DireccionesUsadasPorLosUsuarios` (`DireccionUsadaPorElUsuario`, `Identificador`, `LastUpdate`, `LastUser`) VALUES (?, ?, NOW(4), ?) ON DUPLICATE KEY UPDATE `LastUpdate` = NOW(4), `LastUser` = ?;", [ipUsuario, Identificador, LastUser, LastUser]);
323
+ const Token = await jwt.sign({ Identificador: Identificador, uid: Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '2h' });
324
+ await ConexionSigu.query("INSERT INTO `SIGU`.`SIGU_Sesiones` VALUES (?, ?, ?, NOW(4), ?) ON DUPLICATE KEY UPDATE `Token` = ?, `LastUser` = ?", [Identificador, Solicitud.headers.host.trim(), Token, LastUser, Token, LastUser]);
325
+ return { Token, Dominio: ((process.env.ENV || 'local') === 'production' ? '.sigu.utn.ac.cr' : '-') };
326
+ } catch (error) {
327
+ console.log(error);
328
+ return { error: "Error verificando 2FA" };
329
+ } finally {
330
+ if (ConexionSigu) await ConexionSigu.end();
331
+ }
332
+ },
333
+
334
+ async iniciarSesion() {
335
+ return this.EnlaceDeAcceso;
336
+ },
337
+
338
+ async cerrarSesion(Token) {
339
+ let Resultado = undefined;
340
+ try {
341
+ Resultado = await this.obtenerDatosDelUsuario(Token);
342
+ if (!Resultado) throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorVerificacionDeToken(), ManejadorDeErrores.obtenerNumeroDeLinea());
343
+ } catch (error) {
344
+ console.log(error);
345
+ return;
346
+ }
347
+ await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Sesiones` WHERE `Token` = ?", [Token.split(" ")[1]]);
348
+ return this.EnlaceDeAcceso;
349
+ },
350
+
351
+ async cerrarSesionPorToken(Token) {
352
+ await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Sesiones` WHERE `Token` = ?", [Token]);
353
+ return true;
354
+ },
355
+
356
+ async crearToken(Identificador) {
357
+ let Token = undefined;
358
+ if (this.NombresParalocalhost().includes(process.env.HOST) && (typeof process.env.DB_HOST_SIGU === "undefined")) {
359
+ const jwt = require('jsonwebtoken');
360
+ Token = await jwt.sign({ uid: Identificador, Identificador }, await this.palabraSecretaParaTokens(), { expiresIn: '10h' });
361
+ await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Sesiones` WHERE `Identificador` = ?", [Identificador]);
362
+ await ejecutarConsultaSIGU("INSERT INTO `SIGU`.`SIGU_Sesiones` VALUES (?, 'Backend', ?, NOW(4), USER())", [Identificador, Token]);
363
+ }
364
+ const fs = require('fs');
365
+ const ExpresionRegular = new RegExp(/[A-Za-z0-9_-]{30,}\.[A-Za-z0-9_-]{30,}\.[A-Za-z0-9_-]{30,}/, 'g');
366
+ let ArchivoREST = process.cwd() + '/index.rest';
367
+ let contenido = fs.readFileSync(ArchivoREST, 'utf-8');
368
+ let contenidoActualizado = contenido.replace(ExpresionRegular, Token);
369
+ fs.writeFileSync(ArchivoREST, contenidoActualizado);
370
+ ArchivoREST = process.cwd() + '/' + this.NombreCanonicoDelModulo + '.rest';
371
+ contenido = fs.readFileSync(ArchivoREST, 'utf-8');
372
+ contenidoActualizado = contenido.replace(ExpresionRegular, Token);
373
+ fs.writeFileSync(ArchivoREST, contenidoActualizado);
374
+ try {
375
+ const CWD = process.cwd();
376
+ process.chdir('..');
377
+ ArchivoREST = process.cwd() + '/' + this.NombreDelRepositorioDelFrontend + '/src/app/datos-globales.service.ts';
378
+ process.chdir(CWD);
379
+ contenido = fs.readFileSync(ArchivoREST, 'utf-8');
380
+ contenidoActualizado = contenido.replace(ExpresionRegular, Token);
381
+ fs.writeFileSync(ArchivoREST, contenidoActualizado);
382
+ } catch (error) {
383
+ console.warn("No fue posible actualizar el archivo datos-globales.service.ts del front: ", error);
384
+ }
385
+ return Token;
386
+ },
387
+
388
+ };
@@ -0,0 +1,254 @@
1
+ const { ejecutarConsulta, ejecutarConsultaSIGU } = require('../db.js');
2
+ const ManejadorDeErrores = require('../ManejadorDeErrores.js');
3
+ const { envioDeCorreo } = require('../EnvioDeCorreos.js');
4
+
5
+ module.exports = {
6
+
7
+ async validarAccesoDelOrigen(Solicitud) {
8
+ if (this.NombresParalocalhost().includes(process.env.HOST) && (typeof process.env.DB_HOST_SIGU === "undefined")) {
9
+ return true;
10
+ }
11
+ const Resultado = await ejecutarConsultaSIGU("SELECT COUNT(*) AS `Total` FROM `SIGU`.`SIGU_RepositoriosAccesos`\
12
+ WHERE `RepositorioDestino` = ? AND `RepositorioOrigen` = ?"
13
+ , [this.NombreDelRepositorioDelBackend, this.NombreDelRepositorioDelFrontend]);
14
+ if (Resultado[0]['Total'] === 1) {
15
+ return true;
16
+ } else {
17
+ console.error('validarAccesoDelOrigen:', Solicitud.headers.origin, 'intentó consumir a:', this.NombreDelRepositorioDelBackend);
18
+ const Mensaje = "Error de validarAccesoDelOrigen por fallo de autorización.";
19
+ const solicitudCompleta = {
20
+ metodo: Solicitud.method,
21
+ url: Solicitud.originalUrl,
22
+ ip: Solicitud.ip,
23
+ headers: Solicitud.headers,
24
+ query: Solicitud.query,
25
+ params: Solicitud.params,
26
+ body: Solicitud.body,
27
+ };
28
+ const contenidoHTML = `
29
+ <h2>Solicitud HTTP completa</h2>
30
+ <p><strong>Método:</strong> ${solicitudCompleta.metodo}</p>
31
+ <p><strong>URL:</strong> ${solicitudCompleta.url}</p>
32
+ <p><strong>IP:</strong> ${solicitudCompleta.ip}</p>
33
+ <h3>Headers:</h3>
34
+ <pre>${JSON.stringify(solicitudCompleta.headers, null, 2)}</pre>
35
+ <h3>Query:</h3>
36
+ <pre>${JSON.stringify(solicitudCompleta.query, null, 2)}</pre>
37
+ <h3>Params:</h3>
38
+ <pre>${JSON.stringify(solicitudCompleta.params, null, 2)}</pre>
39
+ <h3>Body:</h3>
40
+ <pre>${JSON.stringify(solicitudCompleta.body, null, 2)}</pre>
41
+ `;
42
+ envioDeCorreo(process.env.DESTINATARIODEINFORMESDEERROR, Mensaje, contenidoHTML);
43
+ }
44
+ return false;
45
+ },
46
+
47
+ async obtenerDetalleDelModulo() {
48
+ const Modulos = await ejecutarConsultaSIGU("SELECT `a`.`Nombre`, `a`.`Padre`, `a`.`Descripcion`, `a`.`Detalle`,\
49
+ `a`.`Tipo`, IF(`a`.`Icono` <> '', CONCAT('https://storage.sigu.utn.ac.cr/images/cards/', `a`.`Icono`), '') AS `Icono`, `a`.`Color`, `a`.`Correo`,\
50
+ `a`.`Version`, `a`.`FechaDePublicacion`, `a`.`AcuerdoDeNivelDeServicio`, `a`.`DiccionarioDeDatos`, `a`.`Repositorios`,\
51
+ `a`.`EnlaceDelVideo`,`a`.`EnlaceDelManual`\
52
+ , `a`.`Estado`, REGEXP_SUBSTR(SUBSTRING_INDEX(`a`.`Repositorios`, '/', -1), '[^,]*front[^,]*') AS `Frontend`\
53
+ FROM `SIGU`.`SIGU_ModulosV2` `a`\
54
+ WHERE `a`.`Nombre` = ?", [this.NombreCanonicoDelModulo]);
55
+ return Modulos.map((linea) => {
56
+ linea.Enlace = this.generarEnlace(linea.Frontend);
57
+ return linea;
58
+ });
59
+ },
60
+
61
+ async registrarVistaDelManual(Token) {
62
+ const { uid } = await this.obtenerDatosDelUsuario(Token);
63
+ await ejecutarConsulta(
64
+ "INSERT INTO `DatosMiscelaneos` (`DatoMiscelaneo`, `Datos`, `LastUser`) VALUES ('VistasDelManual', JSON_OBJECT('Total', 1), ?) ON DUPLICATE KEY UPDATE `Datos` = JSON_SET(`Datos`, '$.Total', CAST(JSON_VALUE(`Datos`, '$.Total') AS UNSIGNED) + 1), `LastUser` = ?",
65
+ [uid, uid]
66
+ );
67
+ },
68
+
69
+ async obtenerMensajesModulares() {
70
+ return await ejecutarConsultaSIGU("SELECT `MensajeModularId`, `Titulo`, `Texto`, `FechaYHoraDeInicio`, `FechaYHoraDeFinalizacion` FROM `SIGU`.`SIGU_MensajesModulares` WHERE NOW(4) BETWEEN `FechaYHoraDeInicio` AND `FechaYHoraDeFinalizacion` AND `Modulo` = ?"
71
+ , [this.NombreCanonicoDelModulo]);
72
+ },
73
+
74
+ async obtenerMensajesInstitucionales() {
75
+ return await ejecutarConsultaSIGU("SELECT `MensajeInstitucionalId`, `Titulo`, `Texto`, `FechaYHoraDeInicio`, `FechaYHoraDeFinalizacion` FROM `SIGU`.`SIGU_MensajesInstitucionales` WHERE NOW(4) BETWEEN `FechaYHoraDeInicio` AND `FechaYHoraDeFinalizacion`");
76
+ },
77
+
78
+ async obtenerConsentimientoInformado(Modulo = undefined) {
79
+ if (Modulo === undefined) {
80
+ Modulo = this.NombreCanonicoDelModulo;
81
+ }
82
+ let Datos = await ejecutarConsultaSIGU("SELECT `ConsentimientoInformadoId`, `Titulo`, `Texto`, `TextoDeAceptacion` FROM `SIGU`.`SIGU_ConsentimientosInformadosV2`\
83
+ WHERE `Estado` = 'Activo' AND `Modulo` = ? AND `Titulo` <> 'Versión del módulo'", [Modulo]);
84
+ if (Datos.length === 0) {
85
+ Datos = await ejecutarConsultaSIGU("SELECT `ConsentimientoInformadoId`, `Titulo`, `Texto`, `TextoDeAceptacion` FROM `SIGU`.`SIGU_ConsentimientosInformadosV2`\
86
+ WHERE `Estado` = 'Activo' AND `Modulo` = ? AND `Titulo` <> 'Versión del módulo'", [this.NombreCanonicoDelModulo]);
87
+ }
88
+ return Datos[0];
89
+ },
90
+
91
+ async creacionDeConsetimientoInformado(Titulo, Texto, TextoDeAceptacion) {
92
+ return await ejecutarConsultaSIGU("UPDATE `SIGU`.`SIGU_ConsentimientosInformadosV2` SET `Estado` = 'Inactivo' WHERE\
93
+ `Modulo` = ? AND `Titulo` <> 'Versión del módulo'; \
94
+ SELECT MAX(`ConsentimientoInformadoId`) + 1 INTO @`NuevoId` FROM `SIGU`.`SIGU_ConsentimientosInformadosV2`; \
95
+ INSERT INTO `SIGU`.`SIGU_ConsentimientosInformadosV2` VALUES\
96
+ (@`NuevoId`, ?, ?, ?, 'Activo', ?, NOW(), USER())\
97
+ ON DUPLICATE KEY UPDATE `Estado` = 'Activo';"
98
+ , [this.NombreCanonicoDelModulo, Titulo, Texto, TextoDeAceptacion, this.NombreCanonicoDelModulo]);
99
+ },
100
+
101
+ obtenerCorreoParaReportes() {
102
+ return this.CorreoParaReportes;
103
+ },
104
+
105
+ async obtenerModulos(Token, Padre, ModulosFavoritos) {
106
+ let Resultado = undefined;
107
+ try {
108
+ Resultado = await this.obtenerDatosDelUsuario(Token);
109
+ if (!Resultado) {
110
+ throw new ManejadorDeErrores(ManejadorDeErrores.mensajeDeErrorVerificacionDeToken(), ManejadorDeErrores.obtenerNumeroDeLinea());
111
+ }
112
+ } catch (error) {
113
+ console.log(error);
114
+ return;
115
+ }
116
+ const Permisos = await ejecutarConsultaSIGU("SELECT `PermisoId` FROM `SIGU`.`SIGU_PermisosPersonasV2` WHERE `Identificador` = ?"
117
+ , [Resultado.Identificador]);
118
+ const Modulos = await ejecutarConsultaSIGU("SELECT `a`.`Nombre`, `a`.`Padre`, `a`.`Descripcion`, `a`.`Detalle`,\
119
+ `a`.`Tipo`, IF(`a`.`Icono` <> '', CONCAT('https://storage.sigu.utn.ac.cr/images/cards/', `a`.`Icono`), '') AS `Icono`, `a`.`Color`, `a`.`Correo`,\
120
+ `a`.`Version`, `a`.`FechaDePublicacion`, `a`.`AcuerdoDeNivelDeServicio`, `a`.`DiccionarioDeDatos`, `a`.`Repositorios`,\
121
+ `a`.`EnlaceDelVideo`,`a`.`EnlaceDelManual`\
122
+ , `a`.`Estado`, REGEXP_SUBSTR(SUBSTRING_INDEX(`a`.`Repositorios`, '/', -1), '[^,]*front[^,]*') AS `Frontend`\
123
+ , `c`.`Etiqueta`, `c`.`ColorDeLaEtiqueta`\
124
+ , (SELECT COUNT(*) FROM `SIGU`.`SIGU_ModulosV2` `h`\
125
+ JOIN `SIGU`.`SIGU_PermisosV2` `hp` ON (`h`.`Nombre` = `hp`.`Modulo`)\
126
+ WHERE `h`.`Estado` = 'Activo' AND `h`.`Padre` = `a`.`Nombre` AND `hp`.`PermisoId` IN (?)) AS `CantidadDeHijos`\
127
+ FROM `SIGU`.`SIGU_ModulosV2` `a`\
128
+ JOIN `SIGU`.`SIGU_PermisosV2` `b` ON (`a`.`Nombre` = `b`.`Modulo`)\
129
+ LEFT JOIN `SIGU`.`SIGU_ModulosV2InformacionExtra` `c` ON (`a`.`Nombre` = `c`.`Modulo`)\
130
+ WHERE `a`.`Estado` = 'Activo' AND `a`.`Padre` = ? AND `b`.`PermisoId` IN (?)\
131
+ ORDER BY FIELD(a.Nombre, " + ModulosFavoritos + ") DESC, a.Nombre", [Permisos.map(p => p.PermisoId), Padre, Permisos.map(p => p.PermisoId)]);
132
+ return Modulos.map((linea) => {
133
+ linea.Enlace = this.generarEnlace(linea.Frontend);
134
+ return linea;
135
+ });
136
+ },
137
+
138
+ async configurarFrontend(Token = undefined) {
139
+ let NombreUsuario = '';
140
+ if (Token) {
141
+ try {
142
+ const datosUsuario = await this.obtenerDatosDelUsuario(Token);
143
+ if (datosUsuario) {
144
+ NombreUsuario = `${datosUsuario.Nombre || ''} ${datosUsuario.PrimerApellido || ''} ${datosUsuario.SegundoApellido || ''}`.trim();
145
+ }
146
+ } catch (e) {
147
+ console.error('Error obteniendo datos de usuario para frontend:', e);
148
+ }
149
+ }
150
+ return {
151
+ "Modulo": this.NombreCanonicoDelModulo,
152
+ "Titulo": this.NombreDelModulo,
153
+ "Version": this.Version + "$$" + this.versionDelNucleo().split(' ')[0],
154
+ "Descripcion": this.DescripcionDelModulo,
155
+ "Detalle": this.DetalleDelModulo,
156
+ "NombreUsuario": NombreUsuario
157
+ };
158
+ },
159
+
160
+ async obtenerEnlaceDePortal() {
161
+ return this.EnlaceDePortal;
162
+ },
163
+
164
+ async obtenerEnlaceDePerfil() {
165
+ return this.EnlaceDePerfil;
166
+ },
167
+
168
+ async obtenerTarjetas() {
169
+ const resultados = await ejecutarConsultaSIGU(
170
+ "SELECT `Datos` FROM `SIGU`.`SIGU_ModulosV2Tarjetas` WHERE `Modulo` = ? AND JSON_EXTRACT(`Datos`, '$.Activa') = TRUE",
171
+ [this.NombreCanonicoDelModulo]
172
+ );
173
+ return resultados.map(fila => typeof fila.Datos === 'string' ? JSON.parse(fila.Datos) : fila.Datos);
174
+ },
175
+
176
+ async inicializar(solicitud) {
177
+ const ConsentimientoInformado = require('./ConsentimientoInformado.js');
178
+ const LastUser = await this.generarLastUser(solicitud);
179
+ const [configuracion, detalleDelModulo, notificaciones, mensajesModulares, consentimiento] = await Promise.all([
180
+ this.configurarFrontend(solicitud.headers.authorization),
181
+ this.obtenerDetalleDelModulo(),
182
+ this.obtenerNotificaciones(solicitud.headers.authorization),
183
+ this.obtenerMensajesModulares(),
184
+ ConsentimientoInformado.ConsentimientoInformado({ LastUser })
185
+ ]);
186
+ return {
187
+ TienePermiso: true,
188
+ ...configuracion,
189
+ Descripcion: detalleDelModulo[0]?.Descripcion ?? configuracion.Descripcion,
190
+ Detalle: detalleDelModulo[0]?.Detalle ?? configuracion.Detalle,
191
+ EnlaceDelManual: detalleDelModulo[0]?.EnlaceDelManual ?? '-',
192
+ EnlaceDelVideo: detalleDelModulo[0]?.EnlaceDelVideo ?? '-',
193
+ Notificaciones: notificaciones,
194
+ MensajesModulares: mensajesModulares,
195
+ Consentimiento: consentimiento
196
+ };
197
+ },
198
+
199
+ async obtenerTarjetasDelContenedor(token) {
200
+ const ConfiguracionDeTarjetas = require('./ConfiguracionDeTarjetas.js');
201
+ const path = require('path');
202
+ const serviciosDir = path.join(__dirname, '..');
203
+
204
+ const usuario = await this.obtenerDatosDelUsuario(token);
205
+ const [tarjetas, configuracion] = await Promise.all([
206
+ this.obtenerTarjetas(),
207
+ ConfiguracionDeTarjetas.obtener({ Identificador: usuario.uid })
208
+ ]);
209
+
210
+ const datosDeServicio = await Promise.all(
211
+ tarjetas
212
+ .filter(t => t.Archivo)
213
+ .map(async t => {
214
+ try {
215
+ const rutaArchivo = this._buscarArchivoRecursivo(serviciosDir, t.Archivo);
216
+ if (!rutaArchivo) return null;
217
+ const servicio = require(rutaArchivo);
218
+ const [cantidades, tienePermisoExtra] = await Promise.all([
219
+ typeof servicio.Cantidades === 'function' ? servicio.Cantidades(token) : null,
220
+ typeof servicio.PermisoExtra === 'function' ? servicio.PermisoExtra(token) : null
221
+ ]);
222
+ return [t['Título'], { cantidades, tienePermisoExtra }];
223
+ } catch { return null; }
224
+ })
225
+ );
226
+
227
+ const datosPorTitulo = Object.fromEntries(datosDeServicio.filter(Boolean));
228
+
229
+ const titulosAExcluir = new Set(
230
+ Object.entries(datosPorTitulo)
231
+ .filter(([, { cantidades }]) => cantidades && !(cantidades.cantidadMaxima > 0))
232
+ .map(([titulo]) => titulo)
233
+ );
234
+
235
+ return tarjetas
236
+ .filter(t => !t.RequierePermisoExtra || datosPorTitulo[t['Título']]?.tienePermisoExtra)
237
+ .filter(t => !titulosAExcluir.has(t['Título']))
238
+ .map(t => {
239
+ const config = configuracion.find(c => c.Titulo === t['Título']);
240
+ if (config) {
241
+ t['Posición'] = config.Posicion;
242
+ if (config.ColorDeBorde) t.ColorDeBorde = config.ColorDeBorde;
243
+ }
244
+ const datos = datosPorTitulo[t['Título']];
245
+ if (datos?.cantidades) {
246
+ t['Cantidad'] = datos.cantidades.cantidad;
247
+ t['CantidadMaxima'] = datos.cantidades.cantidadMaxima;
248
+ }
249
+ return t;
250
+ })
251
+ .sort((a, b) => a['Posición'] - b['Posición']);
252
+ },
253
+
254
+ };