underpost 2.90.1 → 2.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -798,7 +798,6 @@ const generateRandomPasswordSelection = (length) => {
798
798
  ',',
799
799
  '.',
800
800
  '|',
801
- '\\',
802
801
  ];
803
802
  const numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
804
803
 
@@ -102,8 +102,8 @@ const Input = {
102
102
  </div>
103
103
  `
104
104
  : options?.placeholderIcon
105
- ? html` <div class="fl input-row-${id}">${options.placeholderIcon} ${inputElement}</div> `
106
- : inputElement}
105
+ ? html` <div class="fl input-row-${id}">${options.placeholderIcon} ${inputElement}</div> `
106
+ : inputElement}
107
107
  <div class="in input-info input-info-${id}">&nbsp</div>
108
108
  </div>
109
109
  </div>`;
@@ -189,7 +189,7 @@ const Input = {
189
189
 
190
190
  switch (inputData.inputType) {
191
191
  case 'file':
192
- if (fileObj[inputData.model] && s(`.${inputData.id}`)) {
192
+ if (fileObj && fileObj[inputData.model] && s(`.${inputData.id}`)) {
193
193
  const dataTransfer = new DataTransfer();
194
194
 
195
195
  if (fileObj[inputData.model].fileBlob)
@@ -207,7 +207,9 @@ const Input = {
207
207
  continue;
208
208
  break;
209
209
  case 'md':
210
- RichText.Tokens[inputData.id].easyMDE.value(fileObj[inputData.model].mdPlain);
210
+ if (fileObj && fileObj[inputData.model] && fileObj[inputData.model].mdPlain) {
211
+ RichText.Tokens[inputData.id].easyMDE.value(fileObj[inputData.model].mdPlain);
212
+ }
211
213
  continue;
212
214
  break;
213
215
 
@@ -54,19 +54,18 @@ const Modal = {
54
54
  disableBoxShadow: false,
55
55
  },
56
56
  ) {
57
- options.heightBottomBar = 50;
58
- options.heightTopBar = 50;
59
- let originHeightBottomBar = options.heightBottomBar ? newInstance(options.heightBottomBar) : 0;
60
- let originHeightTopBar = options.heightTopBar ? newInstance(options.heightTopBar) : 0;
57
+ const originHeightBottomBar = 50;
58
+ const originHeightTopBar = 50;
59
+ options.heightBottomBar = 0;
60
+ options.heightTopBar = 100;
61
+ if (options && options.barMode && options.barMode === 'top-bottom-bar') {
62
+ options.heightTopBar = 50;
63
+ options.heightBottomBar = 50;
64
+ }
61
65
  let width = 300;
62
66
  let height = 400;
63
67
  let top = options.style?.top ? options.style.top : 0;
64
68
  let left = options.style?.left ? options.style.left : 0;
65
- const topBottomBarEnable = options && options.barMode && options.barMode === 'top-bottom-bar';
66
- if (!topBottomBarEnable) {
67
- options.heightTopBar = options.heightTopBar + options.heightBottomBar;
68
- options.heightBottomBar = 0;
69
- }
70
69
  let transition = `opacity 0.3s, box-shadow 0.3s, bottom 0.3s`;
71
70
  const originSlideMenuWidth = 320;
72
71
  const collapseSlideMenuWidth = 50;
@@ -828,8 +827,7 @@ const Modal = {
828
827
  },
829
828
  dragDisabled: true,
830
829
  maximize: true,
831
- heightBottomBar: 0,
832
- heightTopBar: options.heightTopBar,
830
+ barMode: options.barMode,
833
831
  });
834
832
 
835
833
  // Bind hover/focus and click-outside to dismiss
@@ -997,8 +995,6 @@ const Modal = {
997
995
  dragDisabled: true,
998
996
  maximize: true,
999
997
  slideMenu: 'modal-menu',
1000
- heightTopBar: originHeightTopBar,
1001
- heightBottomBar: originHeightBottomBar,
1002
998
  barMode: options.barMode,
1003
999
  observer: true,
1004
1000
  disableBoxShadow: true,
@@ -1150,7 +1146,7 @@ const Modal = {
1150
1146
  dragDisabled: true,
1151
1147
  disableCenter: true,
1152
1148
  // maximize: true,
1153
- // barMode: options.barMode,
1149
+ barMode: options.barMode,
1154
1150
  });
1155
1151
  Responsive.Event[`view-${id}`] = () => {
1156
1152
  if (!this.Data[id] || !s(`.${id}`)) return delete Responsive.Event[`view-${id}`];
@@ -1259,8 +1255,6 @@ const Modal = {
1259
1255
  },
1260
1256
  dragDisabled: true,
1261
1257
  maximize: true,
1262
- heightBottomBar: 0,
1263
- heightTopBar: originHeightTopBar,
1264
1258
  barMode: options.barMode,
1265
1259
  });
1266
1260
 
@@ -1319,8 +1313,6 @@ const Modal = {
1319
1313
  },
1320
1314
  dragDisabled: true,
1321
1315
  maximize: true,
1322
- heightTopBar: originHeightTopBar,
1323
- heightBottomBar: originHeightBottomBar,
1324
1316
  barMode: options.barMode,
1325
1317
  });
1326
1318
 
@@ -2489,6 +2481,9 @@ const subMenuHandler = (routes, route) => {
2489
2481
  }
2490
2482
  setTimeout(() => {
2491
2483
  let cid = getQueryParams().cid;
2484
+ if (cid && cid.includes(',')) {
2485
+ cid = cid.split(',')[0];
2486
+ }
2492
2487
  if (s(`.main-sub-btn-active`)) s(`.main-sub-btn-active`).classList.remove('main-sub-btn-active');
2493
2488
  if (cid && s(`.btn-${route}-${cid}`)) {
2494
2489
  s(`.btn-${route}-${cid}`).classList.add('main-sub-btn-active');
@@ -108,12 +108,32 @@ const Panel = {
108
108
  openPanelForm();
109
109
  // s(`.btn-${idPanel}-add`).click();
110
110
  s(`.${scrollClassContainer}`).scrollTop = 0;
111
- Input.setValues(
112
- formData,
113
- obj,
114
- options.originData().find((d) => d._id === obj._id || d.id === obj.id),
115
- options.filesData().find((d) => d._id === obj._id || d.id === obj.id),
116
- );
111
+
112
+ const originData = options.originData();
113
+ const filesData = options.filesData();
114
+
115
+ // Convert IDs to strings for comparison to handle ObjectId vs string issues
116
+ const searchId = String(obj._id || obj.id);
117
+ const foundOrigin = originData.find((d) => String(d._id || d.id) === searchId);
118
+ const foundFiles = filesData.find((d) => String(d._id || d.id) === searchId);
119
+
120
+ if (!foundOrigin) {
121
+ logger.error('Could not find origin data for ID:', searchId);
122
+ logger.error(
123
+ 'Available originData IDs:',
124
+ originData.map((d) => String(d._id || d.id)),
125
+ );
126
+ }
127
+
128
+ if (!foundFiles) {
129
+ logger.error('Could not find files data for ID:', searchId);
130
+ logger.error(
131
+ 'Available filesData IDs:',
132
+ filesData.map((d) => String(d._id || d.id)),
133
+ );
134
+ }
135
+
136
+ Input.setValues(formData, obj, foundOrigin, foundFiles);
117
137
  if (options.on.initEdit) await options.on.initEdit({ data: obj });
118
138
  });
119
139
  s(`.a-${payload._id}`).onclick = async (e) => {
@@ -230,9 +230,10 @@ const PanelForm = {
230
230
  let message = '';
231
231
  let status = 'success';
232
232
  let indexFormDoc = -1;
233
- const filesData = data.fileId ? data.fileId : [null];
234
233
 
235
- for (const file of filesData) {
234
+ const inputFiles = data.fileId ? data.fileId : [null];
235
+
236
+ for (const file of inputFiles) {
236
237
  indexFormDoc++;
237
238
  let fileId;
238
239
 
@@ -247,8 +248,17 @@ const PanelForm = {
247
248
  status,
248
249
  });
249
250
  if (status === 'success') {
250
- mdFileId = data[0]._id;
251
- if (data[1]) fileId = data[1]._id;
251
+ // Identify files by comparing filename instead of just mimetype
252
+ // This handles the case where an .md file is uploaded as the optional file
253
+ // - mdFileId: matches the generated mdFileName from the title
254
+ // - fileId: any other file (including other .md files)
255
+ for (const uploadedFile of data) {
256
+ if (uploadedFile.name === mdFileName) {
257
+ mdFileId = uploadedFile._id;
258
+ } else {
259
+ fileId = uploadedFile._id;
260
+ }
261
+ }
252
262
  }
253
263
  })();
254
264
  const body = {
@@ -329,8 +339,12 @@ const PanelForm = {
329
339
 
330
340
  const getPanelData = async (isLoadMore = false) => {
331
341
  const panelData = PanelForm.Data[idPanel];
342
+ logger.warn('getPanelData called, isLoadMore:', isLoadMore);
332
343
  try {
333
- if (panelData.loading || !panelData.hasMore) return;
344
+ if (panelData.loading || !panelData.hasMore) {
345
+ logger.warn('getPanelData early return - loading:', panelData.loading, 'hasMore:', panelData.hasMore);
346
+ return;
347
+ }
334
348
  panelData.loading = true;
335
349
 
336
350
  if (!isLoadMore) {
@@ -364,45 +378,51 @@ const PanelForm = {
364
378
  let mdBlob, fileBlob;
365
379
  let mdPlain, filePlain;
366
380
 
367
- {
368
- const {
369
- data: [file],
370
- } = await FileService.get({ id: documentObject.mdFileId });
371
-
372
- // const ext = file.name.split('.')[file.name.split('.').length - 1];
373
- mdBlob = file;
374
- mdPlain = await getRawContentFile(getBlobFromUint8ArrayFile(file.data.data, file.mimetype));
375
- mdFileId = newInstance(mdPlain);
376
- }
377
- if (documentObject.fileId) {
378
- const {
379
- data: [file],
380
- } = await FileService.get({ id: documentObject.fileId._id });
381
+ try {
382
+ {
383
+ const {
384
+ data: [file],
385
+ } = await FileService.get({ id: documentObject.mdFileId._id });
381
386
 
382
- // const ext = file.name.split('.')[file.name.split('.').length - 1];
383
- fileBlob = file;
384
- filePlain = undefined;
385
- fileId = getSrcFromFileData(file);
386
- }
387
+ // const ext = file.name.split('.')[file.name.split('.').length - 1];
388
+ mdBlob = file;
389
+ mdPlain = await getRawContentFile(getBlobFromUint8ArrayFile(file.data.data, file.mimetype));
390
+ mdFileId = newInstance(mdPlain);
391
+ }
392
+ if (documentObject.fileId) {
393
+ const {
394
+ data: [file],
395
+ } = await FileService.get({ id: documentObject.fileId._id });
396
+
397
+ // const ext = file.name.split('.')[file.name.split('.').length - 1];
398
+ fileBlob = file;
399
+ filePlain = undefined;
400
+ fileId = getSrcFromFileData(file);
401
+ }
387
402
 
388
- panelData.filesData.push({
389
- id: documentObject._id,
390
- _id: documentObject._id,
391
- mdFileId: { mdBlob, mdPlain },
392
- fileId: { fileBlob, filePlain },
393
- });
403
+ panelData.filesData.push({
404
+ id: documentObject._id,
405
+ _id: documentObject._id,
406
+ mdFileId: { mdBlob, mdPlain },
407
+ fileId: { fileBlob, filePlain },
408
+ });
394
409
 
395
- panelData.data.push({
396
- id: documentObject._id,
397
- title: documentObject.title,
398
- createdAt: documentObject.createdAt,
399
- tags: documentObject.tags.filter((t) => !prefixTags.includes(t)),
400
- mdFileId: marked.parse(mdFileId),
401
- userId: documentObject.userId._id,
402
- fileId,
403
- tools: Elements.Data.user.main.model.user._id === documentObject.userId._id,
404
- _id: documentObject._id,
405
- });
410
+ panelData.data.push({
411
+ id: documentObject._id,
412
+ title: documentObject.title,
413
+ createdAt: documentObject.createdAt,
414
+ tags: documentObject.tags.filter((t) => !prefixTags.includes(t)),
415
+ mdFileId: marked.parse(mdFileId),
416
+ userId: documentObject.userId._id,
417
+ fileId,
418
+ tools: Elements.Data.user.main.model.user._id === documentObject.userId._id,
419
+ _id: documentObject._id,
420
+ });
421
+ } catch (fileError) {
422
+ logger.error('Error fetching files for document:', documentObject._id, fileError);
423
+ // Still add the document to originData even if file fetching fails
424
+ // but skip adding to data and filesData arrays
425
+ }
406
426
  }
407
427
 
408
428
  panelData.skip += result.data.data.length;
@@ -497,7 +517,11 @@ const PanelForm = {
497
517
  JSON.stringify(Elements.Data.user.main.model.user, null, 4),
498
518
  );
499
519
 
500
- if (loadingGetData || (lastCid === cid && !forceUpdate)) return;
520
+ // Normalize empty values for comparison (undefined, null, '' should all be treated as empty)
521
+ const normalizedCid = cid || '';
522
+ const normalizedLastCid = lastCid || '';
523
+
524
+ if (loadingGetData || (normalizedLastCid === normalizedCid && !forceUpdate)) return;
501
525
  loadingGetData = true;
502
526
  lastUserId = newInstance(Elements.Data.user.main.model.user._id);
503
527
  lastCid = cid;
@@ -579,16 +603,7 @@ const PanelForm = {
579
603
  id: options.parentIdModal ? 'html-' + options.parentIdModal : 'main-body',
580
604
  routeId: options.route,
581
605
  event: async (path) => {
582
- PanelForm.Data[idPanel] = {
583
- ...PanelForm.Data[idPanel],
584
- originData: [],
585
- data: [],
586
- filesData: [],
587
- // skip: 0,
588
- limit: 3, // Load 5 items per page
589
- hasMore: true,
590
- loading: false,
591
- };
606
+ // Don't manually clear arrays - updatePanel() will handle it if needed
592
607
  await PanelForm.Data[idPanel].updatePanel();
593
608
  },
594
609
  });
@@ -31,7 +31,11 @@ class MongooseDBService {
31
31
  logger.info('MongooseDB connect', { host, name, uri });
32
32
  return await mongoose
33
33
  .createConnection(uri, {
34
- // Options like useNewUrlParser and useUnifiedTopology are often set here.
34
+ serverSelectionTimeoutMS: 5000,
35
+ // readPreference: 'primary',
36
+ // directConnection: true,
37
+ // useNewUrlParser: true,
38
+ // useUnifiedTopology: true,
35
39
  })
36
40
  .asPromise();
37
41
  }
package/src/index.js CHANGED
@@ -36,7 +36,7 @@ class Underpost {
36
36
  * @type {String}
37
37
  * @memberof Underpost
38
38
  */
39
- static version = 'v2.90.1';
39
+ static version = 'v2.92.0';
40
40
  /**
41
41
  * Repository cli API
42
42
  * @static
package/src/server/dns.js CHANGED
@@ -100,6 +100,160 @@ class Dns {
100
100
  return ipv4.address;
101
101
  }
102
102
 
103
+ /**
104
+ * Setup nftables tables and chains if they don't exist.
105
+ * @static
106
+ * @memberof DnsManager
107
+ */
108
+ static setupNftables() {
109
+ shellExec(`sudo nft add table inet filter 2>/dev/null || true`, { silent: true });
110
+ shellExec(
111
+ `sudo nft add chain inet filter input '{ type filter hook input priority 0; policy accept; }' 2>/dev/null || true`,
112
+ { silent: true },
113
+ );
114
+ shellExec(
115
+ `sudo nft add chain inet filter output '{ type filter hook output priority 0; policy accept; }' 2>/dev/null || true`,
116
+ { silent: true },
117
+ );
118
+ shellExec(
119
+ `sudo nft add chain inet filter forward '{ type filter hook forward priority 0; policy accept; }' 2>/dev/null || true`,
120
+ { silent: true },
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Bans an IP address from ingress traffic.
126
+ * @static
127
+ * @memberof DnsManager
128
+ * @param {string} ip - The IP address to ban.
129
+ */
130
+ static banIngress(ip) {
131
+ Dns.setupNftables();
132
+ if (!validator.isIP(ip)) {
133
+ logger.error(`Invalid IP address: ${ip}`);
134
+ return;
135
+ }
136
+ shellExec(`sudo nft add rule inet filter input ip saddr ${ip} counter drop`, { silent: true });
137
+ logger.info(`Banned ingress for IP: ${ip}`);
138
+ }
139
+
140
+ /**
141
+ * Bans an IP address from egress traffic.
142
+ * @static
143
+ * @memberof DnsManager
144
+ * @param {string} ip - The IP address to ban.
145
+ */
146
+ static banEgress(ip) {
147
+ Dns.setupNftables();
148
+ if (!validator.isIP(ip)) {
149
+ logger.error(`Invalid IP address: ${ip}`);
150
+ return;
151
+ }
152
+ shellExec(`sudo nft add rule inet filter output ip daddr ${ip} counter drop`, { silent: true });
153
+ shellExec(`sudo nft add rule inet filter forward ip daddr ${ip} counter drop`, { silent: true });
154
+ logger.info(`Banned egress for IP: ${ip}`);
155
+ }
156
+
157
+ /**
158
+ * Helper to get nftables rule handles for a specific IP and chain.
159
+ * @static
160
+ * @memberof DnsManager
161
+ * @param {string} chain - The chain name (input, output, forward).
162
+ * @param {string} ip - The IP address.
163
+ * @param {string} type - The type (saddr or daddr).
164
+ * @returns {string[]} Array of handles.
165
+ */
166
+ static getNftHandles(chain, ip, type) {
167
+ const output = shellExec(`sudo nft -a list chain inet filter ${chain}`, { stdout: true, silent: true });
168
+ const lines = output.split('\n');
169
+ const handles = [];
170
+ // Regex to match IP and handle. Note: output format depends on nft version but usually contains "handle <id>" at end.
171
+ // Example: ip saddr 1.2.3.4 counter packets 0 bytes 0 drop # handle 5
172
+ const regex = new RegExp(`ip ${type} ${ip} .* handle (\\d+)`);
173
+ for (const line of lines) {
174
+ const match = line.match(regex);
175
+ if (match) {
176
+ handles.push(match[1]);
177
+ }
178
+ }
179
+ return handles;
180
+ }
181
+
182
+ /**
183
+ * Unbans an IP address from ingress traffic.
184
+ * @static
185
+ * @memberof DnsManager
186
+ * @param {string} ip - The IP address to unban.
187
+ */
188
+ static unbanIngress(ip) {
189
+ const handles = Dns.getNftHandles('input', ip, 'saddr');
190
+ for (const handle of handles) {
191
+ shellExec(`sudo nft delete rule inet filter input handle ${handle}`, { silent: true });
192
+ }
193
+ logger.info(`Unbanned ingress for IP: ${ip}`);
194
+ }
195
+
196
+ /**
197
+ * Unbans an IP address from egress traffic.
198
+ * @static
199
+ * @memberof DnsManager
200
+ * @param {string} ip - The IP address to unban.
201
+ */
202
+ static unbanEgress(ip) {
203
+ const outputHandles = Dns.getNftHandles('output', ip, 'daddr');
204
+ for (const handle of outputHandles) {
205
+ shellExec(`sudo nft delete rule inet filter output handle ${handle}`, { silent: true });
206
+ }
207
+ const forwardHandles = Dns.getNftHandles('forward', ip, 'daddr');
208
+ for (const handle of forwardHandles) {
209
+ shellExec(`sudo nft delete rule inet filter forward handle ${handle}`, { silent: true });
210
+ }
211
+ logger.info(`Unbanned egress for IP: ${ip}`);
212
+ }
213
+
214
+ /**
215
+ * Lists all banned ingress IPs.
216
+ * @static
217
+ * @memberof DnsManager
218
+ */
219
+ static listBannedIngress() {
220
+ const output = shellExec(`sudo nft list chain inet filter input`, { stdout: true, silent: true });
221
+ console.log(output);
222
+ }
223
+
224
+ /**
225
+ * Lists all banned egress IPs.
226
+ * @static
227
+ * @memberof DnsManager
228
+ */
229
+ static listBannedEgress() {
230
+ console.log('--- Output Chain ---');
231
+ console.log(shellExec(`sudo nft list chain inet filter output`, { stdout: true, silent: true }));
232
+ console.log('--- Forward Chain ---');
233
+ console.log(shellExec(`sudo nft list chain inet filter forward`, { stdout: true, silent: true }));
234
+ }
235
+
236
+ /**
237
+ * Clears all banned ingress IPs.
238
+ * @static
239
+ * @memberof DnsManager
240
+ */
241
+ static clearBannedIngress() {
242
+ shellExec(`sudo nft flush chain inet filter input`, { silent: true });
243
+ logger.info('Cleared all ingress bans.');
244
+ }
245
+
246
+ /**
247
+ * Clears all banned egress IPs.
248
+ * @static
249
+ * @memberof DnsManager
250
+ */
251
+ static clearBannedEgress() {
252
+ shellExec(`sudo nft flush chain inet filter output`, { silent: true });
253
+ shellExec(`sudo nft flush chain inet filter forward`, { silent: true });
254
+ logger.info('Cleared all egress bans.');
255
+ }
256
+
103
257
  /**
104
258
  * Performs the dynamic DNS update logic.
105
259
  * It checks if the public IP has changed and, if so, updates the configured DNS records.
@@ -119,7 +119,9 @@ class UnderpostStartUp {
119
119
  * @param {boolean} options.run - Whether to run the deployment.
120
120
  */
121
121
  async callback(deployId = 'dd-default', env = 'development', options = { build: false, run: false }) {
122
+ UnderpostRootEnv.API.set('container-status', `${deployId}-${env}-build-deployment`);
122
123
  if (options.build === true) await UnderpostStartUp.API.build(deployId, env);
124
+ UnderpostRootEnv.API.set('container-status', `${deployId}-${env}-initializing-deployment`);
123
125
  if (options.run === true) await UnderpostStartUp.API.run(deployId, env);
124
126
  },
125
127
  /**