@underpostnet/underpost 2.99.8 → 3.0.1
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/.env.production +1 -0
- package/.github/workflows/gitlab.ci.yml +20 -0
- package/.github/workflows/publish.ci.yml +18 -34
- package/.vscode/extensions.json +8 -50
- package/.vscode/settings.json +0 -77
- package/CHANGELOG.md +116 -1
- package/{cli.md → CLI-HELP.md} +48 -41
- package/README.md +3 -3
- package/bin/build.js +1 -15
- package/bin/deploy.js +4 -133
- package/bin/file.js +10 -8
- package/bin/zed.js +63 -2
- package/jsdoc.json +1 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/deployment/fastapi/initial_data.sh +4 -52
- package/manifests/ipfs/configmap.yaml +57 -0
- package/manifests/ipfs/headless-service.yaml +35 -0
- package/manifests/ipfs/kustomization.yaml +8 -0
- package/manifests/ipfs/statefulset.yaml +149 -0
- package/manifests/ipfs/storage-class.yaml +9 -0
- package/package.json +9 -5
- package/scripts/k3s-node-setup.sh +89 -0
- package/scripts/lxd-vm-setup.sh +23 -0
- package/scripts/rocky-setup.sh +1 -13
- package/src/api/user/user.router.js +0 -47
- package/src/cli/baremetal.js +7 -9
- package/src/cli/cluster.js +72 -121
- package/src/cli/deploy.js +8 -5
- package/src/cli/index.js +31 -30
- package/src/cli/ipfs.js +184 -0
- package/src/cli/lxd.js +192 -237
- package/src/cli/repository.js +4 -1
- package/src/cli/run.js +3 -2
- package/src/client/components/core/Docs.js +92 -6
- package/src/client/components/core/VanillaJs.js +36 -25
- package/src/client/services/user/user.management.js +0 -5
- package/src/client/services/user/user.service.js +1 -1
- package/src/index.js +12 -1
- package/src/runtime/express/Express.js +3 -2
- package/src/server/client-build-docs.js +178 -41
- package/src/server/conf.js +1 -1
- package/src/server/logger.js +22 -10
- package/.vscode/zed.keymap.json +0 -39
- package/.vscode/zed.settings.json +0 -20
- package/manifests/lxd/underpost-setup.sh +0 -163
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Badge } from './Badge.js';
|
|
2
2
|
import { BtnIcon } from './BtnIcon.js';
|
|
3
|
-
import { Css, renderCssAttr, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
|
|
3
|
+
import { Css, darkTheme, renderCssAttr, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
|
|
4
4
|
import { buildBadgeToolTipMenuOption, Modal, renderViewTitle } from './Modal.js';
|
|
5
5
|
import { listenQueryPathInstance, setQueryPath, closeModalRouteChangeEvent, getProxyPath } from './Router.js';
|
|
6
|
-
import { htmls, s } from './VanillaJs.js';
|
|
6
|
+
import { htmls, s, sIframe } from './VanillaJs.js';
|
|
7
7
|
|
|
8
8
|
// https://mintlify.com/docs/quickstart
|
|
9
9
|
|
|
@@ -39,6 +39,7 @@ const Docs = {
|
|
|
39
39
|
RouterInstance: Modal.Data['modal-docs'].options.RouterInstance,
|
|
40
40
|
});
|
|
41
41
|
const iframeEl = s(`.iframe-${ModalId}`);
|
|
42
|
+
let swaggerThemeEventKey = null;
|
|
42
43
|
if (iframeEl) {
|
|
43
44
|
iframeEl.addEventListener('load', () => {
|
|
44
45
|
try {
|
|
@@ -51,7 +52,95 @@ const Docs = {
|
|
|
51
52
|
// cross-origin or security restriction — safe to ignore
|
|
52
53
|
}
|
|
53
54
|
window.scrollTo(0, 0);
|
|
55
|
+
// Bind Shift+K inside the iframe to focus the parent SearchBox (mirrors app-wide shortcut)
|
|
56
|
+
try {
|
|
57
|
+
const iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow?.document;
|
|
58
|
+
if (iframeDoc) {
|
|
59
|
+
iframeDoc.addEventListener('keydown', (e) => {
|
|
60
|
+
if (e.shiftKey && e.key.toLowerCase() === 'k') {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
if (s(`.top-bar-search-box`)) {
|
|
64
|
+
if (s(`.main-body-btn-ui-close`) && s(`.main-body-btn-ui-close`).classList.contains('hide')) {
|
|
65
|
+
s(`.main-body-btn-ui-open`).click();
|
|
66
|
+
}
|
|
67
|
+
s(`.top-bar-search-box`).blur();
|
|
68
|
+
s(`.top-bar-search-box`).focus();
|
|
69
|
+
s(`.top-bar-search-box`).select();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// cross-origin or security restriction — safe to ignore
|
|
76
|
+
}
|
|
54
77
|
});
|
|
78
|
+
|
|
79
|
+
if (type === 'src') {
|
|
80
|
+
swaggerThemeEventKey = `jsdocs-iframe-${ModalId}`;
|
|
81
|
+
|
|
82
|
+
const applyJsDocsTheme = (isDark) => {
|
|
83
|
+
try {
|
|
84
|
+
const iframeWin = iframeEl.contentWindow;
|
|
85
|
+
if (!iframeWin) return;
|
|
86
|
+
const theme = isDark ? 'dark' : 'light';
|
|
87
|
+
if (typeof iframeWin.updateTheme === 'function') {
|
|
88
|
+
// clean-jsdoc-theme exposes updateTheme() globally
|
|
89
|
+
iframeWin.updateTheme(theme);
|
|
90
|
+
} else {
|
|
91
|
+
// Fallback: replicate localUpdateTheme manually
|
|
92
|
+
const iframeDoc = iframeEl.contentDocument || iframeWin.document;
|
|
93
|
+
if (!iframeDoc || !iframeDoc.body) return;
|
|
94
|
+
iframeDoc.body.setAttribute('data-theme', theme);
|
|
95
|
+
iframeDoc.body.classList.remove('dark', 'light');
|
|
96
|
+
iframeDoc.body.classList.add(theme);
|
|
97
|
+
const iconID = isDark ? '#light-theme-icon' : '#dark-theme-icon';
|
|
98
|
+
const svgUses = sIframe(iframeEl, '.theme-svg-use') ? iframeDoc.querySelectorAll('.theme-svg-use') : [];
|
|
99
|
+
svgUses.forEach((svg) => svg.setAttribute('xlink:href', iconID));
|
|
100
|
+
iframeWin.localStorage?.setItem('theme', theme);
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// cross-origin or security restriction — safe to ignore
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Apply current theme as soon as the iframe content is ready
|
|
108
|
+
iframeEl.addEventListener('load', () => applyJsDocsTheme(darkTheme));
|
|
109
|
+
|
|
110
|
+
// Keep in sync whenever the parent page theme changes
|
|
111
|
+
ThemeEvents[swaggerThemeEventKey] = () => {
|
|
112
|
+
if (s(`.iframe-${ModalId}`)) applyJsDocsTheme(darkTheme);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (type === 'api') {
|
|
117
|
+
swaggerThemeEventKey = `swagger-iframe-${ModalId}`;
|
|
118
|
+
|
|
119
|
+
const applySwaggerTheme = (isDark) => {
|
|
120
|
+
try {
|
|
121
|
+
const iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow?.document;
|
|
122
|
+
if (!iframeDoc || !iframeDoc.body) return;
|
|
123
|
+
if (isDark) {
|
|
124
|
+
iframeDoc.body.classList.add('swagger-dark');
|
|
125
|
+
} else {
|
|
126
|
+
iframeDoc.body.classList.remove('swagger-dark');
|
|
127
|
+
}
|
|
128
|
+
iframeEl.contentWindow?.localStorage?.setItem('swagger-theme', isDark ? 'dark' : 'light');
|
|
129
|
+
const toggleBtn = sIframe(iframeEl, '#swagger-theme-toggle');
|
|
130
|
+
if (toggleBtn) toggleBtn.textContent = isDark ? '\u2600\uFE0F Light Mode' : '\uD83C\uDF19 Dark Mode';
|
|
131
|
+
} catch (e) {
|
|
132
|
+
// cross-origin or security restriction — safe to ignore
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Apply current theme as soon as the iframe content is ready
|
|
137
|
+
iframeEl.addEventListener('load', () => applySwaggerTheme(darkTheme));
|
|
138
|
+
|
|
139
|
+
// Keep in sync whenever the parent page theme changes
|
|
140
|
+
ThemeEvents[swaggerThemeEventKey] = () => {
|
|
141
|
+
if (s(`.iframe-${ModalId}`)) applySwaggerTheme(darkTheme);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
55
144
|
}
|
|
56
145
|
Modal.Data[ModalId].onObserverListener[ModalId] = () => {
|
|
57
146
|
if (s(`.iframe-${ModalId}`)) {
|
|
@@ -67,6 +156,7 @@ const Docs = {
|
|
|
67
156
|
};
|
|
68
157
|
Modal.Data[ModalId].onObserverListener[ModalId]();
|
|
69
158
|
Modal.Data[ModalId].onCloseListener[ModalId] = () => {
|
|
159
|
+
if (swaggerThemeEventKey) delete ThemeEvents[swaggerThemeEventKey];
|
|
70
160
|
closeModalRouteChangeEvent({ closedId: ModalId });
|
|
71
161
|
};
|
|
72
162
|
},
|
|
@@ -342,10 +432,6 @@ const Docs = {
|
|
|
342
432
|
<div class="docs-landing">
|
|
343
433
|
<div class="docs-header">
|
|
344
434
|
<h1>Documentation</h1>
|
|
345
|
-
<p>
|
|
346
|
-
Find everything you need to build amazing applications with our platform. Get started with our guides, API
|
|
347
|
-
reference, and examples.
|
|
348
|
-
</p>
|
|
349
435
|
<!--
|
|
350
436
|
<div class="search-bar">
|
|
351
437
|
<i class="fas fa-search"></i>
|
|
@@ -7,31 +7,6 @@
|
|
|
7
7
|
import { s4 } from './CommonJs.js';
|
|
8
8
|
import { windowGetH, windowGetW } from './windowGetDimensions.js';
|
|
9
9
|
|
|
10
|
-
/*
|
|
11
|
-
|
|
12
|
-
Name: es6-string-html
|
|
13
|
-
Id: Tobermory.es6-string-html
|
|
14
|
-
Description: Syntax highlighting in es6 multiline strings
|
|
15
|
-
Version: 2.12.1
|
|
16
|
-
Publisher: Tobermory
|
|
17
|
-
VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html
|
|
18
|
-
|
|
19
|
-
Name: es6-string-css
|
|
20
|
-
Id: bashmish.es6-string-css
|
|
21
|
-
Description: Highlight CSS language in ES6 template literals
|
|
22
|
-
Version: 0.1.0
|
|
23
|
-
Publisher: Mikhail Bashkirov
|
|
24
|
-
VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=bashmish.es6-string-css
|
|
25
|
-
|
|
26
|
-
Name: lit-html
|
|
27
|
-
Id: bierner.lit-html
|
|
28
|
-
Description: Syntax highlighting and IntelliSense for html inside of JavaScript and TypeScript tagged template strings
|
|
29
|
-
Version: 1.11.1
|
|
30
|
-
Publisher: Matt Bierner
|
|
31
|
-
VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=bierner.lit-html
|
|
32
|
-
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
10
|
/**
|
|
36
11
|
* Query selector.
|
|
37
12
|
*
|
|
@@ -443,12 +418,48 @@ function hexToRgbA(hex) {
|
|
|
443
418
|
|
|
444
419
|
const htmlStrSanitize = (str) => (str ? str.replace(/<\/?[^>]+(>|$)/g, '').trim() : '');
|
|
445
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Query selector inside an iframe. Allows obtaining a single element that is
|
|
423
|
+
* inside an iframe in order to execute events on it.
|
|
424
|
+
* Note: the iframe must be same-origin for this to work.
|
|
425
|
+
*
|
|
426
|
+
* @param {string|Element} iframeEl The CSS selector string or the iframe Element itself.
|
|
427
|
+
* @param {string} el The query selector for the element inside the iframe.
|
|
428
|
+
* @returns {Element|null} The matched element inside the iframe, or null if not found.
|
|
429
|
+
* @memberof VanillaJS
|
|
430
|
+
*/
|
|
431
|
+
const sIframe = (iframeEl, el) => {
|
|
432
|
+
const iframe = typeof iframeEl === 'string' ? s(iframeEl) : iframeEl;
|
|
433
|
+
if (!iframe) return null;
|
|
434
|
+
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
435
|
+
return iframeDoc ? iframeDoc.querySelector(el) : null;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Query selector all inside an iframe. Allows obtaining all elements matching a
|
|
440
|
+
* selector that are inside an iframe in order to execute events on them.
|
|
441
|
+
* Note: the iframe must be same-origin for this to work.
|
|
442
|
+
*
|
|
443
|
+
* @param {string|Element} iframeEl The CSS selector string or the iframe Element itself.
|
|
444
|
+
* @param {string} el The query selector for the elements inside the iframe.
|
|
445
|
+
* @returns {NodeList|null} A NodeList of matched elements inside the iframe, or null if not found.
|
|
446
|
+
* @memberof VanillaJS
|
|
447
|
+
*/
|
|
448
|
+
const saIframe = (iframeEl, el) => {
|
|
449
|
+
const iframe = typeof iframeEl === 'string' ? s(iframeEl) : iframeEl;
|
|
450
|
+
if (!iframe) return null;
|
|
451
|
+
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
452
|
+
return iframeDoc ? iframeDoc.querySelectorAll(el) : null;
|
|
453
|
+
};
|
|
454
|
+
|
|
446
455
|
export {
|
|
447
456
|
s,
|
|
448
457
|
htmls,
|
|
449
458
|
append,
|
|
450
459
|
prepend,
|
|
451
460
|
sa,
|
|
461
|
+
sIframe,
|
|
462
|
+
saIframe,
|
|
452
463
|
copyData,
|
|
453
464
|
pasteData,
|
|
454
465
|
preHTML,
|
|
@@ -38,7 +38,7 @@ const UserService = {
|
|
|
38
38
|
}),
|
|
39
39
|
),
|
|
40
40
|
get: (options = {}) => {
|
|
41
|
-
const { id, page, limit, filterModel, sortModel, sort, asc, order } = options;
|
|
41
|
+
const { id = 'all', page, limit, filterModel, sortModel, sort, asc, order } = options;
|
|
42
42
|
const url = buildQueryUrl(getApiBaseUrl({ id, endpoint }), {
|
|
43
43
|
page,
|
|
44
44
|
limit,
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import UnderpostDB from './cli/db.js';
|
|
|
12
12
|
import UnderpostDeploy from './cli/deploy.js';
|
|
13
13
|
import UnderpostRootEnv from './cli/env.js';
|
|
14
14
|
import UnderpostFileStorage from './cli/fs.js';
|
|
15
|
+
import UnderpostIPFS from './cli/ipfs.js';
|
|
15
16
|
import UnderpostImage from './cli/image.js';
|
|
16
17
|
import UnderpostLxd from './cli/lxd.js';
|
|
17
18
|
import UnderpostMonitor from './cli/monitor.js';
|
|
@@ -41,7 +42,7 @@ class Underpost {
|
|
|
41
42
|
* @type {String}
|
|
42
43
|
* @memberof Underpost
|
|
43
44
|
*/
|
|
44
|
-
static version = '
|
|
45
|
+
static version = 'v3.0.1';
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Required Node.js major version
|
|
@@ -143,6 +144,15 @@ class Underpost {
|
|
|
143
144
|
static get fs() {
|
|
144
145
|
return UnderpostFileStorage.API;
|
|
145
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* IPFS cli API
|
|
149
|
+
* @static
|
|
150
|
+
* @type {UnderpostIPFS.API}
|
|
151
|
+
* @memberof Underpost
|
|
152
|
+
*/
|
|
153
|
+
static get ipfs() {
|
|
154
|
+
return UnderpostIPFS.API;
|
|
155
|
+
}
|
|
146
156
|
/**
|
|
147
157
|
* Monitor cli API
|
|
148
158
|
* @static
|
|
@@ -296,6 +306,7 @@ export {
|
|
|
296
306
|
UnderpostStatic,
|
|
297
307
|
UnderpostLxd,
|
|
298
308
|
UnderpostKickStart,
|
|
309
|
+
UnderpostIPFS,
|
|
299
310
|
UnderpostMonitor,
|
|
300
311
|
UnderpostRepository,
|
|
301
312
|
UnderpostRun,
|
|
@@ -20,6 +20,7 @@ import { createPeerServer } from '../../server/peer.js';
|
|
|
20
20
|
import { createValkeyConnection } from '../../server/valkey.js';
|
|
21
21
|
import { applySecurity, authMiddlewareFactory } from '../../server/auth.js';
|
|
22
22
|
import { ssrMiddlewareFactory } from '../../server/ssr.js';
|
|
23
|
+
import { buildSwaggerUiOptions } from '../../server/client-build-docs.js';
|
|
23
24
|
|
|
24
25
|
import { shellExec } from '../../server/process.js';
|
|
25
26
|
import { devProxyHostFactory, isDevProxyContext, isTlsDevProxy } from '../../server/conf.js';
|
|
@@ -167,8 +168,8 @@ class ExpressService {
|
|
|
167
168
|
// Swagger UI setup
|
|
168
169
|
if (fs.existsSync(swaggerJsonPath)) {
|
|
169
170
|
const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
|
|
170
|
-
|
|
171
|
-
app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc));
|
|
171
|
+
const swaggerUiOptions = await buildSwaggerUiOptions();
|
|
172
|
+
app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc, swaggerUiOptions));
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
// Security and CORS
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import fs from 'fs-extra';
|
|
10
|
-
import swaggerAutoGen from 'swagger-autogen';
|
|
11
10
|
import { shellExec } from './process.js';
|
|
12
11
|
import { loggerFactory } from './logger.js';
|
|
13
12
|
import { JSONweb } from './client-formatted.js';
|
|
13
|
+
import { ssrFactory } from './ssr.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Builds API documentation using Swagger
|
|
@@ -63,53 +63,91 @@ const buildApiDocs = async ({
|
|
|
63
63
|
components: {
|
|
64
64
|
schemas: {
|
|
65
65
|
userRequest: {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
type: 'object',
|
|
67
|
+
required: ['username', 'password', 'email'],
|
|
68
|
+
properties: {
|
|
69
|
+
username: { type: 'string', example: 'user123' },
|
|
70
|
+
password: { type: 'string', example: 'Password123!' },
|
|
71
|
+
email: { type: 'string', format: 'email', example: 'user@example.com' },
|
|
72
|
+
},
|
|
69
73
|
},
|
|
70
74
|
userResponse: {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
status: { type: 'string', example: 'success' },
|
|
78
|
+
data: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
token: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjY2YzM3N2Y1N2Y5OWU1OTY5YjgxZG...',
|
|
84
|
+
},
|
|
85
|
+
user: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
_id: { type: 'string', example: '66c377f57f99e5969b81de89' },
|
|
89
|
+
email: { type: 'string', format: 'email', example: 'user@example.com' },
|
|
90
|
+
emailConfirmed: { type: 'boolean', example: false },
|
|
91
|
+
username: { type: 'string', example: 'user123' },
|
|
92
|
+
role: { type: 'string', example: 'user' },
|
|
93
|
+
profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
81
97
|
},
|
|
82
98
|
},
|
|
83
99
|
},
|
|
84
100
|
userUpdateResponse: {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
status: { type: 'string', example: 'success' },
|
|
104
|
+
data: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
_id: { type: 'string', example: '66c377f57f99e5969b81de89' },
|
|
108
|
+
email: { type: 'string', format: 'email', example: 'user@example.com' },
|
|
109
|
+
emailConfirmed: { type: 'boolean', example: false },
|
|
110
|
+
username: { type: 'string', example: 'user123222' },
|
|
111
|
+
role: { type: 'string', example: 'user' },
|
|
112
|
+
profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
93
115
|
},
|
|
94
116
|
},
|
|
95
117
|
userGetResponse: {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
status: { type: 'string', example: 'success' },
|
|
121
|
+
data: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
_id: { type: 'string', example: '66c377f57f99e5969b81de89' },
|
|
125
|
+
email: { type: 'string', format: 'email', example: 'user@example.com' },
|
|
126
|
+
emailConfirmed: { type: 'boolean', example: false },
|
|
127
|
+
username: { type: 'string', example: 'user123222' },
|
|
128
|
+
role: { type: 'string', example: 'user' },
|
|
129
|
+
profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
104
132
|
},
|
|
105
133
|
},
|
|
106
134
|
userLogInRequest: {
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
type: 'object',
|
|
136
|
+
required: ['email', 'password'],
|
|
137
|
+
properties: {
|
|
138
|
+
email: { type: 'string', format: 'email', example: 'user@example.com' },
|
|
139
|
+
password: { type: 'string', example: 'Password123!' },
|
|
140
|
+
},
|
|
109
141
|
},
|
|
110
142
|
userBadRequestResponse: {
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
status: { type: 'string', example: 'error' },
|
|
146
|
+
message: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
example: 'Bad request. Please check your inputs, and try again',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
113
151
|
},
|
|
114
152
|
},
|
|
115
153
|
securitySchemes: {
|
|
@@ -121,15 +159,100 @@ const buildApiDocs = async ({
|
|
|
121
159
|
},
|
|
122
160
|
};
|
|
123
161
|
|
|
162
|
+
/**
|
|
163
|
+
* swagger-autogen has no requestBody annotation support — it only handles
|
|
164
|
+
* #swagger.parameters, responses, security, etc. We define the requestBody
|
|
165
|
+
* objects here and inject them into the generated JSON as a post-processing step.
|
|
166
|
+
*
|
|
167
|
+
* Each key is an "<method> <path>" pair matching the generated paths object.
|
|
168
|
+
* The value is a valid OAS 3.0 requestBody object.
|
|
169
|
+
*/
|
|
170
|
+
const requestBodies = {
|
|
171
|
+
'post /user': {
|
|
172
|
+
description: 'User registration data',
|
|
173
|
+
required: true,
|
|
174
|
+
content: {
|
|
175
|
+
'application/json': {
|
|
176
|
+
schema: { $ref: '#/components/schemas/userRequest' },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
'post /user/auth': {
|
|
181
|
+
description: 'User login credentials',
|
|
182
|
+
required: true,
|
|
183
|
+
content: {
|
|
184
|
+
'application/json': {
|
|
185
|
+
schema: { $ref: '#/components/schemas/userLogInRequest' },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
'put /user/{id}': {
|
|
190
|
+
description: 'User fields to update',
|
|
191
|
+
required: true,
|
|
192
|
+
content: {
|
|
193
|
+
'application/json': {
|
|
194
|
+
schema: { $ref: '#/components/schemas/userRequest' },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
124
200
|
logger.warn('build swagger api docs', doc.info);
|
|
125
201
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
202
|
+
// swagger-autogen@2.9.2 bug: getProducesTag, getConsumesTag, getResponsesTag missing __¬¬¬__ decode before eval
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
`node_modules/swagger-autogen/src/swagger-tags.js`,
|
|
205
|
+
fs
|
|
206
|
+
.readFileSync(`node_modules/swagger-autogen/src/swagger-tags.js`, 'utf8')
|
|
207
|
+
// getProducesTag and getConsumesTag: already decode " but not __¬¬¬__
|
|
208
|
+
.replaceAll(
|
|
209
|
+
`data.replaceAll('\\n', ' ').replaceAll('\u201c', '\u201d')`,
|
|
210
|
+
`data.replaceAll('\\n', ' ').replaceAll('\u201c', '\u201d').replaceAll('__\u00ac\u00ac\u00ac__', '"')`,
|
|
211
|
+
)
|
|
212
|
+
// getResponsesTag: decodes neither " nor __¬¬¬__
|
|
213
|
+
.replaceAll(
|
|
214
|
+
`data.replaceAll('\\n', ' ');`,
|
|
215
|
+
`data.replaceAll('\\n', ' ').replaceAll('__\u00ac\u00ac\u00ac__', '"');`,
|
|
216
|
+
),
|
|
217
|
+
'utf8',
|
|
218
|
+
);
|
|
219
|
+
setTimeout(async () => {
|
|
220
|
+
const { default: swaggerAutoGen } = await import('swagger-autogen');
|
|
221
|
+
const outputFile = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
|
|
222
|
+
const routes = [];
|
|
223
|
+
for (const api of apis) {
|
|
224
|
+
if (['user'].includes(api)) routes.push(`./src/api/${api}/${api}.router.js`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await swaggerAutoGen({ openapi: '3.0.0' })(outputFile, routes, doc);
|
|
228
|
+
|
|
229
|
+
// Post-process: inject requestBody into operations — swagger-autogen silently
|
|
230
|
+
// ignores #swagger.requestBody annotations and has no internal OAS-3 body support.
|
|
231
|
+
if (fs.existsSync(outputFile)) {
|
|
232
|
+
const swaggerJson = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
|
|
233
|
+
let patched = false;
|
|
234
|
+
|
|
235
|
+
for (const [key, requestBody] of Object.entries(requestBodies)) {
|
|
236
|
+
const [method, ...pathParts] = key.split(' ');
|
|
237
|
+
const opPath = pathParts.join(' ');
|
|
238
|
+
if (swaggerJson.paths?.[opPath]?.[method]) {
|
|
239
|
+
swaggerJson.paths[opPath][method].requestBody = requestBody;
|
|
240
|
+
// Remove any stale in:body entry from parameters (OAS 3.0 doesn't allow it)
|
|
241
|
+
if (Array.isArray(swaggerJson.paths[opPath][method].parameters)) {
|
|
242
|
+
swaggerJson.paths[opPath][method].parameters = swaggerJson.paths[opPath][method].parameters.filter(
|
|
243
|
+
(p) => p.in !== 'body',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
patched = true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
131
249
|
|
|
132
|
-
|
|
250
|
+
if (patched) {
|
|
251
|
+
fs.writeFileSync(outputFile, JSON.stringify(swaggerJson, null, 2), 'utf8');
|
|
252
|
+
// logger.warn('swagger post-process: requestBody injected', Object.keys(requestBodies));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
133
256
|
};
|
|
134
257
|
|
|
135
258
|
/**
|
|
@@ -228,4 +351,18 @@ const buildDocs = async ({
|
|
|
228
351
|
});
|
|
229
352
|
};
|
|
230
353
|
|
|
231
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Builds Swagger UI customization options by rendering the SwaggerDarkMode SSR body component.
|
|
356
|
+
* Returns the customCss and customJsStr strings required by swagger-ui-express to enable
|
|
357
|
+
* a dark/light mode toggle button with a black/gray gradient dark theme.
|
|
358
|
+
* @function buildSwaggerUiOptions
|
|
359
|
+
* @memberof clientBuildDocs
|
|
360
|
+
* @returns {Promise<{customCss: string, customJsStr: string}>} Swagger UI setup options
|
|
361
|
+
*/
|
|
362
|
+
const buildSwaggerUiOptions = async () => {
|
|
363
|
+
const swaggerDarkMode = await ssrFactory('./src/client/ssr/body/SwaggerDarkMode.js');
|
|
364
|
+
const { css, js } = swaggerDarkMode();
|
|
365
|
+
return { customCss: css, customJsStr: js };
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
export { buildDocs, buildSwaggerUiOptions };
|
package/src/server/conf.js
CHANGED
|
@@ -1335,7 +1335,7 @@ const buildCliDoc = (program, oldVersion, newVersion) => {
|
|
|
1335
1335
|
});
|
|
1336
1336
|
md = md.replaceAll(oldVersion, newVersion);
|
|
1337
1337
|
fs.writeFileSync(`./src/client/public/nexodev/docs/references/Command Line Interface.md`, md, 'utf8');
|
|
1338
|
-
fs.writeFileSync(`./
|
|
1338
|
+
fs.writeFileSync(`./CLI-HELP.md`, md, 'utf8');
|
|
1339
1339
|
const readme = fs.readFileSync(`./README.md`, 'utf8');
|
|
1340
1340
|
fs.writeFileSync('./README.md', readme.replaceAll(oldVersion, newVersion), 'utf8');
|
|
1341
1341
|
};
|
package/src/server/logger.js
CHANGED
|
@@ -101,27 +101,37 @@ const setUpInfo = async (logger = new winston.Logger()) => {
|
|
|
101
101
|
* @param meta - The `meta` parameter in the `loggerFactory` function is used to extract the last part
|
|
102
102
|
* of a URL and use it to create log files in a specific directory.
|
|
103
103
|
* @param logLevel - Specify the logging level for the logger instance. e.g., 'error', 'warn', 'info', 'debug'.
|
|
104
|
+
* @param enableFileLogs - Whether to write logs to files. Defaults to the value of the `ENABLE_FILE_LOGS` environment variable.
|
|
104
105
|
* @returns {underpostLogger} The `loggerFactory` function returns a logger instance created using Winston logger
|
|
105
106
|
* library. The logger instance is configured with various transports for printing out messages to
|
|
106
107
|
* different destinations such as the terminal, error.log file, and all.log file. The logger instance
|
|
107
108
|
* also has a method `setUpInfo` attached to it for setting up additional information.
|
|
108
109
|
* @memberof Logger
|
|
109
110
|
*/
|
|
110
|
-
const loggerFactory = (
|
|
111
|
+
const loggerFactory = (
|
|
112
|
+
meta = { url: '' },
|
|
113
|
+
logLevel = '',
|
|
114
|
+
enableFileLogs = process.env.ENABLE_FILE_LOGS === 'true' || process.env.ENABLE_FILE_LOGS === true,
|
|
115
|
+
) => {
|
|
111
116
|
meta = meta.url.split('/').pop();
|
|
112
117
|
// Define which transports the logger must use to print out messages.
|
|
113
118
|
// In this example, we are using three different transports
|
|
114
119
|
const transports = [
|
|
115
120
|
// Allow the use the terminal to print the messages
|
|
116
121
|
new winston.transports.Console(),
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
// Optionally write log files when enableFileLogs is true
|
|
123
|
+
...(enableFileLogs
|
|
124
|
+
? [
|
|
125
|
+
// Allow to print all the error level messages inside the error.log file
|
|
126
|
+
new winston.transports.File({
|
|
127
|
+
filename: `logs/${meta}/error.log`,
|
|
128
|
+
level: 'error',
|
|
129
|
+
}),
|
|
130
|
+
// Allow to print all the error messages inside the all.log file
|
|
131
|
+
// (also includes error logs that are also printed inside error.log)
|
|
132
|
+
new winston.transports.File({ filename: `logs/${meta}/all.log` }),
|
|
133
|
+
]
|
|
134
|
+
: []),
|
|
125
135
|
];
|
|
126
136
|
|
|
127
137
|
// Create the logger instance that has to be exported
|
|
@@ -154,6 +164,7 @@ const loggerFactory = (meta = { url: '' }, logLevel = '') => {
|
|
|
154
164
|
* @param {Object} meta - An object containing metadata, such as the URL, to be used in the logger.
|
|
155
165
|
* @param {string} logLevel - The logging level to be used for the logger (e.g., 'error', 'warn', 'info', 'debug').
|
|
156
166
|
* @param {Function} skip - A function to determine whether to skip logging for a particular request.
|
|
167
|
+
* @param {boolean} enableFileLogs - Whether to write logs to files. Defaults to false.
|
|
157
168
|
* @returns {Function} A middleware function that can be used in an Express application to log HTTP requests.
|
|
158
169
|
* @memberof Logger
|
|
159
170
|
*/
|
|
@@ -161,10 +172,11 @@ const loggerMiddleware = (
|
|
|
161
172
|
meta = { url: '' },
|
|
162
173
|
logLevel = 'info',
|
|
163
174
|
skip = (req, res) => process.env.NODE_ENV === 'production',
|
|
175
|
+
enableFileLogs = process.env.ENABLE_FILE_LOGS === 'true' || process.env.ENABLE_FILE_LOGS === true,
|
|
164
176
|
) => {
|
|
165
177
|
const stream = {
|
|
166
178
|
// Use the http severity
|
|
167
|
-
write: (message) => loggerFactory(meta, logLevel).http(message),
|
|
179
|
+
write: (message) => loggerFactory(meta, logLevel, enableFileLogs).http(message),
|
|
168
180
|
};
|
|
169
181
|
|
|
170
182
|
morgan.token('host', function (req, res) {
|