underpost 3.2.8 → 3.2.10

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 (92) hide show
  1. package/.github/workflows/npmpkg.ci.yml +1 -0
  2. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  3. package/.github/workflows/release.cd.yml +1 -0
  4. package/.vscode/settings.json +10 -5
  5. package/CHANGELOG.md +223 -2
  6. package/CLI-HELP.md +36 -7
  7. package/README.md +38 -9
  8. package/bin/build.js +27 -11
  9. package/bin/deploy.js +20 -21
  10. package/bin/file.js +32 -13
  11. package/bin/index.js +2 -1
  12. package/bin/vs.js +1 -1
  13. package/bump.config.js +26 -0
  14. package/conf.js +20 -4
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  19. package/manifests/kind-config-dev.yaml +8 -0
  20. package/manifests/mongodb/pv-pvc.yaml +44 -8
  21. package/manifests/mongodb/statefulset.yaml +55 -68
  22. package/package.json +40 -25
  23. package/scripts/k3s-node-setup.sh +30 -11
  24. package/scripts/nat-iptables.sh +103 -18
  25. package/src/api/core/core.router.js +19 -14
  26. package/src/api/core/core.service.js +5 -5
  27. package/src/api/default/default.router.js +22 -18
  28. package/src/api/default/default.service.js +5 -5
  29. package/src/api/document/document.router.js +28 -23
  30. package/src/api/document/document.service.js +100 -23
  31. package/src/api/file/file.router.js +19 -13
  32. package/src/api/file/file.service.js +9 -7
  33. package/src/api/test/test.router.js +17 -12
  34. package/src/api/types.js +24 -0
  35. package/src/api/user/guest.service.js +5 -4
  36. package/src/api/user/user.router.js +297 -288
  37. package/src/api/user/user.service.js +100 -35
  38. package/src/cli/baremetal.js +20 -11
  39. package/src/cli/cluster.js +243 -55
  40. package/src/cli/db.js +106 -62
  41. package/src/cli/deploy.js +297 -154
  42. package/src/cli/fs.js +19 -3
  43. package/src/cli/index.js +37 -9
  44. package/src/cli/ipfs.js +4 -6
  45. package/src/cli/kubectl.js +4 -1
  46. package/src/cli/lxd.js +217 -135
  47. package/src/cli/release.js +289 -131
  48. package/src/cli/repository.js +91 -34
  49. package/src/cli/run.js +297 -56
  50. package/src/cli/test.js +9 -3
  51. package/src/client/Default.index.js +9 -3
  52. package/src/client/components/core/Auth.js +19 -5
  53. package/src/client/components/core/Docs.js +6 -34
  54. package/src/client/components/core/FileExplorer.js +6 -6
  55. package/src/client/components/core/Modal.js +65 -2
  56. package/src/client/components/core/PanelForm.js +56 -52
  57. package/src/client/components/core/Recover.js +4 -4
  58. package/src/client/components/core/Worker.js +170 -350
  59. package/src/client/services/default/default.management.js +20 -25
  60. package/src/client/services/user/guest.service.js +10 -3
  61. package/src/client/sw/core.sw.js +174 -112
  62. package/src/db/DataBaseProvider.js +120 -20
  63. package/src/db/mongo/MongoBootstrap.js +587 -0
  64. package/src/db/mongo/MongooseDB.js +126 -22
  65. package/src/index.js +1 -1
  66. package/src/runtime/express/Express.js +2 -2
  67. package/src/runtime/wp/Wp.js +8 -5
  68. package/src/server/auth.js +2 -2
  69. package/src/server/client-build-docs.js +1 -1
  70. package/src/server/client-build.js +94 -129
  71. package/src/server/conf.js +20 -65
  72. package/src/server/data-query.js +32 -20
  73. package/src/server/dns.js +22 -0
  74. package/src/server/process.js +180 -19
  75. package/src/server/runtime.js +1 -1
  76. package/src/server/start.js +26 -7
  77. package/src/server/valkey.js +9 -2
  78. package/src/ws/IoInterface.js +16 -16
  79. package/src/ws/core/channels/core.ws.chat.js +11 -11
  80. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  81. package/src/ws/core/channels/core.ws.stream.js +19 -19
  82. package/src/ws/core/core.ws.connection.js +8 -8
  83. package/src/ws/core/core.ws.server.js +6 -5
  84. package/src/ws/default/channels/default.ws.main.js +10 -10
  85. package/src/ws/default/default.ws.connection.js +4 -4
  86. package/src/ws/default/default.ws.server.js +4 -3
  87. package/typedoc.json +10 -1
  88. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  89. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  90. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  91. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  92. /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/cli/db.js CHANGED
