underpost 2.8.878 → 2.8.882

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.
Files changed (61) hide show
  1. package/.env.development +35 -3
  2. package/.env.production +40 -3
  3. package/.env.test +35 -3
  4. package/.github/workflows/release.cd.yml +3 -3
  5. package/README.md +20 -2
  6. package/bin/deploy.js +40 -0
  7. package/cli.md +3 -1
  8. package/conf.js +29 -3
  9. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  10. package/manifests/deployment/dd-test-development/deployment.yaml +6 -6
  11. package/package.json +1 -2
  12. package/src/api/document/document.controller.js +66 -0
  13. package/src/api/document/document.model.js +51 -0
  14. package/src/api/document/document.router.js +24 -0
  15. package/src/api/document/document.service.js +133 -0
  16. package/src/cli/deploy.js +1 -1
  17. package/src/cli/index.js +2 -0
  18. package/src/cli/repository.js +2 -0
  19. package/src/cli/run.js +27 -1
  20. package/src/client/Default.index.js +46 -1
  21. package/src/client/components/core/Account.js +8 -1
  22. package/src/client/components/core/AgGrid.js +18 -9
  23. package/src/client/components/core/Auth.js +258 -89
  24. package/src/client/components/core/BtnIcon.js +13 -3
  25. package/src/client/components/core/Content.js +2 -1
  26. package/src/client/components/core/CssCore.js +40 -27
  27. package/src/client/components/core/Docs.js +189 -88
  28. package/src/client/components/core/Input.js +34 -19
  29. package/src/client/components/core/LoadingAnimation.js +5 -10
  30. package/src/client/components/core/Modal.js +280 -123
  31. package/src/client/components/core/ObjectLayerEngine.js +470 -104
  32. package/src/client/components/core/ObjectLayerEngineModal.js +1 -0
  33. package/src/client/components/core/Panel.js +9 -2
  34. package/src/client/components/core/PanelForm.js +234 -76
  35. package/src/client/components/core/Router.js +15 -15
  36. package/src/client/components/core/ToolTip.js +83 -19
  37. package/src/client/components/core/Translate.js +1 -1
  38. package/src/client/components/core/VanillaJs.js +7 -3
  39. package/src/client/components/core/windowGetDimensions.js +202 -0
  40. package/src/client/components/default/MenuDefault.js +105 -41
  41. package/src/client/components/default/RoutesDefault.js +2 -0
  42. package/src/client/services/default/default.management.js +1 -0
  43. package/src/client/services/document/document.service.js +97 -0
  44. package/src/client/services/file/file.service.js +2 -0
  45. package/src/client/ssr/Render.js +1 -1
  46. package/src/client/ssr/head/DefaultScripts.js +2 -0
  47. package/src/client/ssr/head/Seo.js +1 -0
  48. package/src/index.js +1 -1
  49. package/src/mailer/EmailRender.js +1 -1
  50. package/src/server/auth.js +68 -17
  51. package/src/server/client-build.js +2 -3
  52. package/src/server/client-formatted.js +40 -12
  53. package/src/server/conf.js +5 -1
  54. package/src/server/crypto.js +195 -76
  55. package/src/server/object-layer.js +196 -0
  56. package/src/server/peer.js +47 -5
  57. package/src/server/process.js +85 -1
  58. package/src/server/runtime.js +23 -23
  59. package/src/server/ssr.js +52 -10
  60. package/src/server/valkey.js +89 -1
  61. package/test/crypto.test.js +117 -0
