blackcoffee2 2.1.0
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.
- package/CHANGELOG.md +664 -0
- package/LICENSE +201 -0
- package/NOTICE +25 -0
- package/README.md +246 -0
- package/apps.zip +0 -0
- package/bin/adminclient +105 -0
- package/bin/blackcoffee +133 -0
- package/cli/admin-users.js +282 -0
- package/cli/commands/app.js +561 -0
- package/cli/commands/config.js +182 -0
- package/cli/commands/db.js +257 -0
- package/cli/commands/server.js +200 -0
- package/config/applications.json +5 -0
- package/config/database.json +28 -0
- package/config/database.json.example +23 -0
- package/config/server.json +32 -0
- package/controllers/admin/AdminController.js +529 -0
- package/controllers/admin/AdminViewController.js +90 -0
- package/controllers/admin/AuthController.js +293 -0
- package/controllers/admin/DatabaseAdminController.js +218 -0
- package/core/SQLiteAdapter.js +333 -0
- package/core/appLoader.js +385 -0
- package/core/databasePoolManager.js +431 -0
- package/core/hotReload.js +363 -0
- package/data/ADMIN-README.md +145 -0
- package/data/CHANGELOG.md +48 -0
- package/data/GTK3-NODE-PROPOSALS.md +410 -0
- package/data/admin-db.js +150 -0
- package/data/admin-gui.js +452 -0
- package/data/blackcoffee_admin.db-shm +0 -0
- package/data/blackcoffee_admin.db-wal +0 -0
- package/data/migrations/001_create_admin_users.sql +33 -0
- package/docs/APP_HOOKS_HANDLER.md +432 -0
- package/docs/APP_HOOKS_REQUIREMENTS.md +588 -0
- package/docs/ARCHITECTURE.md +435 -0
- package/docs/CREAR_APP_Y_USAR_POOLS.md +1595 -0
- package/docs/EVENTS_APP_MANUAL.md +289 -0
- package/docs/INSITU_BINARY_UPLOAD_PROPOSAL.md +186 -0
- package/docs/INSITU_FIREWALL_EXCEPTION.md +187 -0
- package/docs/ROADMAP.md +242 -0
- package/docs/ROADMAP.md.backup +243 -0
- package/includes/404-hooks.js +423 -0
- package/includes/adminAuth.js +214 -0
- package/includes/adminExtension.js +53 -0
- package/includes/appHooks.js +302 -0
- package/includes/initAdminDb.js +115 -0
- package/includes/routeLoader.js +67 -0
- package/includes/sessions.js +223 -0
- package/issues/001-duplicate-module-loading.md +92 -0
- package/manuales/ADMIN_EXTENSION_COMMANDS_MANUAL.md +261 -0
- package/manuales/ADMIN_EXTENSION_HOOK_EXAMPLE.md +28 -0
- package/manuales/ADMIN_EXTENSION_INTEGRATION_MANUAL.md +232 -0
- package/manuales/CACHE_REGEX_COMMANDS.md +136 -0
- package/manuales/CACHE_SYSTEM_MAP.md +206 -0
- package/manuales/CREACION_DE_CONTROLADORES_INSITU.md +383 -0
- package/manuales/QUEUE_CLI_MODULE_MANUAL.md +289 -0
- package/manuales/QUEUE_SYSTEM_MANUAL.md +320 -0
- package/manuales/ROUTE_CACHE_MODULE_MANUAL.md +205 -0
- package/manuales/SESSION_MANAGER_GUIDE.md +529 -0
- package/manuales/SESSION_SECURITY_FLAGS.md +174 -0
- package/manuales/WAF_MODULE_MANUAL.md +229 -0
- package/manuales/after_route_handler_filter_example.md +116 -0
- package/manuales/after_route_handler_usage.md +130 -0
- package/manuales/an/303/241lisis-completo-insitu-framework.md +213 -0
- package/manuales/async_hooks_promises_guide.md +325 -0
- package/manuales/before_route_handler_filter_example.md +97 -0
- package/manuales/before_route_handler_usage.md +122 -0
- package/manuales/hooks_chaining_conditions_guide.md +261 -0
- package/manuales/hooks_filters_documentation.md +493 -0
- package/manuales/hooks_filters_documentation_en.md +493 -0
- package/manuales/hooks_vs_middlewares_comparison.md +87 -0
- package/manuales/manual-mvc-completo.md +934 -0
- package/manuales/modulos_administracion.md +89 -0
- package/manuales/router_execution_points.md +74 -0
- package/manuales/static_file_hooks_usage.md +222 -0
- package/models/AdminUserModel.js +132 -0
- package/package.json +45 -0
- package/programatically/PRoutes.js +89 -0
- package/programatically/initFlow.js +211 -0
- package/public/admin/css/db-pools.css +336 -0
- package/public/admin/css/styles.css +310 -0
- package/public/admin/database.html +312 -0
- package/public/admin/index.html +116 -0
- package/public/admin/js/app.js +470 -0
- package/public/admin/js/db-pools.js +253 -0
- package/public/admin/login.html +278 -0
- package/public/assets/css/styles.css +477 -0
- package/public/assets/js/main.js +89 -0
- package/public/index.html +136 -0
- package/public/templates/404.html +158 -0
- package/routes/admin-views.json +20 -0
- package/routes/admin.json +38 -0
- package/routes/auth.json +32 -0
- package/routes/static.json +18 -0
- package/server.js +299 -0
- package/test-aplicacion.con-logisession/BlackCoffee.js +226 -0
- package/test-aplicacion.con-logisession/SSL_SETUP.md +53 -0
- package/test-aplicacion.con-logisession/certs/ca-certificate.pem +32 -0
- package/test-aplicacion.con-logisession/certs/ca-private-key.pem +52 -0
- package/test-aplicacion.con-logisession/certs/certificate-2048.pem +22 -0
- package/test-aplicacion.con-logisession/certs/certificate.pem +32 -0
- package/test-aplicacion.con-logisession/certs/private-key-2048.pem +28 -0
- package/test-aplicacion.con-logisession/certs/private-key.pem +52 -0
- package/test-aplicacion.con-logisession/config/iaQueueSetup.js +84 -0
- package/test-aplicacion.con-logisession/config/qwen-rules.json +39 -0
- package/test-aplicacion.con-logisession/controllers/analyticsController.js +117 -0
- package/test-aplicacion.con-logisession/controllers/auth/AdminAuthController.js +142 -0
- package/test-aplicacion.con-logisession/controllers/auth/AuthController.js +439 -0
- package/test-aplicacion.con-logisession/controllers/auth/AuthViewController.js +223 -0
- package/test-aplicacion.con-logisession/controllers/endpointController.js +66 -0
- package/test-aplicacion.con-logisession/controllers/example.js +183 -0
- package/test-aplicacion.con-logisession/controllers/iaQueueController.js +367 -0
- package/test-aplicacion.con-logisession/controllers/queueController.js +206 -0
- package/test-aplicacion.con-logisession/controllers/qwenQueueController.js +197 -0
- package/test-aplicacion.con-logisession/controllers/test.js +0 -0
- package/test-aplicacion.con-logisession/controllers/tracking/EventsNoFinishController.js +78 -0
- package/test-aplicacion.con-logisession/controllers/tracking/TrackingController.js +412 -0
- package/test-aplicacion.con-logisession/controllers/tracking/TrackingControllerWithLoadModel.js +437 -0
- package/test-aplicacion.con-logisession/hooks/admin-hooks.js +20 -0
- package/test-aplicacion.con-logisession/hooks/general-hooks.js +97 -0
- package/test-aplicacion.con-logisession/hooks/queue-hooks.js +64 -0
- package/test-aplicacion.con-logisession/hooks/route-directory-hooks.js +38 -0
- package/test-aplicacion.con-logisession/hooks/security-hooks.js +24 -0
- package/test-aplicacion.con-logisession/insitu-admin-client/README.md +69 -0
- package/test-aplicacion.con-logisession/insitu-admin-client/package.json +23 -0
- package/test-aplicacion.con-logisession/insitu-admin-client.js +257 -0
- package/test-aplicacion.con-logisession/models/ExampleModel.js +88 -0
- package/test-aplicacion.con-logisession/models/QueueJobModel.js +263 -0
- package/test-aplicacion.con-logisession/models/TokenModel.js +207 -0
- package/test-aplicacion.con-logisession/models/auth/AuthModel.js +66 -0
- package/test-aplicacion.con-logisession/models/auth/UserModel.js +189 -0
- package/test-aplicacion.con-logisession/models/tracking/CompletedCartModel.js +213 -0
- package/test-aplicacion.con-logisession/models/tracking/EventModel.js +366 -0
- package/test-aplicacion.con-logisession/models/tracking/EventsNoFinishModel.js +131 -0
- package/test-aplicacion.con-logisession/models/tracking/SessionModel.js +360 -0
- package/test-aplicacion.con-logisession/models/tracking/SiteFlowModel.js +286 -0
- package/test-aplicacion.con-logisession/models/tracking/TokenModel.js +207 -0
- package/test-aplicacion.con-logisession/package-lock.json +3313 -0
- package/test-aplicacion.con-logisession/package.json +32 -0
- package/test-aplicacion.con-logisession/public/blackcoffee-welcome/index.html +1339 -0
- package/test-aplicacion.con-logisession/public/css/style.css +64 -0
- package/test-aplicacion.con-logisession/public/ejemplo-estatica/index.html +18 -0
- package/test-aplicacion.con-logisession/public/ejemplo-estatica/script.js +16 -0
- package/test-aplicacion.con-logisession/public/ejemplo-estatica/styles.css +43 -0
- package/test-aplicacion.con-logisession/public/images/logo.svg +7 -0
- package/test-aplicacion.con-logisession/public/js/main.js +67 -0
- package/test-aplicacion.con-logisession/routes/analytics-routes.json +8 -0
- package/test-aplicacion.con-logisession/routes/auth-routes.json +98 -0
- package/test-aplicacion.con-logisession/routes/blackcoffee-welcome-routes.json +20 -0
- package/test-aplicacion.con-logisession/routes/duplicate-test-routes.json.disabled +16 -0
- package/test-aplicacion.con-logisession/routes/ejemplo-estatica-routes.json +11 -0
- package/test-aplicacion.con-logisession/routes/endpoints-routes.json +8 -0
- package/test-aplicacion.con-logisession/routes/ia-queue-routes.json +26 -0
- package/test-aplicacion.con-logisession/routes/product-routes.json.disabled +20 -0
- package/test-aplicacion.con-logisession/routes/queue-routes.json +32 -0
- package/test-aplicacion.con-logisession/routes/qwen-routes.json +14 -0
- package/test-aplicacion.con-logisession/routes/static-routes.json +29 -0
- package/test-aplicacion.con-logisession/routes/tracking-routes.json +58 -0
- package/test-aplicacion.con-logisession/routes/tracking-with-loadmodel-routes.json +51 -0
- package/test-aplicacion.con-logisession/utils/dbAdapter.js +88 -0
- package/test-aplicacion.con-logisession/utils/qbWrapper.js +4 -0
- package/test-aplicacion.con-logisession/utils/queueProcessor.js +305 -0
- package/test-aplicacion.con-logisession/utils/qwenRulesService.js +131 -0
- package/test-aplicacion.con-logisession/utils/tokenHelper.js +22 -0
- package/test-aplicacion.con-logisession/views/auth/dashboard.html +443 -0
- package/test-aplicacion.con-logisession/views/auth/forgot-password.html +200 -0
- package/test-aplicacion.con-logisession/views/auth/login.html +213 -0
- package/test-aplicacion.con-logisession/views/auth/register.html +294 -0
- package/test-aplicacion.con-logisession/views/contact/form.html +47 -0
- package/test-aplicacion.con-logisession/views/products/index.html +39 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Documentación de Módulos del Sistema de Administración
|
|
2
|
+
|
|
3
|
+
Este documento describe los módulos disponibles en el servidor de administración del framework Insitu.
|
|
4
|
+
|
|
5
|
+
## Módulos Disponibles
|
|
6
|
+
|
|
7
|
+
### 1. Routes Module (`routes`)
|
|
8
|
+
- **Descripción**: Módulo para ver rutas registradas y activas
|
|
9
|
+
- **Comandos**: `routes`, `active`
|
|
10
|
+
|
|
11
|
+
### 2. System Module (`system`)
|
|
12
|
+
- **Descripción**: Módulo para información del sistema
|
|
13
|
+
- **Comandos**: `version`, `status`, `help`
|
|
14
|
+
|
|
15
|
+
### 3. Cache Module (`cachemodule`)
|
|
16
|
+
- **Descripción**: Módulo para administrar y monitorear el sistema de caché
|
|
17
|
+
- **Comandos**: `cache`, `cache-stats`, `cache-clear`, `cache-info`
|
|
18
|
+
|
|
19
|
+
### 4. Queue Management Module (`queuemanagementmodule`)
|
|
20
|
+
- **Descripción**: Módulo para gestionar las colas del sistema
|
|
21
|
+
- **Comandos**: `queues`, `queue-info`, `queue-pause`, `queue-resume`, `queue-clear`, `queue-retry-failed`
|
|
22
|
+
|
|
23
|
+
### 5. Route Cache Module (`routecachemodule`)
|
|
24
|
+
- **Descripción**: Módulo para gestionar y visualizar el cache de rutas dinámicas y estáticas
|
|
25
|
+
- **Comandos**: `route-cache`, `cache-stats`, `cache-view`, `cache-clear`, `cache-dynamic`, `cache-static`
|
|
26
|
+
|
|
27
|
+
### 6. Route Manager Module (`routemanagermodule`)
|
|
28
|
+
- **Descripción**: Módulo para crear y deshabilitar rutas de manera interactiva
|
|
29
|
+
- **Comandos**: `manage-routes`, `route-manager`, `route-mgmt`
|
|
30
|
+
|
|
31
|
+
### 7. Stats Module (`statsmodule`)
|
|
32
|
+
- **Descripción**: Módulo para mostrar estadísticas del servidor
|
|
33
|
+
- **Comandos**: `stats`, `statistics`, `requests`, `endpoints`
|
|
34
|
+
|
|
35
|
+
### 8. System Module (`systemmodule`)
|
|
36
|
+
- **Descripción**: Módulo para mostrar información del sistema
|
|
37
|
+
- **Comandos**: `sysinfo`, `system`, `resources`
|
|
38
|
+
|
|
39
|
+
### 9. Time Module (`timemodule`)
|
|
40
|
+
- **Descripción**: Módulo para mostrar hora y fecha
|
|
41
|
+
- **Comandos**: `time`, `date`
|
|
42
|
+
|
|
43
|
+
### 10. View Cache Stats Module (`viewcachestatsmodule`)
|
|
44
|
+
- **Descripción**: Módulo para mostrar estadísticas del caché de vistas
|
|
45
|
+
- **Comandos**: `view-cache`
|
|
46
|
+
|
|
47
|
+
### 11. WAF Module (`wafmodule`)
|
|
48
|
+
- **Descripción**: Módulo para mostrar estadísticas del Web Application Firewall
|
|
49
|
+
- **Comandos**: `waf-status`, `waf-stats`, `waf-blocked`, `waf-security`, `waf-block-ip`, `waf-unblock-ip`, `waf-whitelist`, `waf-blacklist`, `waf-x-headers`, `create-x-rule`, `remove-x-rule`, `save-rules`, `load-rules`, `waf-logs`, `waf-monitored`
|
|
50
|
+
|
|
51
|
+
## Conexión al Servidor de Administración
|
|
52
|
+
|
|
53
|
+
Existen dos formas principales de conectarte al servidor de administración:
|
|
54
|
+
|
|
55
|
+
### 1. Usando Netcat (nc)
|
|
56
|
+
Para conectarte al servidor de administración, usa el siguiente comando:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
nc localhost 9999
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Una vez conectado, puedes usar cualquiera de los comandos mencionados arriba para interactuar con los diferentes módulos.
|
|
63
|
+
|
|
64
|
+
### 2. Usando el Cliente Oficial de Administración
|
|
65
|
+
El framework también incluye un cliente CLI especializado para interactuar con el servidor de administración:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node icca.js
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Este cliente ofrece características adicionales como:
|
|
72
|
+
- Historial de comandos
|
|
73
|
+
- Autocompletado de comandos
|
|
74
|
+
- Reutilización de comandos anteriores
|
|
75
|
+
- Comandos especiales del cliente:
|
|
76
|
+
- `history`: Muestra el historial de comandos
|
|
77
|
+
- `clear`: Limpia la pantalla
|
|
78
|
+
- `re <número>`: Reutiliza el comando número n del historial
|
|
79
|
+
- `re <texto>`: Reutiliza el último comando que contiene el texto
|
|
80
|
+
- `help-cli`: Muestra ayuda del cliente
|
|
81
|
+
|
|
82
|
+
Opciones de conexión:
|
|
83
|
+
- `-h, --host HOST`: Especifica el host del servidor (por defecto: localhost)
|
|
84
|
+
- `-p, --port PORT`: Especifica el puerto del servidor (por defecto: 9999)
|
|
85
|
+
|
|
86
|
+
Ejemplo:
|
|
87
|
+
```bash
|
|
88
|
+
node icca.js --host localhost --port 9999
|
|
89
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Puntos de ejecución en el framework Insitu
|
|
2
|
+
|
|
3
|
+
## 1. Ejecución del método indicado por el router
|
|
4
|
+
|
|
5
|
+
El punto exacto donde se ejecuta el handler (método) asociado a una ruta se encuentra en:
|
|
6
|
+
`/home/bds/insitu-js/lib/core/server.js`
|
|
7
|
+
|
|
8
|
+
Líneas específicas:
|
|
9
|
+
- Línea 906: `await matchedRoute.route.handler(req, res);` (solo para solicitudes OPTIONS)
|
|
10
|
+
- Línea 958: `await matchedRoute.route.handler(req, res);` (para otros métodos HTTP)
|
|
11
|
+
|
|
12
|
+
Esto ocurre después de que el sistema encuentra una ruta coincidente (`matchedRoute`) y antes de disparar el hook correspondiente (`handleRouteHandlerExecuted`).
|
|
13
|
+
|
|
14
|
+
### Hooks y filtros disponibles
|
|
15
|
+
|
|
16
|
+
Después de las modificaciones realizadas, ahora existen hooks y filtros disponibles para la ejecución de handlers en la línea 958 (solicitudes que no sean OPTIONS):
|
|
17
|
+
|
|
18
|
+
- **Antes de la ejecución del handler** (línea ~965):
|
|
19
|
+
- Hook de acción: `before_route_handler`
|
|
20
|
+
- Filtro: `before_route_handler` (permite modificar req/res)
|
|
21
|
+
|
|
22
|
+
- **Después de la ejecución del handler** (línea ~975):
|
|
23
|
+
- Hook de acción: `after_route_handler`
|
|
24
|
+
- Filtro: `after_route_handler` (permite modificar req/res)
|
|
25
|
+
|
|
26
|
+
Estos hooks y filtros **no se aplican** a las solicitudes OPTIONS (línea 906).
|
|
27
|
+
|
|
28
|
+
### Explicación de la duplicación
|
|
29
|
+
|
|
30
|
+
La duplicación del código `await matchedRoute.route.handler(req, res);` en las líneas 906 y 958 se debe a que hay dos caminos de ejecución diferentes en la función `handleRequest`:
|
|
31
|
+
|
|
32
|
+
1. **Línea 906**: Se ejecuta en el caso de solicitudes **OPTIONS** que tienen un handler específico registrado para esa ruta. Esta parte del código está dentro de un bloque condicional que maneja específicamente el método HTTP OPTIONS.
|
|
33
|
+
|
|
34
|
+
2. **Línea 958**: Se ejecuta para otros métodos HTTP (GET, POST, PUT, etc.) que no sean OPTIONS, en el flujo normal de procesamiento de rutas.
|
|
35
|
+
|
|
36
|
+
El código está estructurado de esta manera para manejar de forma diferente las solicitudes OPTIONS (que son parte del mecanismo CORS) respecto a otros tipos de solicitudes. Las solicitudes OPTIONS pueden requerir un tratamiento especial en el contexto de CORS, por lo que se separaron en diferentes ramas de lógica.
|
|
37
|
+
|
|
38
|
+
Aunque la instrucción es idéntica en ambos lugares, están en contextos ligeramente diferentes para permitir un manejo específico de solicitudes OPTIONS versus otros métodos HTTP.
|
|
39
|
+
|
|
40
|
+
## 2. Carga de archivos estáticos
|
|
41
|
+
|
|
42
|
+
El punto exacto donde se carga un archivo estático se encuentra en el método `createStaticFileHandler` en:
|
|
43
|
+
`/home/bds/insitu-js/lib/core/server.js`
|
|
44
|
+
|
|
45
|
+
Bloque de código clave para archivos índice (líneas aproximadas 400-415):
|
|
46
|
+
```javascript
|
|
47
|
+
const fileContent = await fs.promises.readFile(indexPath);
|
|
48
|
+
const mimeType = getMimeType(indexPath);
|
|
49
|
+
|
|
50
|
+
this.setStaticFileHeaders(res, mimeType, staticConfig);
|
|
51
|
+
|
|
52
|
+
res.writeHead(200);
|
|
53
|
+
res.end(fileContent);
|
|
54
|
+
|
|
55
|
+
// Disparar hook después de servir archivo
|
|
56
|
+
StaticFileHooksHandler.handleStaticFileServed(this, indexPath, req, res);
|
|
57
|
+
return;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Y también para archivos normales (líneas aproximadas 490-495):
|
|
61
|
+
```javascript
|
|
62
|
+
const fileContent = await fs.promises.readFile(physicalPath);
|
|
63
|
+
const mimeType = getMimeType(physicalPath);
|
|
64
|
+
|
|
65
|
+
this.setStaticFileHeaders(res, mimeType, staticConfig);
|
|
66
|
+
|
|
67
|
+
res.writeHead(200);
|
|
68
|
+
res.end(fileContent);
|
|
69
|
+
|
|
70
|
+
// Disparar hook después de servir archivo
|
|
71
|
+
StaticFileHooksHandler.handleStaticFileServed(this, physicalPath, req, res);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Estos bloques de código se ejecutan cuando una ruta estática coincide con la solicitud y se debe servir un archivo físico desde el sistema de archivos.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Cómo usar los hooks `before_static_file_load` y `after_static_file_load`
|
|
2
|
+
|
|
3
|
+
Los hooks `before_static_file_load` y `after_static_file_load` permiten interceptar y manipular la carga de archivos estáticos en el framework Insitu. Estos hooks se ejecutan en ambos puntos donde se cargan archivos estáticos: tanto para archivos normales como para archivos índice (como `index.html`).
|
|
4
|
+
|
|
5
|
+
## Hook `before_static_file_load`
|
|
6
|
+
|
|
7
|
+
Este hook se ejecuta justo antes de que se lea un archivo estático del sistema de archivos.
|
|
8
|
+
|
|
9
|
+
### Ejemplo 1: Uso como acción para registrar accesos
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// Registrar cada vez que se va a cargar un archivo estático
|
|
13
|
+
hooks.addAction('before_static_file_load', (req, res, filePath, staticConfig) => {
|
|
14
|
+
console.log(`Cargando archivo estático: ${filePath}`);
|
|
15
|
+
console.log(`URL solicitada: ${req.url}`);
|
|
16
|
+
}, 10, 4);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Ejemplo 2: Uso como filtro para modificar la respuesta
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
// Añadir headers personalizados antes de cargar el archivo
|
|
23
|
+
hooks.addFilter('before_static_file_load', (data, req, res, filePath, staticConfig) => {
|
|
24
|
+
// Añadir header para indicar que es un archivo estático
|
|
25
|
+
res.setHeader('X-Static-File', 'true');
|
|
26
|
+
|
|
27
|
+
// Añadir header con la ruta del archivo
|
|
28
|
+
res.setHeader('X-File-Path', filePath);
|
|
29
|
+
|
|
30
|
+
// Devolver el objeto modificado
|
|
31
|
+
return {
|
|
32
|
+
...data,
|
|
33
|
+
res: res
|
|
34
|
+
};
|
|
35
|
+
}, 10, 4);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Ejemplo 3: Uso como filtro para implementar autenticación en archivos estáticos
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
// Verificar permisos antes de servir archivos estáticos
|
|
42
|
+
hooks.addFilter('before_static_file_load', (data, req, res, filePath, staticConfig) => {
|
|
43
|
+
// Verificar si el archivo está protegido
|
|
44
|
+
if (isProtectedFile(filePath)) {
|
|
45
|
+
// Aquí podrías implementar tu lógica de autenticación
|
|
46
|
+
const isAuthenticated = checkAuthentication(req);
|
|
47
|
+
|
|
48
|
+
if (!isAuthenticated) {
|
|
49
|
+
// Si no está autenticado, enviar respuesta de error
|
|
50
|
+
if (!res.headersSent) {
|
|
51
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
52
|
+
res.end('Acceso prohibido');
|
|
53
|
+
}
|
|
54
|
+
return data; // Retorna sin cargar el archivo
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Si está autorizado, continuar normalmente
|
|
59
|
+
return data;
|
|
60
|
+
}, 5, 4);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Ejemplo 4: Uso como filtro para modificar la configuración estática
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// Modificar la configuración estática basada en la solicitud
|
|
67
|
+
hooks.addFilter('before_static_file_load', (data, req, res, filePath, staticConfig) => {
|
|
68
|
+
// Modificar la configuración basada en el tipo de usuario
|
|
69
|
+
if (req.userRole === 'admin') {
|
|
70
|
+
// Permitir acceso a archivos adicionales para administradores
|
|
71
|
+
const modifiedConfig = {
|
|
72
|
+
...staticConfig,
|
|
73
|
+
// Añadir configuraciones específicas para admins
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...data,
|
|
78
|
+
staticConfig: modifiedConfig
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return data;
|
|
83
|
+
}, 15, 4);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Hook `after_static_file_load`
|
|
87
|
+
|
|
88
|
+
Este hook se ejecuta después de que se ha leído el archivo del sistema de archivos pero antes de enviarlo al cliente.
|
|
89
|
+
|
|
90
|
+
### Ejemplo 1: Uso como acción para registrar métricas
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// Registrar cada vez que se completa la carga de un archivo estático
|
|
94
|
+
hooks.addAction('after_static_file_load', (req, res, filePath, staticConfig) => {
|
|
95
|
+
console.log(`Archivo estático cargado: ${filePath}`);
|
|
96
|
+
console.log(`Tamaño: ${res.getHeader('Content-Length')} bytes`);
|
|
97
|
+
}, 10, 4);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Ejemplo 2: Uso como filtro para modificar el contenido del archivo
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
// Modificar el contenido del archivo antes de enviarlo (solo para ciertos tipos de archivo)
|
|
104
|
+
hooks.addFilter('after_static_file_load', (data, req, res, filePath, staticConfig, fileContent) => {
|
|
105
|
+
// Solo para archivos HTML
|
|
106
|
+
if (filePath.endsWith('.html') && !res.headersSent) {
|
|
107
|
+
// Acceder al contenido del archivo
|
|
108
|
+
let content = data.fileContent.toString();
|
|
109
|
+
|
|
110
|
+
// Modificar el contenido (por ejemplo, inyectar un script)
|
|
111
|
+
if (content.includes('</body>')) {
|
|
112
|
+
content = content.replace('</body>', '<script>console.log("Archivo modificado por el hook");</script></body>');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Devolver el objeto con el contenido modificado
|
|
116
|
+
return {
|
|
117
|
+
...data,
|
|
118
|
+
fileContent: Buffer.from(content)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return data;
|
|
123
|
+
}, 10, 5);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Ejemplo 2b: Uso con Cheerio para manipulación HTML más avanzada
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
const cheerio = require('cheerio'); // Necesitas instalar cheerio: npm install cheerio
|
|
130
|
+
|
|
131
|
+
// Manipular HTML usando Cheerio para inyección de scripts o estilos
|
|
132
|
+
hooks.addFilter('after_static_file_load', (data, req, res, filePath, staticConfig, fileContent) => {
|
|
133
|
+
// Solo para archivos HTML
|
|
134
|
+
if (filePath.endsWith('.html') && !res.headersSent) {
|
|
135
|
+
try {
|
|
136
|
+
// Convertir el buffer a string y cargarlo en Cheerio
|
|
137
|
+
const htmlString = data.fileContent.toString();
|
|
138
|
+
const $ = cheerio.load(htmlString);
|
|
139
|
+
|
|
140
|
+
// Añadir un script al head
|
|
141
|
+
$('head').append('<script src="/assets/custom-script.js"></script>');
|
|
142
|
+
|
|
143
|
+
// Añadir un estilo personalizado
|
|
144
|
+
$('body').prepend('<style>.custom-class { color: blue; }</style>');
|
|
145
|
+
|
|
146
|
+
// Devolver el HTML modificado
|
|
147
|
+
const modifiedHtml = $.html();
|
|
148
|
+
return {
|
|
149
|
+
...data,
|
|
150
|
+
fileContent: Buffer.from(modifiedHtml)
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Error al manipular el archivo HTML:', error);
|
|
154
|
+
// Devolver el contenido original si hay un error
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return data;
|
|
160
|
+
}, 10, 5);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Ejemplo 3: Uso como filtro para auditoría post-carga
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
// Registrar información de auditoría después de cargar el archivo
|
|
167
|
+
hooks.addFilter('after_static_file_load', (data, req, res, filePath, staticConfig) => {
|
|
168
|
+
const auditLog = {
|
|
169
|
+
timestamp: new Date().toISOString(),
|
|
170
|
+
userId: req.userId || 'anonymous',
|
|
171
|
+
filePath: filePath,
|
|
172
|
+
url: req.url,
|
|
173
|
+
method: req.method,
|
|
174
|
+
statusCode: res.statusCode || 200
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Guardar registro de auditoría
|
|
178
|
+
console.log('STATIC_FILE_AUDIT:', JSON.stringify(auditLog));
|
|
179
|
+
|
|
180
|
+
return data;
|
|
181
|
+
}, 5, 4);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Ejemplo 4: Uso como filtro para compresión condicional
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
// Aplicar compresión condicional después de cargar el archivo
|
|
188
|
+
hooks.addFilter('after_static_file_load', (data, req, res, filePath, staticConfig) => {
|
|
189
|
+
// Verificar si el cliente admite compresión
|
|
190
|
+
const acceptEncoding = req.headers['accept-encoding'];
|
|
191
|
+
if (acceptEncoding && acceptEncoding.includes('gzip')) {
|
|
192
|
+
// Aquí podrías aplicar compresión gzip si el archivo es grande
|
|
193
|
+
// y el tipo de archivo es apropiado para compresión
|
|
194
|
+
if (shouldCompressFile(filePath)) {
|
|
195
|
+
res.setHeader('Content-Encoding', 'gzip');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return data;
|
|
200
|
+
}, 15, 4);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Parámetros importantes:
|
|
204
|
+
|
|
205
|
+
- El hook `before_static_file_load` recibe: `(data, req, res, filePath, staticConfig)` para los filtros
|
|
206
|
+
- El hook `after_static_file_load` recibe: `(data, req, res, filePath, staticConfig, fileContent)` para los filtros
|
|
207
|
+
- El objeto `data` incluye: `{ req, res, filePath, staticConfig, fileContent }`
|
|
208
|
+
- Las acciones para `before_static_file_load` reciben: `(req, res, filePath, staticConfig)`
|
|
209
|
+
- Las acciones para `after_static_file_load` reciben: `(req, res, filePath, staticConfig, fileContent)`
|
|
210
|
+
- El parámetro `acceptedArgs` define cuántos argumentos acepta tu callback (normalmente 4 para before y 5 para after)
|
|
211
|
+
- El parámetro `priority` define el orden de ejecución (valores más bajos se ejecutan primero)
|
|
212
|
+
- El hook se aplica tanto a archivos normales como a archivos índice (como `index.html`)
|
|
213
|
+
|
|
214
|
+
## Consideraciones importantes:
|
|
215
|
+
|
|
216
|
+
1. **Estado de la respuesta**: En `after_static_file_load`, el archivo ya ha sido leído pero aún no enviado, por lo que puedes modificar encabezados pero no el contenido sin una implementación adicional.
|
|
217
|
+
|
|
218
|
+
2. **Rendimiento**: Ten cuidado al implementar lógica pesada en estos hooks ya que afectan el rendimiento de carga de archivos estáticos.
|
|
219
|
+
|
|
220
|
+
3. **Seguridad**: Estos hooks son ideales para implementar controles de acceso, autenticación y auditoría en archivos estáticos.
|
|
221
|
+
|
|
222
|
+
Estos hooks proporcionan una potente capacidad de personalización del comportamiento del framework al servir archivos estáticos, permitiendo implementar lógica de autenticación, auditoría, modificación de headers y más.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelo de Usuario Administrador para BlackCoffee
|
|
3
|
+
* Gestiona la tabla admin_users en SQLite
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { ModelBase } = require('insitu-js');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
class AdminUserModel extends ModelBase {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
super({
|
|
13
|
+
tableName: 'admin_users',
|
|
14
|
+
adapter: options.adapter || 'sqlite-pool',
|
|
15
|
+
fields: ['id', 'username', 'password_hash', 'email', 'role', 'active', 'created_at', 'updated_at', 'last_login']
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hash de contraseña usando crypto
|
|
21
|
+
*/
|
|
22
|
+
static hashPassword(password) {
|
|
23
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
24
|
+
const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
|
|
25
|
+
return `${salt}:${hash}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Verificar contraseña contra el hash almacenado
|
|
30
|
+
*/
|
|
31
|
+
static verifyPassword(password, storedHash) {
|
|
32
|
+
try {
|
|
33
|
+
const [salt, hash] = storedHash.split(':');
|
|
34
|
+
const verifyHash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
|
|
35
|
+
return hash === verifyHash;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Buscar usuario por username
|
|
43
|
+
*/
|
|
44
|
+
async findByUsername(username) {
|
|
45
|
+
if (!this.adapter) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const selectSQL = `SELECT * FROM ${this.tableName} WHERE username = ?`;
|
|
50
|
+
const results = await this.adapter.query(selectSQL, [username]);
|
|
51
|
+
return results[0] || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Buscar usuario por username y verificar password
|
|
56
|
+
*/
|
|
57
|
+
async findByCredentials(username, password) {
|
|
58
|
+
const user = await this.findByUsername(username);
|
|
59
|
+
|
|
60
|
+
if (!user || !user.active) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (AdminUserModel.verifyPassword(password, user.password_hash)) {
|
|
65
|
+
// Actualizar último login usando query directa
|
|
66
|
+
try {
|
|
67
|
+
const updateSQL = `UPDATE ${this.tableName} SET last_login = ?, updated_at = ? WHERE id = ?`;
|
|
68
|
+
await this.adapter.query(updateSQL, [new Date().toISOString(), new Date().toISOString(), user.id]);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('Error actualizando last_login:', err.message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Devolver usuario sin el hash
|
|
74
|
+
const { password_hash, ...userWithoutPassword } = user;
|
|
75
|
+
return userWithoutPassword;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Buscar por ID
|
|
83
|
+
*/
|
|
84
|
+
async findById(id) {
|
|
85
|
+
if (!this.adapter) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const selectSQL = `SELECT * FROM ${this.tableName} WHERE id = ?`;
|
|
90
|
+
const results = await this.adapter.query(selectSQL, [id]);
|
|
91
|
+
return results[0] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Crear usuario con password hasheado
|
|
96
|
+
*/
|
|
97
|
+
async createWithPassword(userData) {
|
|
98
|
+
const { password, ...restData } = userData;
|
|
99
|
+
|
|
100
|
+
const userWithHash = {
|
|
101
|
+
...restData,
|
|
102
|
+
password_hash: AdminUserModel.hashPassword(password),
|
|
103
|
+
active: userData.active !== undefined ? userData.active : 1,
|
|
104
|
+
role: userData.role || 'admin',
|
|
105
|
+
created_at: new Date().toISOString(),
|
|
106
|
+
updated_at: new Date().toISOString()
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const id = await this.create(userWithHash);
|
|
110
|
+
return { id, ...userWithHash };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Cambiar contraseña
|
|
115
|
+
*/
|
|
116
|
+
async changePassword(userId, newPassword) {
|
|
117
|
+
const updateSQL = `UPDATE ${this.tableName} SET password_hash = ?, updated_at = ? WHERE id = ?`;
|
|
118
|
+
const hash = AdminUserModel.hashPassword(newPassword);
|
|
119
|
+
await this.adapter.query(updateSQL, [hash, new Date().toISOString(), userId]);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Verificar si existe el usuario
|
|
125
|
+
*/
|
|
126
|
+
async usernameExists(username) {
|
|
127
|
+
const user = await this.findByUsername(username);
|
|
128
|
+
return !!user;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = AdminUserModel;
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blackcoffee2",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "A app server built with Insitu Framework",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"author": "Benjamin Sanchez Cardenas <bytedogssyndicate@gmail.com>",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://gitlab.com/bytedogssyndicate1/bc2"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node server.js",
|
|
14
|
+
"admin": "node bin/adminclient",
|
|
15
|
+
"admin:connect": "node bin/adminclient",
|
|
16
|
+
"dev": "node --watch server.js",
|
|
17
|
+
"cli": "node bin/blackcoffee"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"blackcoffee": "./bin/blackcoffee"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"better-sqlite3": "^12.6.2",
|
|
24
|
+
"chokidar": "^5.0.0",
|
|
25
|
+
"gtk3-node": "^1.3.0",
|
|
26
|
+
"insitu-js": "^1.3.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.0.0"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"blackcoffee",
|
|
33
|
+
"insitu",
|
|
34
|
+
"application-server",
|
|
35
|
+
"app-server",
|
|
36
|
+
"https-server",
|
|
37
|
+
"rest-api",
|
|
38
|
+
"nodejs",
|
|
39
|
+
"web-server",
|
|
40
|
+
"backend",
|
|
41
|
+
"framework",
|
|
42
|
+
"microservices",
|
|
43
|
+
"api-server"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rutas registradas programáticamente en BlackCoffee v2.0.0
|
|
3
|
+
*
|
|
4
|
+
* Este archivo está reservado para rutas programáticas del núcleo.
|
|
5
|
+
* Los desarrolladores NO deben modificar este archivo.
|
|
6
|
+
*
|
|
7
|
+
* Para agregar rutas, usar:
|
|
8
|
+
* - routes/*.json para rutas declarativas
|
|
9
|
+
* - controllers/ para controladores
|
|
10
|
+
* - apps/ para aplicaciones desplegadas
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registra las rutas programáticas del núcleo
|
|
17
|
+
* @param {Object} server - Instancia del servidor APIServer
|
|
18
|
+
*/
|
|
19
|
+
function PRoutes(server) {
|
|
20
|
+
console.log('📋 Registrando rutas del núcleo...\n');
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Rutas de Administración de Base de Datos
|
|
24
|
+
// ============================================
|
|
25
|
+
const DatabaseAdminController = require('../controllers/admin/DatabaseAdminController');
|
|
26
|
+
|
|
27
|
+
// API: Listar pools
|
|
28
|
+
server.addRoute('GET', '/api/admin/database/pools', (req, res) => {
|
|
29
|
+
DatabaseAdminController.getPools(req, res);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// API: Crear pool
|
|
33
|
+
server.addRoute('POST', '/api/admin/database/pools', (req, res) => {
|
|
34
|
+
DatabaseAdminController.createPool(req, res);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// API: Eliminar pool
|
|
38
|
+
server.addRoute('DELETE', '/api/admin/database/pools/:name', (req, res) => {
|
|
39
|
+
DatabaseAdminController.removePool(req, res);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// API: Conectar pool
|
|
43
|
+
server.addRoute('POST', '/api/admin/database/pools/:name/connect', (req, res) => {
|
|
44
|
+
DatabaseAdminController.connectPool(req, res);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// API: Desconectar pool
|
|
48
|
+
server.addRoute('POST', '/api/admin/database/pools/:name/disconnect', (req, res) => {
|
|
49
|
+
DatabaseAdminController.disconnectPool(req, res);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// UI: Página de Database Pools
|
|
53
|
+
server.addRoute('GET', '/admin/database', (req, res) => {
|
|
54
|
+
const fs = require('fs');
|
|
55
|
+
const dbPagePath = path.join(__dirname, '..', 'public', 'admin', 'database.html');
|
|
56
|
+
|
|
57
|
+
if (fs.existsSync(dbPagePath)) {
|
|
58
|
+
const html = fs.readFileSync(dbPagePath, 'utf8');
|
|
59
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
60
|
+
res.end(html);
|
|
61
|
+
} else {
|
|
62
|
+
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
63
|
+
res.end('<h1>404 - Database Manager Page not found</h1>');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// UI: CSS para Database Pools
|
|
68
|
+
server.addRoute('GET', '-/admin/css/db-pools.css', {
|
|
69
|
+
static: {
|
|
70
|
+
dir: path.join(__dirname, '..', 'public', 'admin', 'css'),
|
|
71
|
+
cacheControl: 'public, max-age=31536000'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// UI: JS para Database Pools
|
|
76
|
+
server.addRoute('GET', '/admin/js/db-pools.js', {
|
|
77
|
+
static: {
|
|
78
|
+
dir: path.join(__dirname, '..', 'public', 'admin', 'js'),
|
|
79
|
+
cacheControl: 'public, max-age=31536000'
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log(' ✅ Rutas de Database Admin registradas');
|
|
84
|
+
console.log(' ✅ PRoutes: listo para rutas del núcleo\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
PRoutes
|
|
89
|
+
};
|