@@ -10,8 +10,9 @@ import { mergeFile, splitFileFactory, loadConfServerJson, resolveConfSecrets } f
10
10
  import { loggerFactory } from '../server/logger.js';
11
11
  import { shellExec } from '../server/process.js';
12
12
  import fs from 'fs-extra';
13
- import { DataBaseProvider } from '../db/DataBaseProvider.js';
13
+ import { DataBaseProviderService } from '../db/DataBaseProvider.js';
14
14
  import { loadReplicas, pathPortAssignmentFactory, loadCronDeployEnv } from '../server/conf.js';
15
+ import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
15
16
  import Underpost from '../index.js';
16
17
  import { timer } from '../client/components/core/CommonJs.js';
17
18
  const logger = loggerFactory(import.meta);
@@ -229,9 +230,12 @@ class UnderpostDB {
229
230
  * @param {string} params.bsonPath - BSON directory path.
230
231
  * @param {boolean} params.drop - Whether to drop existing database.
231
232
  * @param {boolean} params.preserveUUID - Whether to preserve UUIDs.
233
+ * @param {string} [params.user=''] - MongoDB username for authenticated restore.
234
+ * @param {string} [params.password=''] - MongoDB password for authenticated restore.
235
+ * @param {string} [params.authDatabase='admin'] - Auth database for restore command.
232
236
  * @return {boolean} Success status.
233
237
  */
234
- _importMongoDB({ pod, namespace, dbName, bsonPath, drop, preserveUUID }) {
238
+ _importMongoDB({ pod, namespace, dbName, bsonPath, drop, preserveUUID, user = '', password = '', authDatabase = 'admin' }) {
235
239
  try {
236
240
  const podName = pod.NAME;
237
241
  const containerBsonPath = `/${dbName}`;
@@ -268,9 +272,10 @@ class UnderpostDB {
268
272
  }
269
273
 
270
274
  // Restore database
271
- const restoreCmd = `mongorestore -d ${dbName} ${containerBsonPath}${drop ? ' --drop' : ''}${
272
- preserveUUID ? ' --preserveUUID' : ''
273
- }`;
275
+ const authFlags = user && password
276
+ ? ` --username ${JSON.stringify(user)} --password ${JSON.stringify(password)} --authenticationDatabase ${JSON.stringify(authDatabase)}`
277
+ : '';
278
+ const restoreCmd = `mongorestore -d ${dbName} ${containerBsonPath}${drop ? ' --drop' : ''}${preserveUUID ? ' --preserveUUID' : ''}${authFlags}`;
274
279
  Underpost.kubectl.exec({ podName, namespace, command: restoreCmd });
275
280
 
276
281
  logger.info('Successfully imported MongoDB database', { podName, dbName });
@@ -291,9 +296,12 @@ class UnderpostDB {
291
296
  * @param {string} params.dbName - Database name.
292
297
  * @param {string} params.outputPath - Output directory path.
293
298
  * @param {string} [params.collections=''] - Comma-separated collection list.
299
+ * @param {string} [params.user=''] - MongoDB username for authenticated dump.
300
+ * @param {string} [params.password=''] - MongoDB password for authenticated dump.
301
+ * @param {string} [params.authDatabase='admin'] - Auth database for dump command.
294
302
  * @return {boolean} Success status.
295
303
  */
296
- _exportMongoDB({ pod, namespace, dbName, outputPath, collections = '' }) {
304
+ _exportMongoDB({ pod, namespace, dbName, outputPath, collections = '', user = '', password = '', authDatabase = 'admin' }) {
297
305
  try {
298
306
  const podName = pod.NAME;
299
307
  const containerBsonPath = `/${dbName}`;
@@ -308,14 +316,18 @@ class UnderpostDB {
308
316
  });
309
317
 
310
318
  // Dump database or specific collections
319
+ const authFlags = user && password
320
+ ? ` --username ${JSON.stringify(user)} --password ${JSON.stringify(password)} --authenticationDatabase ${JSON.stringify(authDatabase)}`
321
+ : '';
322
+
311
323
  if (collections) {
312
324
  const collectionList = collections.split(',').map((c) => c.trim());
313
325
  for (const collection of collectionList) {
314
- const dumpCmd = `mongodump -d ${dbName} --collection ${collection} -o /`;
326
+ const dumpCmd = `mongodump -d ${dbName} --collection ${collection} -o /${authFlags}`;
315
327
  Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
316
328
  }
317
329
  } else {
318
- const dumpCmd = `mongodump -d ${dbName} -o /`;
330
+ const dumpCmd = `mongodump -d ${dbName} -o /${authFlags}`;
319
331
  Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
320
332
  }
321
333
 
@@ -347,9 +359,12 @@ class UnderpostDB {
347
359
  * @param {string} params.podName - Pod name.
348
360
  * @param {string} params.namespace - Namespace.
349
361
  * @param {string} params.dbName - Database name.
362
+ * @param {string} [params.user=''] - MongoDB username for authenticated stats query.
363
+ * @param {string} [params.password=''] - MongoDB password for authenticated stats query.
364
+ * @param {string} [params.authDatabase='admin'] - Auth database for stats query.
350
365
  * @return {Object|null} Collection statistics or null on error.
351
366
  */
352
- _getMongoStats({ podName, namespace, dbName }) {
367
+ _getMongoStats({ podName, namespace, dbName, user = '', password = '', authDatabase = 'admin' }) {
353
368
  try {
354
369
  logger.info('Getting MongoDB collection statistics', { podName, dbName });
355
370
 
@@ -357,8 +372,11 @@ class UnderpostDB {
357
372
  const script = `db.getSiblingDB('${dbName}').getCollectionNames().map(function(c) { return { collection: c, count: db.getSiblingDB('${dbName}')[c].countDocuments() }; })`;
358
373
 
359
374
  // Execute the script
360
- const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mongosh --quiet --eval "${script}"`;
361
- const output = shellExec(command, { stdout: true, silent: true });
375
+ const authFlags = user && password
376
+ ? ` --authenticationDatabase ${JSON.stringify(authDatabase)} -u ${JSON.stringify(user)} -p ${JSON.stringify(password)}`
377
+ : '';
378
+ const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mongosh --quiet${authFlags} --eval "${script}"`;
379
+ const output = shellExec(command, { stdout: true, silent: true, silentOnError: true });
362
380
 
363
381
  if (!output || output.trim() === '') {
364
382
  logger.warn('No collections found or empty output');
@@ -415,7 +433,7 @@ class UnderpostDB {
415
433
  logger.info('Getting MariaDB table statistics', { podName, dbName });
416
434
 
417
435
  const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mariadb -u ${user} -p${password} ${dbName} -e "SELECT TABLE_NAME as 'table', TABLE_ROWS as 'count' FROM information_schema.TABLES WHERE TABLE_SCHEMA = '${dbName}' ORDER BY TABLE_NAME;" --skip-column-names --batch`;
418
- const output = shellExec(command, { stdout: true, silent: true, disableLog: true });
436
+ const output = shellExec(command, { stdout: true, silent: true, disableLog: true, silentOnError: true });
419
437
 
420
438
  if (!output || output.trim() === '') {
421
439
  logger.warn('No tables found or empty output');
@@ -475,47 +493,7 @@ class UnderpostDB {
475
493
  console.log('='.repeat(70) + '\n');
476
494
  },
477
495
 
478
- /**
479
- * Gets MongoDB primary pod name from replica set status.
480
- * @method getMongoPrimaryPodName
481
- * @memberof UnderpostDB
482
- * @param {Object} [options={}] - Options for getting primary pod.
483
- * @param {string} [options.namespace='default'] - Kubernetes namespace.
484
- * @param {string} [options.podName='mongodb-0'] - Initial pod name to query replica set status.
485
- * @return {string|null} Primary pod name or null if not found.
486
- */
487
- getMongoPrimaryPodName(options = { namespace: 'default', podName: 'mongodb-0' }) {
488
- const { namespace = 'default', podName = 'mongodb-0' } = options;
489
-
490
- try {
491
- logger.info('Checking for MongoDB primary pod', { namespace, checkingPod: podName });
492
-
493
- const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mongosh --quiet --eval 'rs.status().members.filter(m => m.stateStr=="PRIMARY").map(m=>m.name)'`;
494
- const output = shellExec(command, { stdout: true, silent: true });
495
-
496
- if (!output || output.trim() === '') {
497
- logger.warn('No primary pod found in replica set');
498
- return null;
499
- }
500
-
501
- // Parse the output to get the primary pod name
502
- // Output format: [ 'mongodb-0:27017' ] or [ 'mongodb-1.mongodb-service:27017' ] or similar
503
- const match = output.match(/['"]([^'"]+)['"]/);
504
- if (match && match[1]) {
505
- let primaryName = match[1].split(':')[0]; // Extract pod name without port
506
- // Remove service suffix if present (e.g., "mongodb-1.mongodb-service" -> "mongodb-1")
507
- primaryName = primaryName.split('.')[0];
508
- logger.info('Found MongoDB primary pod', { primaryPod: primaryName });
509
- return primaryName;
510
- }
511
496
 
512
- logger.warn('Could not parse primary pod from replica set status', { output });
513
- return null;
514
- } catch (error) {
515
- logger.error('Failed to get MongoDB primary pod', { error: error.message });
516
- return null;
517
- }
518
- },
519
497
 
520
498
  /**
521
499
  * Main callback: Initiates database backup workflow.
@@ -626,7 +604,13 @@ class UnderpostDB {
626
604
  });
627
605
 
628
606
  if (options.primaryPodEnsure) {
629
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: options.primaryPodEnsure });
607
+ const primaryPodName = MongoBootstrap.getPrimaryPodName({
608
+ namespace,
609
+ podName: options.primaryPodEnsure,
610
+ username: process.env.MONGODB_USERNAME || process.env.DB_USER || '',
611
+ password: process.env.MONGODB_PASSWORD || process.env.DB_PASSWORD || '',
612
+ authDatabase: process.env.MONGODB_AUTH_DB || 'admin',
613
+ });
630
614
  if (!primaryPodName) {
631
615
  const baseCommand = options.dev ? 'node bin' : 'underpost';
632
616
  const baseClusterCommand = options.dev ? ' --dev' : '';
@@ -813,7 +797,13 @@ class UnderpostDB {
813
797
  podsToProcess = [];
814
798
  } else {
815
799
  const firstPod = targetPods[0].NAME;
816
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: firstPod });
800
+ const primaryPodName = MongoBootstrap.getPrimaryPodName({
801
+ namespace,
802
+ podName: firstPod,
803
+ username: user,
804
+ password,
805
+ authDatabase: 'admin',
806
+ });
817
807
 
818
808
  if (primaryPodName) {
819
809
  const primaryPod = targetPods.find((p) => p.NAME === primaryPodName);
@@ -839,6 +829,8 @@ class UnderpostDB {
839
829
  pods: podsToProcess.map((p) => p.NAME),
840
830
  });
841
831
 
832
+ let exportSucceeded = false;
833
+
842
834
  // Process each pod
843
835
  for (const pod of podsToProcess) {
844
836
  logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
@@ -871,7 +863,7 @@ class UnderpostDB {
871
863
 
872
864
  if (options.export === true) {
873
865
  const outputPath = options.outPath || toNewSqlPath;
874
- await Underpost.db._exportMariaDB({
866
+ const success = await Underpost.db._exportMariaDB({
875
867
  pod,
876
868
  namespace,
877
869
  dbName,
@@ -879,6 +871,7 @@ class UnderpostDB {
879
871
  password,
880
872
  outputPath,
881
873
  });
874
+ exportSucceeded = exportSucceeded || success;
882
875
  }
883
876
  break;
884
877
  }
@@ -889,6 +882,9 @@ class UnderpostDB {
889
882
  podName: pod.NAME,
890
883
  namespace,
891
884
  dbName,
885
+ user,
886
+ password,
887
+ authDatabase: 'admin',
892
888
  });
893
889
  if (stats) {
894
890
  Underpost.db._displayStats({ provider, dbName, stats });
@@ -904,18 +900,25 @@ class UnderpostDB {
904
900
  bsonPath,
905
901
  drop: options.drop,
906
902
  preserveUUID: options.preserveUUID,
903
+ user,
904
+ password,
905
+ authDatabase: 'admin',
907
906
  });
908
907
  }
909
908
 
910
909
  if (options.export === true) {
911
910
  const outputPath = options.outPath || toNewBsonPath;
912
- Underpost.db._exportMongoDB({
911
+ const success = Underpost.db._exportMongoDB({
913
912
  pod,
914
913
  namespace,
915
914
  dbName,
916
915
  outputPath,
917
916
  collections: options.collections,
917
+ user,
918
+ password,
919
+ authDatabase: 'admin',
918
920
  });
921
+ exportSucceeded = exportSucceeded || success;
919
922
  }
920
923
  break;
921
924
  }
@@ -926,6 +929,10 @@ class UnderpostDB {
926
929
  }
927
930
  }
928
931
 
932
+ if (options.export === true && exportSucceeded === true) {
933
+ Underpost.db._enforceBackupRetention(`../${repoName}/${hostFolder}`);
934
+ }
935
+
929
936
  // Mark this host+path combination as processed
930
937
  processedHostPaths.add(hostPathKey);
931
938
  }
@@ -948,6 +955,43 @@ class UnderpostDB {
948
955
  throw error;
949
956
  }
950
957
  },
958
+ /**
959
+ * Helper: Removes old timestamp backup folders and keeps only the newest ones.
960
+ * @method _enforceBackupRetention
961
+ * @memberof UnderpostDB
962
+ * @param {string} backupDir - Path to host-folder backup directory.
963
+ * @param {number} [maxRetention=MAX_BACKUP_RETENTION] - Maximum folders to keep.
964
+ * @return {number} Number of removed backup folders.
965
+ */
966
+ _enforceBackupRetention(backupDir, maxRetention = MAX_BACKUP_RETENTION) {
967
+ try {
968
+ if (!fs.existsSync(backupDir)) return 0;
969
+
970
+ const timestamps = fs
971
+ .readdirSync(backupDir)
972
+ .filter((entry) => /^\d+$/.test(entry))
973
+ .sort((a, b) => parseInt(b, 10) - parseInt(a, 10));
974
+
975
+ if (timestamps.length <= maxRetention) return 0;
976
+
977
+ const staleTimestamps = timestamps.slice(maxRetention);
978
+ staleTimestamps.forEach((timestamp) => {
979
+ fs.removeSync(`${backupDir}/${timestamp}`);
980
+ });
981
+
982
+ logger.info('Pruned old backup timestamp folders', {
983
+ backupDir,
984
+ kept: maxRetention,
985
+ removed: staleTimestamps.length,
986
+ removedTimestamps: staleTimestamps,
987
+ });
988
+
989
+ return staleTimestamps.length;
990
+ } catch (error) {
991
+ logger.error('Failed to enforce backup retention', { backupDir, maxRetention, error: error.message });
992
+ return 0;
993
+ }
994
+ },
951
995
 
952
996
  /**
953
997
  * Creates cluster metadata for the specified deployment.
@@ -999,7 +1043,7 @@ class UnderpostDB {
999
1043
  const retryDelay = 3000;
1000
1044
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1001
1045
  try {
1002
- await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
1046
+ await DataBaseProviderService.load({ apis: ['instance', 'cron'], host, path, db });
1003
1047
  break;
1004
1048
  } catch (err) {
1005
1049
  if (attempt === maxRetries) {
@@ -1013,7 +1057,7 @@ class UnderpostDB {
1013
1057
 
1014
1058
  try {
1015
1059
  /** @type {import('../api/instance/instance.model.js').InstanceModel} */
1016
- const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
1060
+ const Instance = DataBaseProviderService.getModel('instance', { host, path });
1017
1061
 
1018
1062
  await Instance.deleteMany();
1019
1063
  logger.info('Cleared existing instance metadata');
@@ -1114,10 +1158,10 @@ class UnderpostDB {
1114
1158
 
1115
1159
  const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
1116
1160
 
1117
- await DataBaseProvider.load({ apis: ['cron'], host, path, db });
1161
+ await DataBaseProviderService.load({ apis: ['cron'], host, path, db });
1118
1162
 
1119
1163
  /** @type {import('../api/cron/cron.model.js').CronModel} */
1120
- const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
1164
+ const Cron = DataBaseProviderService.getModel('cron', { host, path });
1121
1165
 
1122
1166
  await Cron.deleteMany();
1123
1167
  logger.info('Cleared existing cron metadata');
@@ -1136,7 +1180,7 @@ class UnderpostDB {
1136
1180
  logger.error('Failed to create cron metadata', { error: error.message });
1137
1181
  }
1138
1182
 
1139
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1183
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
1140
1184
  logger.info('Cluster metadata creation completed');
1141
1185
  } catch (error) {
1142
1186
  logger.error('Cluster metadata creation failed', { error: error.message });
@@ -1229,7 +1273,7 @@ class UnderpostDB {
1229
1273
  let dbProvider;
1230
1274
  for (let attempt = 1; attempt <= 3; attempt++) {
1231
1275
  try {
1232
- dbProvider = await DataBaseProvider.load({ apis, host, path, db });
1276
+ dbProvider = await DataBaseProviderService.load({ apis, host, path, db });
1233
1277
  break;
1234
1278
  } catch (err) {
1235
1279
  if (attempt === 3) throw err;