underpost 2.8.883 → 2.8.884

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/README.md CHANGED
@@ -68,13 +68,15 @@
68
68
 
69
69
 
70
70
 
71
+
72
+
71
73
 
72
74
 
73
75
 
74
76
  <!-- badges -->
75
77
 
76
78
 
77
- [![Node.js CI](https://github.com/underpostnet/engine/actions/workflows/docker-image.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [![Test](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/coverall.yml) [![Downloads](https://img.shields.io/npm/dm/underpost.svg)](https://www.npmjs.com/package/underpost) [![Socket Badge](https://socket.dev/api/badge/npm/package/underpost/2.8.883)](https://socket.dev/npm/package/underpost/overview/2.8.883) [![Coverage Status](https://coveralls.io/repos/github/underpostnet/engine/badge.svg?branch=master)](https://coveralls.io/github/underpostnet/engine?branch=master) [![Version](https://img.shields.io/npm/v/underpost.svg)](https://www.npmjs.org/package/underpost) [![License](https://img.shields.io/npm/l/underpost.svg)](https://www.npmjs.com/package/underpost)
79
+ [![Node.js CI](https://github.com/underpostnet/engine/actions/workflows/docker-image.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [![Test](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/coverall.yml) [![Downloads](https://img.shields.io/npm/dm/underpost.svg)](https://www.npmjs.com/package/underpost) [![Socket Badge](https://socket.dev/api/badge/npm/package/underpost/2.8.884)](https://socket.dev/npm/package/underpost/overview/2.8.884) [![Coverage Status](https://coveralls.io/repos/github/underpostnet/engine/badge.svg?branch=master)](https://coveralls.io/github/underpostnet/engine?branch=master) [![Version](https://img.shields.io/npm/v/underpost.svg)](https://www.npmjs.org/package/underpost) [![License](https://img.shields.io/npm/l/underpost.svg)](https://www.npmjs.com/package/underpost)
78
80
 
79
81
 
80
82
  <!-- end-badges -->
@@ -131,6 +133,8 @@
131
133
 
132
134
 
133
135
 
136
+
137
+
134
138
 
135
139
 
136
140
 
@@ -178,7 +182,7 @@ Run dev client server
178
182
  npm run dev
179
183
  ```
180
184
  <!-- -->
181
- ## underpost ci/cd cli v2.8.883
185
+ ## underpost ci/cd cli v2.8.884
182
186
 
183
187
  ### Usage: `underpost [options] [command]`
184
188
  ```
package/cli.md CHANGED
@@ -1,4 +1,4 @@
1
- ## underpost ci/cd cli v2.8.883
1
+ ## underpost ci/cd cli v2.8.884
2
2
 
3
3
  ### Usage: `underpost [options] [command]`
4
4
  ```
@@ -17,7 +17,7 @@ spec:
17
17
  spec:
18
18
  containers:
19
19
  - name: dd-default-development-blue
20
- image: localhost/rockylinux9-underpost:v2.8.883
20
+ image: localhost/rockylinux9-underpost:v2.8.884
21
21
  # resources:
22
22
  # requests:
23
23
  # memory: "124Ki"
@@ -100,7 +100,7 @@ spec:
100
100
  spec:
101
101
  containers:
102
102
  - name: dd-default-development-green
103
- image: localhost/rockylinux9-underpost:v2.8.883
103
+ image: localhost/rockylinux9-underpost:v2.8.884
104
104
  # resources:
105
105
  # requests:
106
106
  # memory: "124Ki"
@@ -17,7 +17,7 @@ spec:
17
17
  spec:
18
18
  containers:
19
19
  - name: dd-test-development-blue
20
- image: localhost/rockylinux9-underpost:v2.8.883
20
+ image: localhost/rockylinux9-underpost:v2.8.884
21
21
  # resources:
22
22
  # requests:
23
23
  # memory: "96294Ki"
@@ -104,7 +104,7 @@ spec:
104
104
  spec:
105
105
  containers:
106
106
  - name: dd-test-development-green
107
- image: localhost/rockylinux9-underpost:v2.8.883
107
+ image: localhost/rockylinux9-underpost:v2.8.884
108
108
  # resources:
109
109
  # requests:
110
110
  # memory: "96294Ki"
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "main": "src/index.js",
4
4
  "name": "underpost",
5
- "version": "2.8.883",
5
+ "version": "2.8.884",
6
6
  "description": "pwa api rest template",
7
7
  "scripts": {
8
8
  "start": "env-cmd -f .env.production node --max-old-space-size=8192 src/server",
@@ -6,7 +6,7 @@ import {
6
6
  createSessionAndUserToken,
7
7
  createUserAndSession,
8
8
  refreshSessionAndToken,
9
- hashToken,
9
+ logoutSession,
10
10
  jwtSign,
11
11
  getBearerToken,
12
12
  validatePasswordMiddleware,
@@ -382,15 +382,8 @@ const UserService = {
382
382
  const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
383
383
 
384
384
  if (req.params.id === 'logout') {
385
- const refreshToken = req.cookies?.refreshToken;
386
- if (refreshToken) {
387
- const hashedToken = hashToken(refreshToken);
388
- await User.updateOne(
389
- { 'activeSessions.tokenHash': hashedToken },
390
- { $pull: { activeSessions: { tokenHash: hashedToken } } },
391
- );
392
- }
393
- res.clearCookie('refreshToken');
385
+ const result = await logoutSession(User, req, res);
386
+ if (!result) throw new Error('Logout failed');
394
387
  return { message: 'Logged out successfully' };
395
388
  }
396
389
 
@@ -2191,14 +2191,13 @@ const Modal = {
2191
2191
  : { ...Modal.subMenuBtnClass, _: { btnSelector, labelSelector } };
2192
2192
 
2193
2193
  for (const keyDataBtn of Object.keys(_data)) {
2194
- const { btnSelector, labelSelector, open, top } = _data[keyDataBtn];
2194
+ const { labelSelector, top } = _data[keyDataBtn];
2195
2195
  if (top)
2196
2196
  setTimeout(() => {
2197
2197
  top();
2198
2198
  });
2199
- if (open) continue;
2200
2199
  sa(labelSelector).forEach((el) => {
2201
- el.classList.add('hide');
2200
+ if (!el.classList.contains('hide')) el.classList.add('hide');
2202
2201
  el.style.transition = null;
2203
2202
  });
2204
2203
 
@@ -2425,9 +2424,6 @@ const subMenuRender = async (subMenuId) => {
2425
2424
 
2426
2425
  if (!menuBtn || !menuContainer || !arrow) return;
2427
2426
 
2428
- // if (Modal.subMenuBtnClass[subMenuId] && !(isSubMenuOpen(subMenuId) && Modal.subMenuBtnClass[subMenuId].open === true))
2429
- // Modal.subMenuBtnClass[subMenuId].open = false;
2430
-
2431
2427
  const top = () => {
2432
2428
  menuContainer.style.top = menuBtn.offsetTop + Modal.Data['modal-menu'].options.heightTopBar + 'px';
2433
2429
  };
@@ -2442,8 +2438,7 @@ const subMenuRender = async (subMenuId) => {
2442
2438
  menuBtn.style.transition = '.3s';
2443
2439
  arrow.style.transition = '.3s';
2444
2440
 
2445
- if (Modal.subMenuBtnClass[subMenuId].open) {
2446
- Modal.subMenuBtnClass[subMenuId].open = false;
2441
+ if (isSubMenuOpen(subMenuId)) {
2447
2442
  // Close animation
2448
2443
  menuContainer.style.overflow = 'hidden';
2449
2444
  menuContainer.style.height = '0px';
@@ -2453,8 +2448,12 @@ const subMenuRender = async (subMenuId) => {
2453
2448
  arrow.style.rotate = '0deg';
2454
2449
  });
2455
2450
  } else {
2456
- Modal.menuTextLabelAnimation('modal-menu', subMenuId);
2457
- Modal.subMenuBtnClass[subMenuId].open = true;
2451
+ sa(`.menu-label-text-${subMenuId}`).forEach((el) => {
2452
+ if (!el.classList.contains('hide')) el.classList.add('hide');
2453
+ });
2454
+ setTimeout(() => {
2455
+ Modal.menuTextLabelAnimation('modal-menu', subMenuId);
2456
+ });
2458
2457
  // Open animation
2459
2458
  setTimeout(top, 360);
2460
2459
  menuContainer.style.width = '320px';
package/src/index.js CHANGED
@@ -35,7 +35,7 @@ class Underpost {
35
35
  * @type {String}
36
36
  * @memberof Underpost
37
37
  */
38
- static version = 'v2.8.883';
38
+ static version = 'v2.8.884';
39
39
  /**
40
40
  * Repository cli API
41
41
  * @static
@@ -0,0 +1,262 @@
1
+ /**
2
+ * A service dedicated to creating and configuring an Express.js application
3
+ * instance based on server configuration data.
4
+ * @module src/runtime/express/Express.js
5
+ * @namespace ExpressService
6
+ */
7
+
8
+ import fs from 'fs-extra';
9
+ import express from 'express';
10
+ import fileUpload from 'express-fileupload';
11
+ import swaggerUi from 'swagger-ui-express';
12
+ import compression from 'compression';
13
+ import { createServer } from 'http';
14
+
15
+ import UnderpostStartUp from '../../server/start.js';
16
+ import { loggerFactory, loggerMiddleware } from '../../server/logger.js';
17
+ import { getCapVariableName, newInstance } from '../../client/components/core/CommonJs.js';
18
+ import { MailerProvider } from '../../mailer/MailerProvider.js';
19
+ import { DataBaseProvider } from '../../db/DataBaseProvider.js';
20
+ import { createPeerServer } from '../../server/peer.js';
21
+ import { createValkeyConnection } from '../../server/valkey.js';
22
+ import { applySecurity, authMiddlewareFactory } from '../../server/auth.js';
23
+ import { ssrMiddlewareFactory } from '../../server/ssr.js';
24
+
25
+ const logger = loggerFactory(import.meta);
26
+
27
+ /**
28
+ * @class ExpressService
29
+ * @description A service dedicated to creating and configuring an Express.js application
30
+ * instance based on server configuration data.
31
+ * @memberof ExpressService
32
+ */
33
+ class ExpressService {
34
+ /**
35
+ * Creates and configures a complete Express application instance for a specific host/path configuration.
36
+ *
37
+ * @method createApp
38
+ * @memberof ExpressService
39
+ * @param {string} config.host - The host name for the instance (e.g., 'www.example.com').
40
+ * @param {string} config.path - The URL path for the instance (e.g., '/', '/api/v1').
41
+ * @param {number} config.port - The primary listening port for the instance.
42
+ * @param {string} config.client - The client associated with the instance (used for SSR/Mailer configuration lookup).
43
+ * @param {string[]} [config.apis] - A list of API names to load and attach routers for.
44
+ * @param {string[]} config.origins - Allowed origins for CORS.
45
+ * @param {string} [config.directory] - The directory for static files (if overriding default).
46
+ * @param {string} [config.ws] - The WebSocket server name to use.
47
+ * @param {object} [config.mailer] - Mailer configuration.
48
+ * @param {object} [config.db] - Database configuration.
49
+ * @param {string} [config.redirect] - URL or flag to indicate an HTTP redirect should be configured.
50
+ * @param {boolean} [config.peer] - Whether to enable the peer server.
51
+ * @param {object} [config.valkey] - Valkey connection configuration.
52
+ * @param {string} [config.apiBaseHost] - Base host for the API (if running separate API).
53
+ * @param {number} [config.devApiPort] - The dynamically calculated development API port used for CORS in dev mode.
54
+ * @param {string} config.redirectTarget - The full target URL for redirection (used if `redirect` is true).
55
+ * @param {string} config.rootHostPath - The root path for public host assets (e.g., `/public/hostname`).
56
+ * @param {object} config.confSSR - The SSR configuration object, used to look up Mailer templates.
57
+ * @param {import('prom-client').Counter<string>} config.promRequestCounter - Prometheus request counter instance.
58
+ * @param {import('prom-client').Registry} config.promRegister - Prometheus register instance for metrics.
59
+ * @returns {Promise<{portsUsed: number}>} An object indicating how many additional ports were used (e.g., for PeerServer).
60
+ */
61
+ async createApp({
62
+ host,
63
+ path,
64
+ port,
65
+ client,
66
+ apis,
67
+ origins,
68
+ directory,
69
+ ws,
70
+ mailer,
71
+ db,
72
+ redirect,
73
+ peer,
74
+ valkey,
75
+ apiBaseHost,
76
+ devApiPort, // New parameter for dev environment CORS
77
+ redirectTarget,
78
+ rootHostPath,
79
+ confSSR,
80
+ promRequestCounter,
81
+ promRegister,
82
+ }) {
83
+ let portsUsed = 0;
84
+ const runningData = {
85
+ host,
86
+ path,
87
+ runtime: 'nodejs',
88
+ client,
89
+ meta: import.meta,
90
+ apis,
91
+ };
92
+
93
+ const app = express();
94
+
95
+ if (process.env.NODE_ENV === 'production') app.set('trust proxy', true);
96
+
97
+ app.use((req, res, next) => {
98
+ res.on('finish', () => {
99
+ promRequestCounter.inc({
100
+ instance: `${host}:${port}${path}`,
101
+ method: req.method,
102
+ status_code: res.statusCode,
103
+ });
104
+ });
105
+ return next();
106
+ });
107
+
108
+ // Metrics endpoint
109
+ app.get(`${path === '/' ? '' : path}/metrics`, async (req, res) => {
110
+ res.set('Content-Type', promRegister.contentType);
111
+ return res.end(await promRegister.metrics());
112
+ });
113
+
114
+ // Logging, Compression, and Body Parsers
115
+ app.use(loggerMiddleware(import.meta));
116
+ // Compression filter logic is correctly inlined here
117
+ app.use(compression({ filter: (req, res) => !req.headers['x-no-compression'] && compression.filter(req, res) }));
118
+ app.use(express.json({ limit: '100MB' }));
119
+ app.use(express.urlencoded({ extended: true, limit: '20MB' }));
120
+ app.use(fileUpload());
121
+
122
+ if (process.env.NODE_ENV === 'development') app.set('json spaces', 2);
123
+
124
+ // Language handling middleware
125
+ app.use((req, res, next) => {
126
+ const lang = req.headers['accept-language'] || 'en';
127
+ req.lang = typeof lang === 'string' && lang.toLowerCase().match('es') ? 'es' : 'en';
128
+ return next();
129
+ });
130
+
131
+ // Static file serving
132
+ app.use('/', express.static(directory ? directory : `.${rootHostPath}`));
133
+
134
+ // Swagger path definition
135
+ const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
136
+ const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
137
+
138
+ // Flag swagger requests before security middleware
139
+ if (fs.existsSync(swaggerJsonPath)) {
140
+ app.use(swaggerPath, (req, res, next) => {
141
+ res.locals.isSwagger = true;
142
+ next();
143
+ });
144
+ }
145
+
146
+ // Security and CORS
147
+ applySecurity(app, {
148
+ origin: (origin, callback) => {
149
+ // Use devApiPort if provided to calculate the allowed development CORS origin
150
+ const devOrigin =
151
+ apis && process.env.NODE_ENV === 'development' && devApiPort ? [`http://localhost:${devApiPort}`] : [];
152
+
153
+ const allowedOrigins = origins.concat(devOrigin);
154
+
155
+ if (!origin || allowedOrigins.includes(origin)) {
156
+ callback(null, true);
157
+ } else {
158
+ callback(new Error('Not allowed by CORS'));
159
+ }
160
+ },
161
+ });
162
+
163
+ // Handle redirection-only instances
164
+ if (redirect) {
165
+ app.use((req, res, next) => {
166
+ if (process.env.NODE_ENV === 'production' && !req.url.startsWith(`/.well-known/acme-challenge`)) {
167
+ return res.status(302).redirect(redirectTarget + req.url);
168
+ }
169
+ return next();
170
+ });
171
+ await UnderpostStartUp.API.listenPortController(app, port, runningData);
172
+ return { portsUsed };
173
+ }
174
+
175
+ // Create HTTP server for regular instances (required for WebSockets)
176
+ const server = createServer({}, app);
177
+ if (peer) portsUsed++; // Peer server uses one additional port
178
+
179
+ if (!apiBaseHost) {
180
+ // Swagger UI setup
181
+ if (fs.existsSync(swaggerJsonPath)) {
182
+ const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
183
+ // Reusing swaggerPath defined outside, removing unnecessary redeclaration
184
+ app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc));
185
+ }
186
+
187
+ // Database and Valkey connections
188
+ if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
189
+ if (valkey) await createValkeyConnection({ host, path }, valkey);
190
+
191
+ // Mailer setup
192
+ if (mailer) {
193
+ const mailerSsrConf = confSSR[getCapVariableName(client)];
194
+ await MailerProvider.load({
195
+ id: `${host}${path}`,
196
+ meta: `mailer-${host}${path}`,
197
+ host,
198
+ path,
199
+ ...mailer,
200
+ templates: mailerSsrConf ? mailerSsrConf.mailer : {},
201
+ });
202
+ }
203
+
204
+ // API router loading
205
+ if (apis && apis.length > 0) {
206
+ const authMiddleware = authMiddlewareFactory({ host, path });
207
+ const apiPath = `${path === '/' ? '' : path}/${process.env.BASE_API}`;
208
+ for (const api of apis) {
209
+ logger.info(`Build api server`, `${host}${apiPath}/${api}`);
210
+ const { ApiRouter } = await import(`../../api/${api}/${api}.router.js`);
211
+ const router = ApiRouter({ host, path, apiPath, mailer, db, authMiddleware, origins });
212
+ app.use(`${apiPath}/${api}`, router);
213
+ }
214
+ }
215
+
216
+ // WebSocket server setup
217
+ if (ws) {
218
+ const { createIoServer } = await import(`../../ws/${ws}/${ws}.ws.server.js`);
219
+ const { options, meta } = await createIoServer(server, { host, path, db, port, origins });
220
+
221
+ // Listen on the main port for the WS server
222
+ await UnderpostStartUp.API.listenPortController(UnderpostStartUp.API.listenServerFactory(), port, {
223
+ runtime: 'nodejs',
224
+ client: null,
225
+ host,
226
+ path: options.path,
227
+ meta,
228
+ });
229
+ }
230
+
231
+ // Peer server setup
232
+ if (peer) {
233
+ const peerPort = newInstance(port + portsUsed); // portsUsed is 1 here
234
+ const { options, meta, peerServer } = await createPeerServer({
235
+ port: peerPort,
236
+ devPort: port,
237
+ origins,
238
+ host,
239
+ path,
240
+ });
241
+ await UnderpostStartUp.API.listenPortController(peerServer, peerPort, {
242
+ runtime: 'nodejs',
243
+ client: null,
244
+ host,
245
+ path: options.path,
246
+ meta,
247
+ });
248
+ }
249
+ }
250
+
251
+ // SSR middleware loading
252
+ const ssr = await ssrMiddlewareFactory({ app, directory, rootHostPath, path });
253
+ for (const [_, ssrMiddleware] of Object.entries(ssr)) app.use(ssrMiddleware);
254
+
255
+ // Start listening on the main port
256
+ await UnderpostStartUp.API.listenPortController(server, port, runningData);
257
+
258
+ return { portsUsed };
259
+ }
260
+ }
261
+
262
+ export default new ExpressService();
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Exported singleton instance of the LamppService class.
3
+ * This object is used to interact with the Lampp configuration and service.
4
+ * @module src/runtime/lampp/Lampp.js
5
+ * @namespace LamppService
6
+ */
7
+
1
8
  import fs from 'fs-extra';