@@ -0,0 +1,196 @@
1
+ import dotenv from 'dotenv';
2
+
3
+ import fs from 'fs-extra';
4
+ import { PNG } from 'pngjs';
5
+ import sharp from 'sharp';
6
+ import Jimp from 'jimp';
7
+
8
+ import { range } from '../client/components/core/CommonJs.js';
9
+ import { random } from '../client/components/core/CommonJs.js';
10
+
11
+ dotenv.config({ path: `./engine-private/conf/dd-cyberia/.env.production`, override: true });
12
+
13
+ const pngDirectoryIteratorByObjectLayerType = async (
14
+ objectLayerType = 'skin',
15
+ callback = ({ path, objectLayerType, objectLayerId, direction, frame }) => {},
16
+ ) => {
17
+ for (const objectLayerId of await fs.readdir(`./src/client/public/cyberia/assets/${objectLayerType}`)) {
18
+ for (const direction of await fs.readdir(
19
+ `./src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}`,
20
+ )) {
21
+ const dirFolder = `./src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}/${direction}`;
22
+ if (!fs.statSync(dirFolder).isDirectory()) continue;
23
+ for (const frame of await fs.readdir(dirFolder)) {
24
+ const imageFilePath = `./src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}/${direction}/${frame}`;
25
+ await callback({ path: imageFilePath, objectLayerType, objectLayerId, direction, frame });
26
+ }
27
+ }
28
+ }
29
+ };
30
+
31
+ const readPngAsync = (filePath) => {
32
+ return new Promise((resolve, reject) => {
33
+ fs.createReadStream(filePath)
34
+ .pipe(new PNG())
35
+ .on('parsed', function () {
36
+ resolve({
37
+ width: this.width,
38
+ height: this.height,
39
+ data: Buffer.from(this.data),
40
+ });
41
+ })
42
+ .on('error', (err) => {
43
+ reject(err);
44
+ });
45
+ });
46
+ };
47
+
48
+ const frameFactory = async (path, colors = []) => {
49
+ const frame = [];
50
+ try {
51
+ let image;
52
+
53
+ if (path.endsWith('.gif')) {
54
+ image = await Jimp.read(path);
55
+ // remove gif file
56
+ fs.removeSync(path);
57
+ // save image replacing gif for png
58
+ const pngPath = path.replace('.gif', '.png');
59
+ await image.writeAsync(pngPath);
60
+ } else {
61
+ const png = await readPngAsync(path);
62
+ image = new Jimp(png.width, png.height);
63
+ image.bitmap = png;
64
+ }
65
+
66
+ const mazeFactor = parseInt(image.bitmap.height / 24);
67
+ let _y = -1;
68
+ for (const y of range(0, image.bitmap.height - 1)) {
69
+ if (y % mazeFactor === 0) {
70
+ _y++;
71
+ if (!frame[_y]) frame[_y] = [];
72
+ }
73
+ let _x = -1;
74
+ for (const x of range(0, image.bitmap.width - 1)) {
75
+ const rgba = Object.values(Jimp.intToRGBA(image.getPixelColor(x, y)));
76
+ if (y % mazeFactor === 0 && x % mazeFactor === 0) {
77
+ _x++;
78
+ const indexColor = colors.findIndex(
79
+ (c) => c[0] === rgba[0] && c[1] === rgba[1] && c[2] === rgba[2] && c[3] === rgba[3],
80
+ );
81
+ if (indexColor === -1) {
82
+ colors.push(rgba);
83
+ frame[_y][_x] = colors.length - 1;
84
+ } else {
85
+ frame[_y][_x] = indexColor;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ } catch (error) {
91
+ logger.error(`Failed to process image ${path}:`, error);
92
+ }
93
+ return { frame, colors };
94
+ };
95
+
96
+ const getKeyFramesDirectionsFromNumberFolderDirection = (direction) => {
97
+ let objectLayerFrameDirections = [];
98
+
99
+ switch (direction) {
100
+ case '08':
101
+ objectLayerFrameDirections = ['down_idle', 'none_idle', 'default_idle'];
102
+ break;
103
+ case '18':
104
+ objectLayerFrameDirections = ['down_walking'];
105
+ break;
106
+ case '02':
107
+ objectLayerFrameDirections = ['up_idle'];
108
+ break;
109
+ case '12':
110
+ objectLayerFrameDirections = ['up_walking'];
111
+ break;
112
+ case '04':
113
+ objectLayerFrameDirections = ['left_idle', 'up_left_idle', 'down_left_idle'];
114
+ break;
115
+ case '14':
116
+ objectLayerFrameDirections = ['left_walking', 'up_left_walking', 'down_left_walking'];
117
+ break;
118
+ case '06':
119
+ objectLayerFrameDirections = ['right_idle', 'up_right_idle', 'down_right_idle'];
120
+ break;
121
+ case '16':
122
+ objectLayerFrameDirections = ['right_walking', 'up_right_walking', 'down_right_walking'];
123
+ break;
124
+ }
125
+
126
+ return objectLayerFrameDirections;
127
+ };
128
+
129
+ const buildImgFromTile = async (
130
+ options = {
131
+ tile: { map_color: null, frame_matrix: null },
132
+ imagePath: '',
133
+ cellPixelDim: 20,
134
+ opacityFilter: (x, y, color) => 255,
135
+ },
136
+ ) => {
137
+ const { tile, imagePath, cellPixelDim, opacityFilter } = options;
138
+ const mainMatrix = tile.frame_matrix;
139
+ const sharpOptions = {
140
+ create: {
141
+ width: cellPixelDim * mainMatrix[0].length,
142
+ height: cellPixelDim * mainMatrix.length,
143
+ channels: 4,
144
+ background: { r: 0, g: 0, b: 0, alpha: 0 }, // transparent background
145
+ },
146
+ };
147
+
148
+ let image = await sharp(sharpOptions).png().toBuffer();
149
+ fs.writeFileSync(imagePath, image);
150
+ image = await Jimp.read(imagePath);
151
+
152
+ for (let y = 0; y < mainMatrix.length; y++) {
153
+ for (let x = 0; x < mainMatrix[y].length; x++) {
154
+ const colorIndex = mainMatrix[y][x];
155
+ if (colorIndex === null || colorIndex === undefined) continue;
156
+
157
+ const color = tile.map_color[colorIndex];
158
+ if (!color) continue;
159
+
160
+ const rgbaColor = color.length === 4 ? color : [...color, 255]; // Ensure alpha channel
161
+
162
+ for (let dy = 0; dy < cellPixelDim; dy++) {
163
+ for (let dx = 0; dx < cellPixelDim; dx++) {
164
+ const pixelX = x * cellPixelDim + dx;
165
+ const pixelY = y * cellPixelDim + dy;
166
+ image.setPixelColor(Jimp.rgbaToInt(...rgbaColor), pixelX, pixelY);
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ await image.writeAsync(imagePath);
173
+ };
174
+
175
+ const generateRandomStats = () => {
176
+ return {
177
+ effect: random(0, 10),
178
+ resistance: random(0, 10),
179
+ agility: random(0, 10),
180
+ range: random(0, 10),
181
+ intelligence: random(0, 10),
182
+ utility: random(0, 10),
183
+ };
184
+ };
185
+
186
+ const zIndexPriority = { floor: 0, skin: 1, weapon: 2, skill: 3, coin: 4 };
187
+
188
+ export {
189
+ pngDirectoryIteratorByObjectLayerType,
190
+ readPngAsync,
191
+ frameFactory,
192
+ getKeyFramesDirectionsFromNumberFolderDirection,
193
+ buildImgFromTile,
194
+ generateRandomStats,
195
+ zIndexPriority,
196
+ };
@@ -1,30 +1,72 @@
1
+ /**
2
+ * Module for peerjs server management.
3
+ * Initializes and configures the PeerJS server instance, typically running
4
+ * alongside a main Node.js application.
5
+ *
6
+ * @module src/server/peer.js
7
+ * @namespace Peer
8
+ */
9
+
1
10
  import { PeerServer } from 'peer';
2
11
  import dotenv from 'dotenv';
3
12
  import { loggerFactory } from './logger.js';
4
- import fs from 'fs-extra';
5
13
  import UnderpostStartUp from './start.js';
6
14
 
7
15
  dotenv.config();
8
16
 
17
+ /**
18
+ * Logger instance for this module, utilizing the framework's factory.
19
+ * @type {function(*): void}
20
+ * @memberof Peer
21
+ * @private
22
+ */
9
23
  const logger = loggerFactory(import.meta);
10
24
 
25
+ // Documentation references:
11
26
  // https://github.com/peers/peerjs
12
27
  // https://github.com/peers/peerjs-server
13
28
 
29
+ /**
30
+ * Creates and starts a configured PeerJS server instance.
31
+ *
32
+ * This function handles port configuration, CORS origins, and paths, then uses
33
+ * a listener factory to start the server.
34
+ *
35
+ * @async
36
+ * @function createPeerServer
37
+ * @memberof Peer
38
+ * @param {object} config - Configuration object for the PeerJS server setup.
39
+ * @param {number} config.port - The primary port on which the PeerJS server will listen.
40
+ * @param {number} [config.devPort] - Optional development port. If provided and in 'development' NODE_ENV, 'http://localhost:${devPort}' is added to allowed origins.
41
+ * @param {string[]} config.origins - An array of allowed domain origins for Cross-Origin Resource Sharing (CORS).
42
+ * @param {string} config.host - The host address the server is bound to (used internally for configuration).
43
+ * @param {string} config.path - The base path for the API. The PeerJS path ('/peer') will be appended to this.
44
+ * @returns {Promise<object>} A promise that resolves to an object containing the final configuration and the server instance.
45
+ * @returns {import('peer').IConfig} return.options - The final options object used to create the PeerServer.
46
+ * @returns {import('peer').Server} return.peerServer - The created and listening PeerServer instance (wrapped by the listening server factory).
47
+ * @returns {object} return.meta - The module's import meta object (`import.meta`).
48
+ */
14
49
  const createPeerServer = async ({ port, devPort, origins, host, path }) => {
15
- if (process.env.NODE_ENV === 'development' && devPort) origins.push(`http://localhost:${devPort}`);
50
+ if (process.env.NODE_ENV === 'development' && devPort) {
51
+ logger.warn(`Adding development origin: http://localhost:${devPort}`);
52
+ origins.push(`http://localhost:${devPort}`);
53
+ }
54
+
16
55
  /** @type {import('peer').IConfig} */
17
56
  const options = {
18
57
  port,
58
+ // Ensure the path is correctly formatted, handling the root path case
19
59
  path: `${path === '/' ? '' : path}/peer`,
20
60
  corsOptions: {
21
61
  origin: origins,
22
62
  },
23
63
  proxied: true,
24
- // key: fs.readFileSync(''),
25
- // cert: fs.readFileSync(''),
26
- // ca: fs.readFileSync(''),
64
+ // key: fs.readFileSync(''), // Example for HTTPS/SSL
65
+ // cert: fs.readFileSync(''), // Example for HTTPS/SSL
66
+ // ca: fs.readFileSync(''), // Example for HTTPS/SSL
27
67
  };
68
+
69
+ // Use the framework's factory to listen on the server, ensuring graceful startup/shutdown
28
70
  const peerServer = UnderpostStartUp.API.listenServerFactory(() => PeerServer(options));
29
71
 
30
72
  return { options, peerServer, meta: import.meta };
@@ -1,8 +1,14 @@
1
+ /**
2
+ * Module for process and shell command management.
3
+ * Provides utilities for executing shell commands, managing signals, and handling environment details.
4
+ * @module src/server/process.js
5
+ * @namespace Process
6
+ */
7
+
1
8
  // https://nodejs.org/api/process
2
9
 
3
10
  import shell from 'shelljs';
4
11
  import dotenv from 'dotenv';
5
- import fs from 'fs-extra';
6
12
  import { loggerFactory } from './logger.js';
7
13
  import clipboard from 'clipboardy';
8
14
  import UnderpostRootEnv from '../cli/env.js';
@@ -11,9 +17,23 @@ dotenv.config();
11
17
 
12
18
  const logger = loggerFactory(import.meta);
13
19
 
20
+ /**
21
+ * Gets the current working directory, replacing backslashes with forward slashes for consistency.
22
+ * @memberof Process
23
+ * @returns {string} The root directory path.
24
+ */
14
25
  const getRootDirectory = () => process.cwd().replace(/\\/g, '/');
15
26
 
27
+ /**
28
+ * Controls and manages process-level events and signals.
29
+ * @namespace ProcessController
30
+ */
16
31
  const ProcessController = {
32
+ /**
33
+ * List of signals to listen for for graceful shutdown/handling.
34
+ * @memberof ProcessController
35
+ * @type {string[]}
36
+ */
17
37
  SIG: [
18
38
  'SIGPIPE',
19
39
  'SIGHUP',
@@ -28,6 +48,13 @@ const ProcessController = {
28
48
  'SIGSEGV',
29
49
  'SIGILL',
30
50
  ],
51
+
52
+ /**
53
+ * Sets up listeners for various process signals defined in {@link ProcessController.SIG}.
54
+ * Handles graceful exit on 'SIGINT' (Ctrl+C).
55
+ * @memberof ProcessController
56
+ * @returns {Array<process.Process>} An array of process listener handles.
57
+ */
31
58
  onSigListen: function () {
32
59
  return this.SIG.map((sig) =>
33
60
  process.on(sig, (...args) => {
@@ -42,6 +69,14 @@ const ProcessController = {
42
69
  }),
43
70
  );
44
71
  },
72
+
73
+ /**
74
+ * Initializes the ProcessController.
75
+ * Sets up signal listeners, registers a listener for the 'exit' event, and cleans up temporary deployment environment variables.
76
+ * @memberof ProcessController
77
+ * @param {Object} logger - The logger instance to use for internal logging.
78
+ * @returns {void}
79
+ */
45
80
  init: function (logger) {
46
81
  this.logger = logger;
47
82
  process.on('exit', (...args) => {
@@ -52,33 +87,82 @@ const ProcessController = {
52
87
  },
53
88
  };
54
89
 
90
+ /**
91
+ * Executes a shell command using shelljs.
92
+ * @memberof Process
93
+ * @param {string} cmd - The command string to execute.
94
+ * @param {Object} [options] - Options for execution.
95
+ * @param {boolean} [options.silent=false] - Suppress output from shell commands.
96
+ * @param {boolean} [options.async=false] - Run command asynchronously.
97
+ * @param {boolean} [options.stdout=false] - Return stdout content (string) instead of shelljs result object.
98
+ * @param {boolean} [options.disableLog=false] - Prevent logging of the command.
99
+ * @returns {string|shelljs.ShellString} The result of the shell command (string if `stdout: true`, otherwise a ShellString object).
100
+ */
55
101
  const shellExec = (cmd, options = { silent: false, async: false, stdout: false, disableLog: false }) => {
56
102
  if (!options.disableLog) logger.info(`cmd`, cmd);
57
103
  return options.stdout ? shell.exec(cmd, options).stdout : shell.exec(cmd, options);
58
104
  };
59
105
 
106
+ /**
107
+ * Changes the current working directory using shelljs.
108
+ * @memberof Process
109
+ * @param {string} cd - The path to change the directory to.
110
+ * @param {Object} [options] - Options for the CD operation.
111
+ * @param {boolean} [options.disableLog=false] - Prevent logging of the CD command.
112
+ * @returns {shelljs.ShellString} The result of the shelljs cd command.
113
+ */
60
114
  const shellCd = (cd, options = { disableLog: false }) => {
61
115
  if (!options.disableLog) logger.info(`cd`, cd);
62
116
  return shell.cd(cd);
63
117
  };
64
118
 
119
+ /**
120
+ * Opens a new GNOME terminal and executes a command.
121
+ * Note: This function is environment-specific (GNOME/Linux).
122
+ * @memberof Process
123
+ * @param {string} cmd - The command to execute in the new terminal.
124
+ * @param {Object} [options] - Options for the terminal opening.
125
+ * @param {boolean} [options.single=false] - If true, execute as a single session process using `setsid`.
126
+ * @returns {void}
127
+ */
65
128
  const openTerminal = (cmd, options = { single: false }) => {
66
129
  if (options.single === true) {
130
+ // Run as a single session process
67
131
  shellExec(`setsid gnome-terminal -- bash -ic "${cmd}; exec bash" >/dev/null 2>&1 &`);
68
132
  return;
69
133
  }
134
+ // Run asynchronously and disown
70
135
  shellExec(`gnome-terminal -- bash -c "${cmd}; exec bash" & disown`, {
71
136
  async: true,
72
137
  stdout: true,
73
138
  });
74
139
  };
75
140
 
141
+ /**
142
+ * Wraps a command to run it as a daemon process in a shell (keeping the process alive/terminal open).
143
+ * @memberof Process
144
+ * @param {string} cmd - The command to daemonize.
145
+ * @returns {string} The shell command string for the daemon process.
146
+ */
76
147
  const daemonProcess = (cmd) => `exec bash -c '${cmd}; exec tail -f /dev/null'`;
77
148
 
149
+ /**
150
+ * Retrieves the process ID (PID) of the most recently created gnome-terminal instance.
151
+ * Note: This function is environment-specific (GNOME/Linux) and uses `pgrep -n`.
152
+ * @memberof Process
153
+ * @returns {number} The PID of the last gnome-terminal process.
154
+ */
78
155
  // list all terminals: pgrep gnome-terminal
79
156
  // list last terminal: pgrep -n gnome-terminal
80
157
  const getTerminalPid = () => JSON.parse(shellExec(`pgrep -n gnome-terminal`, { stdout: true, silent: true }));
81
158
 
159
+ /**
160
+ * Copies text content to the system clipboard using clipboardy.
161
+ * Logs the copied content for confirmation.
162
+ * @memberof Process
163
+ * @param {string} [data='🦄'] - The data to copy. Defaults to '🦄'.
164
+ * @returns {void}
165
+ */
82
166
  function pbcopy(data) {
83
167
  clipboard.writeSync(data || '🦄');
84
168
  logger.info(`copied to clipboard`, clipboard.readSync());
@@ -99,6 +99,8 @@ const buildRuntime = async () => {
99
99
  return next();
100
100
  });
101
101
 
102
+ if (process.env.NODE_ENV === 'production') app.set('trust proxy', true);
103
+
102
104
  app.use((req, res, next) => {
103
105
  requestCounter.inc({
104
106
  instance: `${host}:${port}${path}`,
@@ -129,17 +131,6 @@ const buildRuntime = async () => {
129
131
  return compression.filter(req, res);
130
132
  }
131
133
 
132
- if (process.argv.includes('static')) {
133
- logger.info('Build static server runtime', `${host}${path}`);
134
- currentPort += 2;
135
- const staticPort = newInstance(currentPort);
136
-
137
- await UnderpostStartUp.API.listenPortController(app, staticPort, runningData);
138
- currentPort++;
139
- continue;
140
- }
141
- logger.info('Build api server runtime', `${host}${path}`);
142
-
143
134
  // parse requests of content-type - application/json
144
135
  app.use(express.json({ limit: '100MB' }));
145
136
 
@@ -163,6 +154,23 @@ const buildRuntime = async () => {
163
154
 
164
155
  // instance public static
165
156
  app.use('/', express.static(directory ? directory : `.${rootHostPath}`));
157
+ if (process.argv.includes('static')) {
158
+ logger.info('Build static server runtime', `${host}${path}`);
159
+ currentPort += 2;
160
+ const staticPort = newInstance(currentPort);
161
+ await UnderpostStartUp.API.listenPortController(app, staticPort, runningData);
162
+ currentPort++;
163
+ continue;
164
+ }
165
+
166
+ // Flag swagger requests before security middleware is applied
167
+ const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
168
+ const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
169
+ if (fs.existsSync(swaggerJsonPath))
170
+ app.use(swaggerPath, (req, res, next) => {
171
+ res.locals.isSwagger = true;
172
+ next();
173
+ });
166
174
 
167
175
  // security
168
176
  applySecurity(app, {
@@ -194,22 +202,14 @@ const buildRuntime = async () => {
194
202
  if (peer) currentPort++;
195
203
 
196
204
  if (!apiBaseHost) {
197
- const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
198
205
  if (fs.existsSync(swaggerJsonPath)) {
199
- // logger.info('Build swagger serve', swaggerJsonPath);
200
-
201
206
  const swaggerInstance =
202
207
  (swaggerDoc) =>
203
208
  (...args) =>
204
209
  swaggerUi.setup(swaggerDoc)(...args);
205
-
206
210
  const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
207
-
208
- app.use(
209
- `${path === '/' ? `/api-docs` : `${path}/api-docs`}`,
210
- swaggerUi.serve,
211
- swaggerInstance(swaggerDoc),
212
- );
211
+ const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
212
+ app.use(swaggerPath, swaggerUi.serve, swaggerInstance(swaggerDoc));
213
213
  }
214
214
 
215
215
  if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
@@ -228,12 +228,12 @@ const buildRuntime = async () => {
228
228
  templates: mailerSsrConf ? mailerSsrConf.mailer : {},
229
229
  });
230
230
  }
231
- if (apis) {
231
+ if (apis && apis.length > 0) {
232
232
  const authMiddleware = authMiddlewareFactory({ host, path });
233
-
234
233
  const apiPath = `${path === '/' ? '' : path}/${process.env.BASE_API}`;
235
234
  for (const api of apis)
236
235
  await (async () => {
236
+ logger.info(`Build api server`, `${host}${apiPath}/${api}`);
237
237
  const { ApiRouter } = await import(`../api/${api}/${api}.router.js`);
238
238
  const router = ApiRouter({ host, path, apiPath, mailer, db, authMiddleware, origins });
239
239
  // router.use(cors({ origin: origins }));
package/src/server/ssr.js CHANGED
@@ -1,8 +1,16 @@
1
+ /**
2
+ * Module for managing server side rendering
3
+ * @module src/server/ssr.js
4
+ * @namespace SSR
5
+ */
6
+
1
7
  import fs from 'fs-extra';
2
8
  import dotenv from 'dotenv';
9
+ import vm from 'node:vm';
10
+
3
11
  import Underpost from '../index.js';
4
12
 
5
- import { ssrFactory, JSONweb } from './client-formatted.js';
13
+ import { srcFormatted, JSONweb } from './client-formatted.js';
6
14
  import { loggerFactory } from './logger.js';
7
15
  import { getRootDirectory } from './process.js';
8
16
 
@@ -10,6 +18,48 @@ dotenv.config();
10
18
 
11
19
  const logger = loggerFactory(import.meta);
12
20
 
21
+ /**
22
+ * Creates a server-side rendering component function from a given file path.
23
+ * It reads the component file, formats it, and executes it in a sandboxed Node.js VM context to extract the component.
24
+ * @param {string} [componentPath='./src/client/ssr/Render.js'] - The path to the SSR component file.
25
+ * @returns {Promise<Function>} A promise that resolves to the SSR component function.
26
+ * @memberof SSR
27
+ */
28
+ const ssrFactory = async (componentPath = `./src/client/ssr/Render.js`) => {
29
+ const context = { SrrComponent: () => {}, npm_package_version: Underpost.version };
30
+ vm.createContext(context);
31
+ vm.runInContext(await srcFormatted(fs.readFileSync(componentPath, 'utf8')), context);
32
+ return context.SrrComponent;
33
+ };
34
+
35
+ /**
36
+ * Sanitizes an HTML string by adding a nonce to all script and style tags for Content Security Policy (CSP).
37
+ * The nonce is retrieved from `res.locals.nonce`.
38
+ * @param {object} res - The Express response object.
39
+ * @param {object} req - The Express request object.
40
+ * @param {string} html - The HTML string to sanitize.
41
+ * @returns {string} The sanitized HTML string with nonces.
42
+ * @memberof SSR
43
+ */
44
+ const sanitizeHtml = (res, req, html) => {
45
+ const nonce = res.locals.nonce;
46
+
47
+ return html
48
+ .replace(/<script(?=\s|>)/gi, `<script nonce="${nonce}"`)
49
+ .replace(/<style(?=\s|>)/gi, `<style nonce="${nonce}"`);
50
+ };
51
+
52
+ /**
53
+ * Factory function to create Express middleware for handling 404 and 500 errors.
54
+ * It generates server-side rendered HTML for these error pages. If static error pages exist, it redirects to them.
55
+ * @param {object} options - The options for creating the middleware.
56
+ * @param {object} options.app - The Express app instance.
57
+ * @param {string} options.directory - The directory for the instance's static files.
58
+ * @param {string} options.rootHostPath - The root path for the host's public files.
59
+ * @param {string} options.path - The base path for the instance.
60
+ * @returns {Promise<{error500: Function, error400: Function}>} A promise that resolves to an object containing the 500 and 404 error handling middleware.
61
+ * @memberof SSR
62
+ */
13
63
  const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => {
14
64
  const Render = await ssrFactory();
15
65
  const ssrPath = path === '/' ? path : `${path}/`;
@@ -48,14 +98,6 @@ const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => {
48
98
  const path500 = `${directory ? directory : `${getRootDirectory()}${rootHostPath}`}/500/index.html`;
49
99
  const page500 = fs.existsSync(path500) ? `${path === '/' ? '' : path}/500` : undefined;
50
100
 
51
- const sanitizeHtml = (res, req, html) => {
52
- const nonce = res.locals.nonce;
53
-
54
- return html
55
- .replace(/<script(?=\s|>)/gi, `<script nonce="${nonce}"`)
56
- .replace(/<style(?=\s|>)/gi, `<style nonce="${nonce}"`);
57
- };
58
-
59
101
  return {
60
102
  error500: function (err, req, res, next) {
61
103
  logger.error(err, err.stack);
@@ -82,4 +124,4 @@ const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => {
82
124
  };
83
125
  };
84
126
 
85
- export { ssrMiddlewareFactory };
127
+ export { ssrMiddlewareFactory, ssrFactory, sanitizeHtml };