cyberia 2.99.8 → 3.0.2
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/engine-cyberia.cd.yml +1 -0
- package/.github/workflows/gitlab.ci.yml +20 -0
- package/.github/workflows/publish.ci.yml +18 -38
- package/.github/workflows/publish.cyberia.ci.yml +18 -38
- package/.vscode/extensions.json +8 -50
- package/.vscode/settings.json +0 -77
- package/CHANGELOG.md +171 -1
- package/{cli.md → CLI-HELP.md} +49 -44
- package/README.md +139 -0
- package/bin/build.js +7 -15
- package/bin/cyberia.js +385 -71
- package/bin/deploy.js +14 -151
- package/bin/file.js +13 -8
- package/bin/index.js +385 -71
- package/bin/zed.js +63 -2
- package/conf.js +32 -3
- package/deployment.yaml +2 -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 +64 -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 +15 -11
- 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/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +2 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +93 -2
- package/src/api/file/file.controller.js +3 -13
- package/src/api/file/file.ref.json +0 -21
- package/src/api/ipfs/ipfs.controller.js +104 -0
- package/src/api/ipfs/ipfs.model.js +71 -0
- package/src/api/ipfs/ipfs.router.js +31 -0
- package/src/api/ipfs/ipfs.service.js +193 -0
- package/src/api/object-layer/README.md +139 -0
- package/src/api/object-layer/object-layer.controller.js +3 -0
- package/src/api/object-layer/object-layer.model.js +15 -1
- package/src/api/object-layer/object-layer.router.js +6 -10
- package/src/api/object-layer/object-layer.service.js +311 -182
- package/src/api/user/user.router.js +0 -47
- package/src/cli/baremetal.js +7 -9
- package/src/cli/cluster.js +95 -152
- package/src/cli/deploy.js +8 -5
- package/src/cli/index.js +31 -31
- 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 +17 -2
- package/src/client/components/core/Docs.js +92 -6
- package/src/client/components/core/LoadingAnimation.js +2 -3
- package/src/client/components/core/Modal.js +1 -1
- package/src/client/components/core/VanillaJs.js +36 -25
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +4 -5
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +280 -29
- package/src/client/services/ipfs/ipfs.service.js +144 -0
- package/src/client/services/object-layer/object-layer.management.js +161 -8
- 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 +4 -3
- package/src/server/auth.js +18 -18
- package/src/server/client-build-docs.js +178 -41
- package/src/server/conf.js +1 -1
- package/src/server/ipfs-client.js +433 -0
- package/src/server/logger.js +22 -10
- package/src/server/object-layer.js +649 -18
- package/src/server/semantic-layer-generator.js +1083 -0
- package/src/server/shape-generator.js +952 -0
- package/test/shape-generator.test.js +457 -0
- package/.vscode/zed.keymap.json +0 -39
- package/.vscode/zed.settings.json +0 -20
- package/bin/ssl.js +0 -63
- package/manifests/lxd/underpost-setup.sh +0 -163
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { DefaultManagement } from '../default/default.management.js';
|
|
2
2
|
import { ObjectLayerService } from './object-layer.service.js';
|
|
3
|
-
import { commonUserGuard } from '../../components/core/CommonJs.js';
|
|
3
|
+
import { commonUserGuard, commonModeratorGuard } from '../../components/core/CommonJs.js';
|
|
4
4
|
import { getProxyPath, setPath, setQueryParams } from '../../components/core/Router.js';
|
|
5
5
|
import { ObjectLayerEngineModal } from '../../components/cyberia/ObjectLayerEngineModal.js';
|
|
6
6
|
import { ObjectLayerEngineViewer } from '../../components/cyberia/ObjectLayerEngineViewer.js';
|
|
7
7
|
import { s } from '../../components/core/VanillaJs.js';
|
|
8
8
|
import { Modal } from '../../components/core/Modal.js';
|
|
9
9
|
import { BtnIcon } from '../../components/core/BtnIcon.js';
|
|
10
|
+
import { NotificationManager } from '../../components/core/NotificationManager.js';
|
|
11
|
+
import { AgGrid } from '../../components/core/AgGrid.js';
|
|
10
12
|
|
|
11
13
|
const ObjectLayerManagement = {
|
|
12
|
-
RenderTable: async ({ Elements, idModal }) => {
|
|
14
|
+
RenderTable: async ({ Elements, idModal: rawIdModal }) => {
|
|
15
|
+
const idModal = rawIdModal || 'modal-object-layer-engine-management';
|
|
16
|
+
const serviceId = 'object-layer-engine-management';
|
|
17
|
+
const gridId = `${serviceId}-grid-${idModal}`;
|
|
13
18
|
const user = Elements.Data.user.main.model.user;
|
|
14
19
|
const { role } = user;
|
|
15
20
|
|
|
@@ -41,9 +46,9 @@ const ObjectLayerManagement = {
|
|
|
41
46
|
// Navigate to viewer route first
|
|
42
47
|
setPath(`${getProxyPath()}object-layer-engine-viewer`);
|
|
43
48
|
// Then add query param without replacing history
|
|
44
|
-
setQueryParams({
|
|
49
|
+
setQueryParams({ id: data._id }, { replace: true });
|
|
45
50
|
if (s(`.modal-object-layer-engine-viewer`)) {
|
|
46
|
-
await ObjectLayerEngineViewer.Reload({ Elements });
|
|
51
|
+
await ObjectLayerEngineViewer.Reload({ Elements, force: true });
|
|
47
52
|
}
|
|
48
53
|
s(`.main-btn-object-layer-engine-viewer`).click();
|
|
49
54
|
});
|
|
@@ -87,7 +92,7 @@ const ObjectLayerManagement = {
|
|
|
87
92
|
// Navigate to editor route first
|
|
88
93
|
setPath(`${getProxyPath()}object-layer-engine`);
|
|
89
94
|
// Then add query param without replacing history
|
|
90
|
-
setQueryParams({
|
|
95
|
+
setQueryParams({ id: data._id }, { replace: true });
|
|
91
96
|
if (s(`.modal-object-layer-engine`)) await ObjectLayerEngineModal.Reload();
|
|
92
97
|
else s(`.main-btn-object-layer-engine`).click();
|
|
93
98
|
});
|
|
@@ -148,6 +153,123 @@ const ObjectLayerManagement = {
|
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
// Custom renderer for delete button (moderator+ only)
|
|
157
|
+
const canDelete = commonModeratorGuard(role);
|
|
158
|
+
|
|
159
|
+
class DeleteButtonRenderer {
|
|
160
|
+
eGui;
|
|
161
|
+
|
|
162
|
+
async init(params) {
|
|
163
|
+
this.eGui = document.createElement('div');
|
|
164
|
+
const { data } = params;
|
|
165
|
+
|
|
166
|
+
if (!data || !data._id || !canDelete) {
|
|
167
|
+
this.eGui.innerHTML = '';
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.eGui.innerHTML = html` ${await BtnIcon.Render({
|
|
172
|
+
label: html`<div class="abs center">
|
|
173
|
+
<i class="fas fa-trash" style="color: #dc3545;"></i>
|
|
174
|
+
</div> `,
|
|
175
|
+
class: `in fll section-mp management-table-btn-mini btn-delete-object-layer-${data._id}`,
|
|
176
|
+
})}`;
|
|
177
|
+
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
const btn = this.eGui.querySelector(`.btn-delete-object-layer-${data._id}`);
|
|
180
|
+
if (btn)
|
|
181
|
+
btn.onclick = async () => {
|
|
182
|
+
const itemId = data?.data?.item?.id || data._id;
|
|
183
|
+
const confirmResult = await Modal.RenderConfirm({
|
|
184
|
+
id: `delete-object-layer-${data._id}`,
|
|
185
|
+
html: async () => html`
|
|
186
|
+
<div class="in section-mp" style="text-align: center">
|
|
187
|
+
<p>Are you sure you want to permanently delete object layer <strong>"${itemId}"</strong>?</p>
|
|
188
|
+
<p style="color: #dc3545; font-size: 13px; margin-top: 8px;">
|
|
189
|
+
This will remove all associated data including render frames, atlas sprite sheet, IPFS pins, and
|
|
190
|
+
static asset files.
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
`,
|
|
194
|
+
});
|
|
195
|
+
if (confirmResult.status !== 'confirm') return;
|
|
196
|
+
try {
|
|
197
|
+
const result = await ObjectLayerService.delete({ id: data._id });
|
|
198
|
+
if (result.status === 'success') {
|
|
199
|
+
NotificationManager.Push({
|
|
200
|
+
html: `Object layer "${itemId}" deleted successfully`,
|
|
201
|
+
status: 'success',
|
|
202
|
+
});
|
|
203
|
+
if (AgGrid.grids[gridId]) {
|
|
204
|
+
AgGrid.grids[gridId].applyTransaction({ remove: [data] });
|
|
205
|
+
}
|
|
206
|
+
const token = DefaultManagement.Tokens[idModal];
|
|
207
|
+
if (token) {
|
|
208
|
+
const newTotal = token.total - 1;
|
|
209
|
+
const newTotalPages = Math.ceil(newTotal / token.limit);
|
|
210
|
+
if (token.page > newTotalPages && newTotalPages > 0) {
|
|
211
|
+
token.page = newTotalPages;
|
|
212
|
+
}
|
|
213
|
+
await DefaultManagement.loadTable(idModal, { reload: false });
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
throw new Error(result.message || 'Failed to delete object layer');
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
NotificationManager.Push({
|
|
220
|
+
html: `Failed to delete: ${error.message}`,
|
|
221
|
+
status: 'error',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getGui() {
|
|
229
|
+
return this.eGui;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
refresh(params) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const createCidRenderer = (cidAccessor) => {
|
|
238
|
+
return class {
|
|
239
|
+
eGui;
|
|
240
|
+
|
|
241
|
+
async init(params) {
|
|
242
|
+
this.eGui = document.createElement('div');
|
|
243
|
+
const { data } = params;
|
|
244
|
+
const cid = cidAccessor(data) || '';
|
|
245
|
+
|
|
246
|
+
if (!cid) {
|
|
247
|
+
this.eGui.innerHTML = html`<span style="color: #666; font-style: italic;">—</span>`;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.eGui.innerHTML = html`<span
|
|
252
|
+
title="${cid}"
|
|
253
|
+
style="font-family: monospace; font-size: 11px; cursor: default; user-select: all;"
|
|
254
|
+
>${cid}</span
|
|
255
|
+
>`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getGui() {
|
|
259
|
+
return this.eGui;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
refresh(params) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// IPFS CID of object layer data JSON (fast-json-stable-stringify)
|
|
269
|
+
const CidRenderer = createCidRenderer((d) => d?.cid);
|
|
270
|
+
// IPFS CID of the consolidated atlas sprite sheet PNG
|
|
271
|
+
const AtlasCidRenderer = createCidRenderer((d) => d?.data?.atlasSpriteSheetCid || d?.atlasSpriteSheetId?.cid);
|
|
272
|
+
|
|
151
273
|
let columnDefs = [
|
|
152
274
|
// {
|
|
153
275
|
// field: '_id',
|
|
@@ -163,6 +285,24 @@ const ObjectLayerManagement = {
|
|
|
163
285
|
},
|
|
164
286
|
{ field: 'data.item.type', headerName: 'Item Type', editable: role === 'user' },
|
|
165
287
|
{ field: 'data.item.description', headerName: 'Description', flex: 1, editable: role === 'user' },
|
|
288
|
+
{
|
|
289
|
+
field: 'cid',
|
|
290
|
+
headerName: 'IPFS CID',
|
|
291
|
+
width: 160,
|
|
292
|
+
cellRenderer: CidRenderer,
|
|
293
|
+
editable: false,
|
|
294
|
+
sortable: false,
|
|
295
|
+
filter: false,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
field: 'data.atlasSpriteSheetCid',
|
|
299
|
+
headerName: 'Atlas CID',
|
|
300
|
+
width: 160,
|
|
301
|
+
cellRenderer: AtlasCidRenderer,
|
|
302
|
+
editable: false,
|
|
303
|
+
sortable: false,
|
|
304
|
+
filter: false,
|
|
305
|
+
},
|
|
166
306
|
{
|
|
167
307
|
field: 'frame08',
|
|
168
308
|
headerName: 'Frame 08 Preview',
|
|
@@ -190,15 +330,28 @@ const ObjectLayerManagement = {
|
|
|
190
330
|
sortable: false,
|
|
191
331
|
filter: false,
|
|
192
332
|
},
|
|
333
|
+
...(canDelete
|
|
334
|
+
? [
|
|
335
|
+
{
|
|
336
|
+
field: 'delete',
|
|
337
|
+
headerName: '',
|
|
338
|
+
width: 100,
|
|
339
|
+
cellRenderer: DeleteButtonRenderer,
|
|
340
|
+
editable: false,
|
|
341
|
+
sortable: false,
|
|
342
|
+
filter: false,
|
|
343
|
+
},
|
|
344
|
+
]
|
|
345
|
+
: []),
|
|
193
346
|
];
|
|
194
347
|
|
|
195
348
|
return await DefaultManagement.RenderTable({
|
|
196
|
-
idModal
|
|
197
|
-
serviceId
|
|
349
|
+
idModal,
|
|
350
|
+
serviceId,
|
|
198
351
|
entity: 'object-layer',
|
|
199
352
|
permissions: {
|
|
200
353
|
add: commonUserGuard(role),
|
|
201
|
-
remove:
|
|
354
|
+
remove: false,
|
|
202
355
|
reload: commonUserGuard(role),
|
|
203
356
|
},
|
|
204
357
|
customEvent: {
|
|
@@ -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.2';
|
|
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';
|
|
@@ -98,7 +99,7 @@ class ExpressService {
|
|
|
98
99
|
|
|
99
100
|
if (origins && isDevProxyContext())
|
|
100
101
|
origins.push(devProxyHostFactory({ host, includeHttp: true, tls: isTlsDevProxy() }));
|
|
101
|
-
app.set('trust proxy',
|
|
102
|
+
app.set('trust proxy', 1);
|
|
102
103
|
|
|
103
104
|
app.use((req, res, next) => {
|
|
104
105
|
res.on('finish', () => {
|
|
@@ -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
|
package/src/server/auth.js
CHANGED
|
@@ -647,24 +647,24 @@ function applySecurity(app, opts = {}) {
|
|
|
647
647
|
}),
|
|
648
648
|
);
|
|
649
649
|
logger.info('Cors origin', origin);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
650
|
+
if (!process.env.DISABLE_API_RATE_LIMIT) {
|
|
651
|
+
// Rate limiting + slow down
|
|
652
|
+
const limiter = rateLimit({
|
|
653
|
+
windowMs: rate.windowMs,
|
|
654
|
+
max: rate.max,
|
|
655
|
+
standardHeaders: true,
|
|
656
|
+
legacyHeaders: false,
|
|
657
|
+
message: { error: 'Too many requests, please try again later.' },
|
|
658
|
+
});
|
|
659
|
+
app.use(limiter);
|
|
660
|
+
|
|
661
|
+
const speedLimiter = slowDown({
|
|
662
|
+
windowMs: slowdown.windowMs,
|
|
663
|
+
delayAfter: slowdown.delayAfter,
|
|
664
|
+
delayMs: () => slowdown.delayMs,
|
|
665
|
+
});
|
|
666
|
+
app.use(speedLimiter);
|
|
667
|
+
}
|
|
668
668
|
// Cookie parsing
|
|
669
669
|
app.use(cookieParser(process.env.JWT_SECRET));
|
|
670
670
|
}
|
|
@@ -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
|
};
|