2
9
  import { getRootDirectory, shellCd, shellExec } from '../../server/process.js';
3
10
  import { loggerFactory } from '../../server/logger.js';
@@ -9,12 +16,14 @@ const logger = loggerFactory(import.meta);
9
16
  * @description Provides utilities for managing the XAMPP (Lampp) service on Linux,
10
17
  * including initialization, router configuration, and virtual host creation.
11
18
  * It manages the server's configuration files and controls the service process.
19
+ * @memberof LamppService
12
20
  */
13
21
  class LamppService {
14
22
  /**
15
23
  * @private
16
24
  * @type {string | undefined}
17
25
  * @description Stores the accumulated Apache virtual host configuration (router definition).
26
+ * @memberof LamppService
18
27
  */
19
28
  router;
20
29
 
@@ -22,12 +31,15 @@ class LamppService {
22
31
  * @public
23
32
  * @type {number[]}
24
33
  * @description A list of ports currently configured and listened to by the Lampp service.
34
+ * @memberof LamppService
25
35
  */
26
36
  ports;
27
37
 
28
38
  /**
29
39
  * Creates an instance of LamppService.
30
40
  * Initializes the router configuration and ports list.
41
+ * @method constructor
42
+ * @memberof LamppService
31
43
  */
32
44
  constructor() {
33
45
  this.router = undefined;
@@ -37,8 +49,11 @@ class LamppService {
37
49
  /**
38
50
  * Checks if the XAMPP (Lampp) service appears to be installed based on the presence of its main configuration file.
39
51
  *
52
+ * @method enabled
40
53
  * @memberof LamppService
41
54
  * @returns {boolean} True if the configuration file exists, indicating Lampp is likely installed.
55
+ * @throws {Error} If the configuration file does not exist.
56
+ * @memberof LamppService
42
57
  */
43
58
  enabled() {
44
59
  return fs.existsSync('/opt/lampp/etc/httpd.conf');
@@ -49,10 +64,11 @@ class LamppService {
49
64
  * This method configures virtual hosts, disables default ports (80/443) in the main config
50
65
  * to avoid conflicts, and starts or stops the service using shell commands.
51
66
  *
52
- * @memberof LamppService.prototype
67
+ * @method initService
53
68
  * @param {object} [options={daemon: false}] - Options for service initialization.
54
69
  * @param {boolean} [options.daemon=false] - Flag to indicate if the service should be run as a daemon (currently unused in logic).
55
70
  * @returns {Promise<void>}
71
+ * @memberof LamppService
56
72
  */
57
73
  async initService(options = { daemon: false }) {
58
74
  let cmd;
@@ -117,9 +133,10 @@ class LamppService {
117
133
  * Appends new Apache VirtualHost configuration content to the internal router string.
118
134
  * If a router config file exists from a previous run, it loads it first.
119
135
  *
120
- * @memberof LamppService.prototype
136
+ * @method appendRouter
121
137
  * @param {string} render - The new VirtualHost configuration string to append.
122
138
  * @returns {string} The complete, updated router configuration string.
139
+ * @memberof LamppService
123
140
  */
124
141
  appendRouter(render) {
125
142
  if (!this.router) {
@@ -135,7 +152,7 @@ class LamppService {
135
152
  /**
136
153
  * Resets the internal router configuration and removes the temporary configuration file.
137
154
  *
138
- * @memberof LamppService.prototype
155
+ * @memberof LamppService
139
156
  * @returns {void}
140
157
  */
141
158
  removeRouter() {
@@ -148,8 +165,9 @@ class LamppService {
148
165
  * This includes downloading the installer, running it, and setting up initial configurations.
149
166
  * Only runs on the 'linux' platform.
150
167
  *
151
- * @memberof LamppService.prototype
168
+ * @method install
152
169
  * @returns {Promise<void>}
170
+ * @memberof LamppService
153
171
  */
154
172
  async install() {
155
173
  if (process.platform === 'linux') {
@@ -199,7 +217,7 @@ class LamppService {
199
217
  * Creates and appends a new Apache VirtualHost entry to the router configuration for a web application.
200
218
  * The router is then applied by calling {@link LamppService#initService}.
201
219
  *
202
- * @memberof LamppService.prototype
220
+ * @method createApp
203
221
  * @param {object} options - Configuration options for the new web application.
204
222
  * @param {number} options.port - The port the VirtualHost should listen on.
205
223
  * @param {string} options.host - The ServerName/host for the VirtualHost.
@@ -210,6 +228,7 @@ class LamppService {
210
228
  * @param {string} [options.redirectTarget] - The target URL for redirection.
211
229
  * @param {boolean} [options.resetRouter] - If true, clears the existing router configuration before appending the new one.
212
230
  * @returns {{disabled: boolean}} An object indicating if the service is disabled.
231
+ * @memberof LamppService
213
232
  */
214
233
  createApp({ port, host, path, directory, rootHostPath, redirect, redirectTarget, resetRouter }) {
215
234
  if (!this.enabled()) return { disabled: true };
@@ -262,13 +281,15 @@ Listen ${port}
262
281
  }
263
282
 
264
283
  /**
265
- * @namespace LamppService
266
284
  * @description Exported singleton instance of the LamppService class.
267
285
  * This object is used to interact with the Lampp configuration and service.
286
+ * @memberof LamppService
268
287
  * @type {LamppService}
269
288
  */
270
289
  const Lampp = new LamppService();
271
290
 
291
+ export { Lampp };
292
+
272
293
  // -- helper info --
273
294
 
274
295
  // ERR too many redirects:
@@ -320,5 +341,3 @@ const Lampp = new LamppService();
320
341
  // RewriteCond %{HTTP_HOST} ^www\. [NC]
321
342
  // RewriteCond %{HTTP_HOST} ^(?:www\.)?(.+)$ [NC]
322
343
  // RewriteRule ^ https://%1%{REQUEST_URI} [L,NE,R=301]
323
-
324
- export { Lampp };
@@ -110,16 +110,22 @@ function jwtIssuerAudienceFactory(options = { host: '', path: '' }) {
110
110
  * @param {object} [options={}] Additional JWT sign options.
111
111
  * @param {string} options.host The host name.
112
112
  * @param {string} options.path The path name.
113
- * @param {number} expireMinutes The token expiration in minutes.
113
+ * @param {number} accessExpireMinutes The access token expiration in minutes.
114
+ * @param {number} refreshExpireMinutes The refresh token expiration in minutes.
114
115
  * @returns {string} The signed JWT.
115
116
  * @throws {Error} If JWT key is not configured.
116
117
  * @memberof Auth
117
118
  */
118
- function jwtSign(payload, options = { host: '', path: '' }, expireMinutes = process.env.ACCESS_EXPIRE_MINUTES) {
119
+ function jwtSign(
120
+ payload,
121
+ options = { host: '', path: '' },
122
+ accessExpireMinutes = process.env.ACCESS_EXPIRE_MINUTES,
123
+ refreshExpireMinutes = process.env.REFRESH_EXPIRE_MINUTES,
124
+ ) {
119
125
  const { issuer, audience } = jwtIssuerAudienceFactory(options);
120
126
  const signOptions = {
121
127
  algorithm: config.jwtAlgorithm,
122
- expiresIn: `${expireMinutes}m`,
128
+ expiresIn: `${accessExpireMinutes}m`,
123
129
  issuer,
124
130
  audience,
125
131
  };
@@ -128,7 +134,11 @@ function jwtSign(payload, options = { host: '', path: '' }, expireMinutes = proc
128
134
 
129
135
  if (!process.env.JWT_SECRET) throw new Error('JWT key not configured');
130
136
 
131
- logger.info('JWT signed', { payload, signOptions, expireMinutes });
137
+ // Add refreshExpiresAt to the payload, which is the same as the token's own expiry.
138
+ const now = Date.now();
139
+ payload.refreshExpiresAt = now + refreshExpireMinutes * 60 * 1000;
140
+
141
+ logger.info('JWT signed', { payload, signOptions, accessExpireMinutes, refreshExpireMinutes });
132
142
 
133
143
  return jwt.sign(payload, process.env.JWT_SECRET, signOptions);
134
144
  }
@@ -170,6 +180,14 @@ const getBearerToken = (req) => {
170
180
  return '';
171
181
  };
172
182
 
183
+ /**
184
+ * Checks if the request is a refresh token request.
185
+ * @param {import('express').Request} req The Express request object.
186
+ * @returns {boolean} True if the request is a refresh token request, false otherwise.
187
+ * @memberof Auth
188
+ */
189
+ const isRefreshTokenReq = (req) => req.method === 'GET' && req.params.id === 'auth';
190
+
173
191
  // ---------- Middleware ----------
174
192
  /**
175
193
  * Creates a middleware to authenticate requests using a JWT Bearer token.
@@ -237,10 +255,7 @@ const authMiddlewareFactory = (options = { host: '', path: '' }) => {
237
255
  return res.status(401).json({ status: 'error', message: 'unauthorized: host or path mismatch' });
238
256
  }
239
257
 
240
- // check session expiresAt
241
- const isRefreshTokenReq = req.method === 'GET' && req.params.id === 'auth';
242
-
243
- if (!isRefreshTokenReq && session.expiresAt < new Date()) {
258
+ if (!isRefreshTokenReq(req) && session.expiresAt < new Date()) {
244
259
  return res.status(401).json({ status: 'error', message: 'unauthorized: session expired' });
245
260
  }
246
261
  }
@@ -342,11 +357,11 @@ const cookieOptionsFactory = (req) => {
342
357
  const reqIsSecure = Boolean(req.secure || forwardedProto.split(',')[0] === 'https');
343
358
 
344
359
  // secure must be true for SameSite=None to work across sites
345
- const secure = isProduction ? reqIsSecure : false;
360
+ const secure = isProduction ? reqIsSecure : req.protocol === 'https';
346
361
  const sameSite = secure ? 'None' : 'Lax';
347
362
 
348
363
  // Safe parse of maxAge minutes
349
- const minutes = Number.parseInt(process.env.REFRESH_EXPIRE_MINUTES, 10);
364
+ const minutes = Number.parseInt(process.env.ACCESS_EXPIRE_MINUTES, 10);
350
365
  const maxAge = Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : undefined;
351
366
 
352
367
  const opts = {
@@ -399,6 +414,48 @@ async function createSessionAndUserToken(user, User, req, res, options = { host:
399
414
  return { jwtid };
400
415
  }
401
416
 
417
+ /**
418
+ * Removes a session by its ID for a given user.
419
+ * @param {import('mongoose').Model} User The Mongoose User model.
420
+ * @param {string} userId The ID of the user.
421
+ * @param {string} sessionId The ID of the session to remove.
422
+ * @returns {Promise<void>}
423
+ * @memberof Auth
424
+ */
425
+ async function removeSession(User, userId, sessionId) {
426
+ return await User.updateOne({ _id: userId }, { $pull: { activeSessions: { _id: sessionId } } });
427
+ }
428
+
429
+ /**
430
+ * Logs out a user session by removing it from the database and clearing the refresh token cookie.
431
+ * @param {import('mongoose').Model} User The Mongoose User model.
432
+ * @param {import('express').Request} req The Express request object.
433
+ * @param {import('express').Response} res The Express response object.
434
+ * @returns {Promise<boolean>} True if a session was found and removed, false otherwise.
435
+ * @memberof Auth
436
+ */
437
+ async function logoutSession(User, req, res) {
438
+ const refreshToken = req.cookies?.refreshToken;
439
+
440
+ if (!refreshToken) {
441
+ return false;
442
+ }
443
+
444
+ const user = await User.findOne({ 'activeSessions.tokenHash': refreshToken });
445
+
446
+ if (!user) {
447
+ return false;
448
+ }
449
+
450
+ const session = user.activeSessions.find((s) => s.tokenHash === refreshToken);
451
+
452
+ const result = await removeSession(User, user._id, session._id);
453
+
454
+ res.clearCookie('refreshToken', { path: '/' });
455
+
456
+ return result.modifiedCount > 0;
457
+ }
458
+
402
459
  /**
403
460
  * Create user and immediate session + access token
404
461
  * @param {import('express').Request} req The Express request object.
@@ -470,12 +527,10 @@ async function refreshSessionAndToken(req, res, User, options = { host: '', path
470
527
  }
471
528
 
472
529
  // Check expiry
473
- if (session.expiresAt && session.expiresAt < new Date()) {
474
- // remove expired session
475
- user.activeSessions.id(session._id).remove();
476
- await user.save({ validateBeforeSave: false });
477
- res.clearCookie('refreshToken', { path: '/' });
478
- throw new Error('Refresh token expired');
530
+ if (!isRefreshTokenReq(req) && session.expiresAt && session.expiresAt < new Date()) {
531
+ const result = await removeSession(User, user._id, session._id);
532
+ if (result) throw new Error('Refresh token expired');
533
+ else throw new Error('Session not found');
479
534
  }
480
535
 
481
536
  // Rotate: generate new token, update stored hash and metadata
@@ -646,5 +701,8 @@ export {
646
701
  createSessionAndUserToken,
647
702
  createUserAndSession,
648
703
  refreshSessionAndToken,
704
+ logoutSession,
705
+ removeSession,
649
706
  applySecurity,
707
+ isRefreshTokenReq,
650
708
  };
@@ -1,31 +1,37 @@
1
+ /**
2
+ * @namespace Runtime
3
+ * @description The main runtime orchestrator responsible for reading configuration,
4
+ * initializing services (Prometheus, Ports, DB, Mailer), and building the
5
+ * specific server runtime for each host/path (e.g., nodejs, lampp).
6
+ */
7
+
1
8
  import fs from 'fs-extra';
2
- import express from 'express';
3
9
  import dotenv from 'dotenv';
4
- import fileUpload from 'express-fileupload';
5
- import swaggerUi from 'swagger-ui-express';
6
10
  import * as promClient from 'prom-client';
7
- import compression from 'compression';
8
11
 
9
12
  import UnderpostStartUp from './start.js';
10
- import { createServer } from 'http';
11
- import { loggerFactory, loggerMiddleware } from './logger.js';
12
- import { getCapVariableName, newInstance } from '../client/components/core/CommonJs.js';
13
- import { MailerProvider } from '../mailer/MailerProvider.js';
14
- import { DataBaseProvider } from '../db/DataBaseProvider.js';
15
- import { createPeerServer } from './peer.js';
13
+ import { loggerFactory } from './logger.js';
14
+ import { newInstance } from '../client/components/core/CommonJs.js';
16
15
  import { Lampp } from '../runtime/lampp/Lampp.js';
17
- import { createValkeyConnection } from './valkey.js';
18
- import { applySecurity, authMiddlewareFactory } from './auth.js';
19
16
  import { getInstanceContext } from './conf.js';
20
- import { ssrMiddlewareFactory } from './ssr.js';
17
+
18
+ import ExpressService from '../runtime/express/Express.js';
21
19
 
22
20
  dotenv.config();
23
21
 
24
22
  const logger = loggerFactory(import.meta);
25
23
 
24
+ /**
25
+ * Reads server configurations, sets up Prometheus metrics, and iterates through
26
+ * all defined hosts and paths to build and start the corresponding runtime instances.
27
+ *
28
+ * @memberof Runtime
29
+ * @returns {Promise<void>}
30
+ */
26
31
  const buildRuntime = async () => {
27
32
  const deployId = process.env.DEPLOY_ID;
28
33
 
34
+ // 1. Initialize Prometheus Metrics
29
35
  const collectDefaultMetrics = promClient.collectDefaultMetrics;
30
36
  collectDefaultMetrics();
31
37
 
@@ -38,12 +44,17 @@ const buildRuntime = async () => {
38
44
  const requestCounter = new promClient.Counter(promCounterOption);
39
45
  const initPort = parseInt(process.env.PORT) + 1;
40
46
  let currentPort = initPort;
47
+
48
+ // 2. Load Configuration
41
49
  const confServer = JSON.parse(fs.readFileSync(`./conf/conf.server.json`, 'utf8'));
42
50
  const confSSR = JSON.parse(fs.readFileSync(`./conf/conf.ssr.json`, 'utf8'));
43
51
  const singleReplicaHosts = [];
52
+
53
+ // 3. Iterate through hosts and paths
44
54
  for (const host of Object.keys(confServer)) {
45
55
  if (singleReplicaHosts.length > 0)
46
56
  currentPort += singleReplicaHosts.reduce((accumulator, currentValue) => accumulator + currentValue.replicas, 0);
57
+
47
58
  const rootHostPath = `/public/${host}`;
48
59
  for (const path of Object.keys(confServer[host])) {
49
60
  confServer[host][path].port = newInstance(currentPort);
@@ -65,6 +76,7 @@ const buildRuntime = async () => {
65
76
  apiBaseHost,
66
77
  } = confServer[host][path];
67
78
 
79
+ // Calculate context data
68
80
  const { redirectTarget, singleReplicaHost } = await getInstanceContext({
69
81
  redirect,
70
82
  singleReplicaHosts,
@@ -91,203 +103,37 @@ const buildRuntime = async () => {
91
103
 
92
104
  switch (runtime) {
93
105
  case 'nodejs':
94
- const app = express();
95
-
96
- app.use((req, res, next) => {
97
- // const info = `${req.headers.host}${req.url}`;
98
- return next();
99
- });
100
-
101
- if (process.env.NODE_ENV === 'production') app.set('trust proxy', true);
102
-
103
- app.use((req, res, next) => {
104
- requestCounter.inc({
105
- instance: `${host}:${port}${path}`,
106
- method: req.method,
107
- status_code: res.statusCode,
108
- });
109
- // decodeURIComponent(req.url)
110
- return next();
111
- });
112
-
113
- app.get(`${path === '/' ? '' : path}/metrics`, async (req, res) => {
114
- res.set('Content-Type', promClient.register.contentType);
115
- return res.end(await promClient.register.metrics());
116
- });
117
-
118
- // set logger
119
- app.use(loggerMiddleware(import.meta));
120
-
121
- // js src compression
122
- app.use(compression({ filter: shouldCompress }));
123
- function shouldCompress(req, res) {
124
- if (req.headers['x-no-compression']) {
125
- // don't compress responses with this request header
126
- return false;
127
- }
128
-
129
- // fallback to standard filter function
130
- return compression.filter(req, res);
131
- }
132
-
133
- // parse requests of content-type - application/json
134
- app.use(express.json({ limit: '100MB' }));
135
-
136
- // parse requests of content-type - application/x-www-form-urlencoded
137
- app.use(express.urlencoded({ extended: true, limit: '20MB' }));
138
-
139
- // file upload middleware
140
- app.use(fileUpload());
141
-
142
- // json formatted response
143
- if (process.env.NODE_ENV === 'development') app.set('json spaces', 2);
144
-
145
- // lang handling middleware
146
- app.use(function (req, res, next) {
147
- const lang = req.headers['accept-language'] || 'en';
148
- if (typeof lang === 'string' && lang.toLowerCase().match('es')) {
149
- req.lang = 'es';
150
- } else req.lang = 'en';
151
- return next();
106
+ // The devApiPort is used for development CORS origin calculation
107
+ // It needs to account for the current port and potential peer server increment
108
+ const devApiPort = currentPort + (peer ? 2 : 1);
109
+
110
+ logger.info('Build nodejs server runtime', `${host}${path}:${port}`);
111
+
112
+ const { portsUsed } = await ExpressService.createApp({
113
+ host,
114
+ path,
115
+ port,
116
+ client,
117
+ apis,
118
+ origins,
119
+ directory,
120
+ ws,
121
+ mailer,
122
+ db,
123
+ redirect,
124
+ peer,
125
+ valkey,
126
+ apiBaseHost,
127
+ devApiPort, // Pass the dynamically calculated dev API port
128
+ redirectTarget,
129
+ rootHostPath,
130
+ confSSR,
131
+ promRequestCounter: requestCounter,
132
+ promRegister: promClient.register,
152
133
  });
153
134
 
154
- // instance public static
155
- app.use('/', express.static(directory ? directory : `.${rootHostPath}`));
156
- if (process.argv.includes('static')) {
157
- logger.info('Build static server runtime', `${host}${path}`);
158
- currentPort += 2;
159
- const staticPort = newInstance(currentPort);
160
- await UnderpostStartUp.API.listenPortController(app, staticPort, runningData);
161
- currentPort++;
162
- continue;
163
- }
164
-
165
- // Flag swagger requests before security middleware is applied
166
- const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
167
- const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
168
- if (fs.existsSync(swaggerJsonPath))
169
- app.use(swaggerPath, (req, res, next) => {
170
- res.locals.isSwagger = true;
171
- next();
172
- });
173
-
174
- // security
175
- applySecurity(app, {
176
- origin: origins.concat(
177
- apis && process.env.NODE_ENV === 'development' ? [`http://localhost:${currentPort + 2}`] : [],
178
- ),
179
- });
180
-
181
- if (redirect) {
182
- app.use(function (req = express.Request, res = express.Response, next = express.NextFunction) {
183
- if (process.env.NODE_ENV === 'production' && !req.url.startsWith(`/.well-known/acme-challenge`))
184
- return res.status(302).redirect(redirectTarget + req.url);
185
- // if (!req.url.startsWith(`/.well-known/acme-challenge`)) return res.status(302).redirect(redirect);
186
- return next();
187
- });
188
- // app.use(
189
- // '*',
190
- // createProxyMiddleware({
191
- // target: redirect,
192
- // changeOrigin: true,
193
- // }),
194
- // );
195
-
196
- await UnderpostStartUp.API.listenPortController(app, port, runningData);
197
- break;
198
- }
199
- // instance server
200
- const server = createServer({}, app);
201
- if (peer) currentPort++;
202
-
203
- if (!apiBaseHost) {
204
- if (fs.existsSync(swaggerJsonPath)) {
205
- const swaggerInstance =
206
- (swaggerDoc) =>
207
- (...args) =>
208
- swaggerUi.setup(swaggerDoc)(...args);
209
- const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
210
- const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
211
- app.use(swaggerPath, swaggerUi.serve, swaggerInstance(swaggerDoc));
212
- }
213
-
214
- if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
215
-
216
- // valkey server
217
- if (valkey) await createValkeyConnection({ host, path }, valkey);
218
-
219
- if (mailer) {
220
- const mailerSsrConf = confSSR[getCapVariableName(client)];
221
- await MailerProvider.load({
222
- id: `${host}${path}`,
223
- meta: `mailer-${host}${path}`,
224
- host,
225
- path,
226
- ...mailer,
227
- templates: mailerSsrConf ? mailerSsrConf.mailer : {},
228
- });
229
- }
230
- if (apis && apis.length > 0) {
231
- const authMiddleware = authMiddlewareFactory({ host, path });
232
- const apiPath = `${path === '/' ? '' : path}/${process.env.BASE_API}`;
233
- for (const api of apis)
234
- await (async () => {
235
- logger.info(`Build api server`, `${host}${apiPath}/${api}`);
236
- const { ApiRouter } = await import(`../api/${api}/${api}.router.js`);
237
- const router = ApiRouter({ host, path, apiPath, mailer, db, authMiddleware, origins });
238
- // router.use(cors({ origin: origins }));
239
- // logger.info('Load api router', { host, path: apiPath, api });
240
- app.use(`${apiPath}/${api}`, router);
241
- })();
242
- }
243
-
244
- if (ws)
245
- await (async () => {
246
- const { createIoServer } = await import(`../ws/${ws}/${ws}.ws.server.js`);
247
- // logger.info('Load socket.io ws router', { host, ws });
248
- // start socket.io
249
- const { options, meta } = await createIoServer(server, {
250
- host,
251
- path,
252
- db,
253
- port,
254
- origins,
255
- });
256
- await UnderpostStartUp.API.listenPortController(UnderpostStartUp.API.listenServerFactory(), port, {
257
- runtime: 'nodejs',
258
- client: null,
259
- host,
260
- path: options.path,
261
- meta,
262
- });
263
- })();
264
-
265
- if (peer) {
266
- const peerPort = newInstance(currentPort);
267
- const { options, meta, peerServer } = await createPeerServer({
268
- port: peerPort,
269
- devPort: port,
270
- origins,
271
- host,
272
- path,
273
- });
274
-
275
- await UnderpostStartUp.API.listenPortController(peerServer, peerPort, {
276
- runtime: 'nodejs',
277
- client: null,
278
- host,
279
- path: options.path,
280
- meta,
281
- });
282
- }
283
- }
284
-
285
- // load ssr
286
- const ssr = await ssrMiddlewareFactory({ app, directory, rootHostPath, path });
287
- for (const [_, ssrMiddleware] of Object.entries(ssr)) app.use(ssrMiddleware);
288
-
289
- await UnderpostStartUp.API.listenPortController(server, port, runningData);
290
-
135
+ // Increment currentPort by any additional ports used by the service (e.g., PeerServer port)
136
+ currentPort += portsUsed;
291
137
  break;
292
138
 
293
139
  case 'lampp':
@@ -1,3 +0,0 @@
1
- const Nginx = {};
2
-
3
- export { Nginx };