dbgate-api-premium 6.3.0 → 6.3.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api-premium",
3
3
  "main": "src/index.js",
4
- "version": "6.3.0",
4
+ "version": "6.3.3",
5
5
  "homepage": "https://dbgate.org/",
6
6
  "repository": {
7
7
  "type": "git",
@@ -29,10 +29,10 @@
29
29
  "compare-versions": "^3.6.0",
30
30
  "cors": "^2.8.5",
31
31
  "cross-env": "^6.0.3",
32
- "dbgate-datalib": "^6.3.0",
32
+ "dbgate-datalib": "^6.3.3",
33
33
  "dbgate-query-splitter": "^4.11.3",
34
- "dbgate-sqltree": "^6.3.0",
35
- "dbgate-tools": "^6.3.0",
34
+ "dbgate-sqltree": "^6.3.3",
35
+ "dbgate-tools": "^6.3.3",
36
36
  "debug": "^4.3.4",
37
37
  "diff": "^5.0.0",
38
38
  "diff2html": "^3.4.13",
@@ -83,7 +83,7 @@
83
83
  "devDependencies": {
84
84
  "@types/fs-extra": "^9.0.11",
85
85
  "@types/lodash": "^4.14.149",
86
- "dbgate-types": "^6.3.0",
86
+ "dbgate-types": "^6.3.3",
87
87
  "env-cmd": "^10.1.0",
88
88
  "jsdoc-to-markdown": "^9.0.5",
89
89
  "node-loader": "^1.0.2",
@@ -20,6 +20,7 @@ const logger = getLogger('storageAuthProvider');
20
20
  const axios = require('axios');
21
21
  const _ = require('lodash');
22
22
  const { authProxyGetTokenFromCode, authProxyGetRedirectUrl } = require('../utility/authProxy');
23
+ const { decryptUser } = require('../utility/crypting');
23
24
 
24
25
  async function loadPermissionsForUserId(userId) {
25
26
  const rolePermissions = sortPermissionsFromTheSameLevel(await storageReadUserRolePermissions(userId));
@@ -77,7 +78,7 @@ class LocalAuthProvider extends StorageProviderBase {
77
78
  if (rows.length == 0) {
78
79
  return { error: 'Login not allowed' };
79
80
  }
80
- const row = rows[0];
81
+ const row = decryptUser(rows[0]);
81
82
  if (row.password == password) {
82
83
  const userId = row.id;
83
84
  const permissions = await loadPermissionsForUserId(userId);
@@ -12,6 +12,7 @@ const {
12
12
  getAuthProviderById,
13
13
  } = require('../auth/authProvider');
14
14
  const storage = require('./storage');
15
+ const { decryptPasswordString } = require('../utility/crypting');
15
16
 
16
17
  const logger = getLogger('auth');
17
18
 
@@ -44,6 +45,7 @@ function authMiddleware(req, res, next) {
44
45
  '/connections/dblogin-auth',
45
46
  '/connections/dblogin-auth-token',
46
47
  '/health',
48
+ '/__health',
47
49
  ];
48
50
 
49
51
  // console.log('********************* getAuthProvider()', getAuthProvider());
@@ -95,7 +97,7 @@ module.exports = {
95
97
  let adminPassword = process.env.ADMIN_PASSWORD;
96
98
  if (!adminPassword) {
97
99
  const adminConfig = await storage.readConfig({ group: 'admin' });
98
- adminPassword = adminConfig?.adminPassword;
100
+ adminPassword = decryptPasswordString(adminConfig?.adminPassword);
99
101
  }
100
102
  if (adminPassword && adminPassword == password) {
101
103
  return {
@@ -102,12 +102,21 @@ function getPortalCollections() {
102
102
  trustServerCertificate: process.env[`SSL_TRUST_CERTIFICATE_${id}`],
103
103
  }));
104
104
 
105
+ for(const conn of connections) {
106
+ for(const prop in process.env) {
107
+ if (prop.startsWith(`CONNECTION_${conn._id}_`)) {
108
+ const name = prop.substring(`CONNECTION_${conn._id}_`.length);
109
+ conn[name] = process.env[prop];
110
+ }
111
+ }
112
+ }
113
+
105
114
  logger.info({ connections: connections.map(pickSafeConnectionInfo) }, 'Using connections from ENV variables');
106
115
  const noengine = connections.filter(x => !x.engine);
107
116
  if (noengine.length > 0) {
108
117
  logger.warn(
109
118
  { connections: noengine.map(x => x._id) },
110
- 'Invalid CONNECTIONS configutation, missing ENGINE for connection ID'
119
+ 'Invalid CONNECTIONS configuration, missing ENGINE for connection ID'
111
120
  );
112
121
  }
113
122
  return connections;
@@ -37,6 +37,8 @@ const loadModelTransform = require('../utility/loadModelTransform');
37
37
  const exportDbModelSql = require('../utility/exportDbModelSql');
38
38
  const axios = require('axios');
39
39
  const { callTextToSqlApi, callCompleteOnCursorApi, callRefactorSqlQueryApi } = require('../utility/authProxy');
40
+ const { decryptConnection } = require('../utility/crypting');
41
+ const { getSshTunnel } = require('../utility/sshTunnel');
40
42
 
41
43
  const logger = getLogger('databaseConnections');
42
44
 
@@ -140,6 +142,11 @@ module.exports = {
140
142
  if (newOpened.disconnected) return;
141
143
  this.close(conid, database, false);
142
144
  });
145
+ subprocess.on('error', err => {
146
+ logger.error(extractErrorLogData(err), 'Error in database connection subprocess');
147
+ if (newOpened.disconnected) return;
148
+ this.close(conid, database, false);
149
+ });
143
150
 
144
151
  subprocess.send({
145
152
  msgtype: 'connect',
@@ -619,9 +626,26 @@ module.exports = {
619
626
  command,
620
627
  { conid, database, outputFile, inputFile, options, selectedTables, skippedTables, argsFormat }
621
628
  ) {
622
- const connection = await connections.getCore({ conid });
629
+ const sourceConnection = await connections.getCore({ conid });
630
+ const connection = {
631
+ ...decryptConnection(sourceConnection),
632
+ };
623
633
  const driver = requireEngineDriver(connection);
624
634
 
635
+ if (!connection.port && driver.defaultPort) {
636
+ connection.port = driver.defaultPort.toString();
637
+ }
638
+
639
+ if (connection.useSshTunnel) {
640
+ const tunnel = await getSshTunnel(connection);
641
+ if (tunnel.state == 'error') {
642
+ throw new Error(tunnel.message);
643
+ }
644
+
645
+ connection.server = tunnel.localHost;
646
+ connection.port = tunnel.localPort;
647
+ }
648
+
625
649
  const settingsValue = await config.getSettings();
626
650
 
627
651
  const externalTools = {};
@@ -195,8 +195,8 @@ module.exports = {
195
195
  },
196
196
 
197
197
  exportDiagram_meta: true,
198
- async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
199
- await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
198
+ async exportDiagram({ filePath, html, css, themeType, themeClassName, watermark }) {
199
+ await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName, watermark));
200
200
  return true;
201
201
  },
202
202
 
@@ -98,6 +98,11 @@ module.exports = {
98
98
  if (newOpened.disconnected) return;
99
99
  this.close(conid, false);
100
100
  });
101
+ subprocess.on('error', err => {
102
+ logger.error(extractErrorLogData(err), 'Error in server connection subprocess');
103
+ if (newOpened.disconnected) return;
104
+ this.close(conid, false);
105
+ });
101
106
  subprocess.send({ msgtype: 'connect', ...connection, globalSettings: await config.getSettings() });
102
107
  return newOpened;
103
108
  });
@@ -12,15 +12,30 @@ const {
12
12
  storageReadConfig,
13
13
  storageWriteConfig,
14
14
  getStorageConnectionError,
15
+ storageSaveRelationDiff,
16
+ selectStorageIdentity,
15
17
  } = require('./storageDb');
16
18
  const { hasPermission } = require('../utility/hasPermission');
17
19
  const { changeSetToSql, removeSchemaFromChangeSet } = require('dbgate-datalib');
18
20
  const storageModel = require('../storageModel');
19
21
  const { dumpSqlCommand } = require('dbgate-sqltree');
20
- const { runCommandOnDriver, getLogger, runQueryFmt } = require('dbgate-tools');
22
+ const {
23
+ runCommandOnDriver,
24
+ getLogger,
25
+ runQueryFmt,
26
+ getPredefinedPermissions,
27
+ runQueryOnDriver,
28
+ } = require('dbgate-tools');
21
29
  const socket = require('../utility/socket');
22
30
  const { obtainRefreshedLicense } = require('../utility/authProxy');
23
31
  const { datadir } = require('../utility/directories');
32
+ const {
33
+ loadEncryptionKeyFromExternal,
34
+ encryptUser,
35
+ encryptConnection,
36
+ encryptPasswordString,
37
+ } = require('../utility/crypting');
38
+ const crypto = require('crypto');
24
39
 
25
40
  const logger = getLogger('storage');
26
41
 
@@ -31,7 +46,6 @@ function mapConnection(connnection) {
31
46
  };
32
47
  }
33
48
 
34
-
35
49
  let refreshLicenseStarted = false;
36
50
 
37
51
  module.exports = {
@@ -48,6 +62,22 @@ module.exports = {
48
62
  }
49
63
 
50
64
  setAuthProviders(providers, providers[defIndex]);
65
+
66
+ const keyResult = await storageSelectFmt(
67
+ "select ~value from ~config where ~group = 'admin' and ~key = 'encryptionKey'"
68
+ );
69
+ const [conn, driver] = await getStorageConnection();
70
+
71
+ await loadEncryptionKeyFromExternal(keyResult[0]?.value, async key => {
72
+ await runQueryFmt(
73
+ driver,
74
+ conn,
75
+ 'insert into ~config (~group, ~key, ~value) values (%v, %v, %v)',
76
+ 'admin',
77
+ 'encryptionKey',
78
+ key
79
+ );
80
+ });
51
81
  },
52
82
 
53
83
  async startRefreshLicense() {
@@ -135,7 +165,7 @@ module.exports = {
135
165
  displayName: 'Internal storage',
136
166
  defaultDatabase: process.env.STORAGE_DATABASE,
137
167
  singleDatabase: true,
138
- ...await getDbConnectionParams(),
168
+ ...(await getDbConnectionParams()),
139
169
  };
140
170
  }
141
171
 
@@ -183,8 +213,8 @@ module.exports = {
183
213
  return null;
184
214
  }
185
215
 
186
- await runQueryFmt(driver, conn, 'delete from auth_methods_config');
187
- await runQueryFmt(driver, conn, 'delete from auth_methods where id > 0');
216
+ await runQueryFmt(driver, conn, 'delete from ~auth_methods_config');
217
+ await runQueryFmt(driver, conn, 'delete from ~auth_methods where ~id > 0');
188
218
 
189
219
  let id = 1;
190
220
  for (const method of authMethods) {
@@ -192,7 +222,7 @@ module.exports = {
192
222
  await runQueryFmt(
193
223
  driver,
194
224
  conn,
195
- 'update auth_methods set name=%v, is_disabled=%v, is_default = %v, is_collapsed = %v where id = %v',
225
+ 'update ~auth_methods set ~name=%v, ~is_disabled=%v, ~is_default = %v, ~is_collapsed = %v where ~id = %v',
196
226
  method.name,
197
227
  method.isDisabled,
198
228
  method.isDefault,
@@ -228,7 +258,7 @@ module.exports = {
228
258
  await runQueryFmt(
229
259
  driver,
230
260
  conn,
231
- 'insert into auth_methods_config (%i, %i, %i) values (%v, %v, %v)',
261
+ 'insert into ~auth_methods_config (%i, %i, %i) values (%v, %v, %v)',
232
262
  'auth_method_id',
233
263
  'key',
234
264
  'value',
@@ -400,7 +430,7 @@ module.exports = {
400
430
  driver,
401
431
  conn,
402
432
  "insert into ~config (~group, ~key, ~value) values ('admin', 'adminPassword', %v)",
403
- newPassword
433
+ encryptPasswordString(newPassword)
404
434
  );
405
435
 
406
436
  return { status: 'ok' };
@@ -409,4 +439,257 @@ module.exports = {
409
439
  getStorageConnectionError() {
410
440
  return getStorageConnectionError();
411
441
  },
442
+
443
+ getUserList_meta: true,
444
+ async getUserList() {
445
+ const resp = await storageSelectFmt(`select ~users.~id,~users.~login,~users.~password,~users.~email from ~users`);
446
+ const usedRoles = await storageSelectFmt(`select ~roles.~name, ~user_roles.~user_id from
447
+ ~user_roles inner join ~roles ON ~user_roles.~role_id = ~roles.~id`);
448
+ const usedPermissions = await storageSelectFmt(`select * from ~user_permissions`);
449
+ return resp.map(x => ({
450
+ ...x,
451
+ password: x.password?.startsWith('crypt:') ? 'crypted' : x.password ? 'plain' : 'no',
452
+ usedRoles: usedRoles.filter(y => y.user_id == x.id).map(y => y.name),
453
+ usedPermissions: usedPermissions.filter(y => y.user_id == x.id).map(y => y.permission),
454
+ }));
455
+ },
456
+
457
+ getUserDetail_meta: true,
458
+ async getUserDetail({ id }) {
459
+ const resp =
460
+ id == 'new'
461
+ ? {}
462
+ : await storageSelectFmt(
463
+ `select ~users.~id,~users.~login,~users.password, ~users.~email from ~users where ~users.~id = %v`,
464
+ id
465
+ );
466
+
467
+ const rolePermissions = id == 'new' ? [] : await storageReadUserRolePermissions(id);
468
+ const basePermissions = [...getPredefinedPermissions('logged-user'), ...rolePermissions];
469
+ const permissions =
470
+ id == 'new'
471
+ ? []
472
+ : (await storageSelectFmt('select * from ~user_permissions where ~user_id = %v', id)).map(x => x.permission);
473
+ const allRoles = await storageSelectFmt('select * from ~roles');
474
+ const usedRoles =
475
+ id == 'new'
476
+ ? []
477
+ : (
478
+ await storageSelectFmt(`select ~user_roles.~role_id from ~user_roles where ~user_roles.~user_id = %v`, id)
479
+ ).map(x => x.role_id);
480
+ const allConnections = await storageSelectFmt('select * from ~connections');
481
+ const usedConnections =
482
+ id == 'new'
483
+ ? []
484
+ : (await storageSelectFmt('select ~connection_id from ~user_connections where ~user_id = %v', id)).map(
485
+ x => x.connection_id
486
+ );
487
+
488
+ return {
489
+ ...resp[0],
490
+ basePermissions,
491
+ permissions,
492
+ allRoles,
493
+ usedRoles,
494
+ allConnections,
495
+ usedConnections,
496
+ encryptPassword: !!(resp[0]?.password?.startsWith('crypt:') || id == 'new'),
497
+ };
498
+ },
499
+
500
+ saveUserDetail_meta: true,
501
+ async saveUserDetail(user) {
502
+ const [conn, driver] = await getStorageConnection();
503
+
504
+ const { login, password, email, usedRoles, usedConnections, permissions } = encryptUser(user);
505
+ let id = user.id;
506
+
507
+ if (id == null) {
508
+ await runQueryFmt(
509
+ driver,
510
+ conn,
511
+ 'insert into ~users (~login, ~password, ~email) values (%v, %v, %v)',
512
+ login,
513
+ password,
514
+ email
515
+ );
516
+ id = await selectStorageIdentity('users');
517
+ } else {
518
+ await runQueryFmt(
519
+ driver,
520
+ conn,
521
+ 'update ~users set ~login=%v, ~password=%v, ~email=%v where ~id = %v',
522
+ login,
523
+ password,
524
+ email,
525
+ id
526
+ );
527
+ }
528
+
529
+ await storageSaveRelationDiff('user_permissions', 'user_id', 'permission', id, permissions);
530
+ await storageSaveRelationDiff('user_roles', 'user_id', 'role_id', id, usedRoles);
531
+ await storageSaveRelationDiff('user_connections', 'user_id', 'connection_id', id, usedConnections);
532
+
533
+ return true;
534
+ },
535
+
536
+ deleteUser_meta: true,
537
+ async deleteUser({ id }) {
538
+ const [conn, driver] = await getStorageConnection();
539
+
540
+ await runQueryFmt(driver, conn, 'delete from ~users where ~id = %v', id);
541
+
542
+ return true;
543
+ },
544
+
545
+ getConnectionList_meta: true,
546
+ async getConnectionList() {
547
+ const resp = await storageSelectFmt(
548
+ `select ~connections.~id,~connections.~engine,~connections.~displayName,~connections.~server,~connections.~user from ~connections`
549
+ );
550
+ return resp;
551
+ },
552
+
553
+ getConnectionDetail_meta: true,
554
+ async getConnectionDetail({ id }) {
555
+ if (id == 'new') {
556
+ return {
557
+ conid: crypto.randomUUID(),
558
+ };
559
+ }
560
+
561
+ const resp = await storageSelectFmt(`select * from ~connections where ~id = %v`, id);
562
+ return resp[0];
563
+ },
564
+
565
+ saveConnectionDetail_meta: true,
566
+ async saveConnectionDetail(savedConnection) {
567
+ const [conn, driver] = await getStorageConnection();
568
+
569
+ const connection = encryptConnection(savedConnection);
570
+
571
+ const tableConnections = storageModel.tables.find(x => x.pureName == 'connections');
572
+ const id = connection.id;
573
+
574
+ const usedColumns = _.difference(
575
+ _.intersection(
576
+ tableConnections.columns.map(x => x.columnName),
577
+ Object.keys(connection)
578
+ ),
579
+ ['id']
580
+ );
581
+
582
+ if (id) {
583
+ await runQueryOnDriver(conn, driver, dmp => {
584
+ dmp.put(`update ~connections set`);
585
+ dmp.putCollection(',', usedColumns, col => {
586
+ dmp.put(`%i = %v`, col, connection[col]);
587
+ });
588
+ dmp.put(` where ~id = %v`, id);
589
+ });
590
+ } else {
591
+ await runQueryFmt(
592
+ driver,
593
+ conn,
594
+ `insert into ~connections (%,i) values (%,v)`,
595
+ usedColumns,
596
+ usedColumns.map(x => connection[x])
597
+ );
598
+ }
599
+
600
+ return true;
601
+ },
602
+
603
+ deleteConnection_meta: true,
604
+ async deleteConnection({ id }) {
605
+ const [conn, driver] = await getStorageConnection();
606
+ if (!conn) {
607
+ return null;
608
+ }
609
+
610
+ await runQueryFmt(driver, conn, 'delete from ~connections where ~id = %v', id);
611
+
612
+ return true;
613
+ },
614
+
615
+ getRoleList_meta: true,
616
+ async getRoleList() {
617
+ const resp = await storageSelectFmt(`select ~roles.~id,~roles.~name from ~roles`);
618
+ const usedPermissions = await storageSelectFmt(`select * from ~role_permissions`);
619
+ return resp.map(x => ({
620
+ ...x,
621
+ usedPermissions: usedPermissions.filter(y => y.role_id == x.id).map(y => y.permission),
622
+ }));
623
+ },
624
+
625
+ getRoleDetail_meta: true,
626
+ async getRoleDetail({ id }) {
627
+ const resp =
628
+ id == 'new' ? {} : await storageSelectFmt(`select ~roles.~id,~roles.~name from ~roles where ~roles.~id = %v`, id);
629
+
630
+ const basePermissions = getPredefinedPermissions(resp?.name ?? 'logged-user');
631
+ const permissions =
632
+ id == 'new'
633
+ ? []
634
+ : (await storageSelectFmt('select * from ~role_permissions where ~role_id = %v', id)).map(x => x.permission);
635
+ const allUsers = await storageSelectFmt('select * from ~users');
636
+ const usedUsers =
637
+ id == 'new'
638
+ ? []
639
+ : (
640
+ await storageSelectFmt(`select ~user_roles.~user_id from ~user_roles where ~user_roles.~role_id = %v`, id)
641
+ ).map(x => x.user_id);
642
+ const allConnections = await storageSelectFmt('select * from ~connections');
643
+ const usedConnections =
644
+ id == 'new'
645
+ ? []
646
+ : (await storageSelectFmt('select ~connection_id from ~role_connections where ~role_id = %v', id)).map(
647
+ x => x.connection_id
648
+ );
649
+
650
+ return {
651
+ ...resp[0],
652
+ basePermissions,
653
+ permissions,
654
+ allUsers,
655
+ usedUsers,
656
+ allConnections,
657
+ usedConnections,
658
+ };
659
+ },
660
+
661
+ deleteRole_meta: true,
662
+ async deleteRole({ id }) {
663
+ const [conn, driver] = await getStorageConnection();
664
+ if (!conn) {
665
+ return null;
666
+ }
667
+
668
+ await runQueryFmt(driver, conn, 'delete from ~roles where ~id = %v', id);
669
+
670
+ return true;
671
+ },
672
+
673
+ saveRoleDetail_meta: true,
674
+ async saveRoleDetail(role) {
675
+ const [conn, driver] = await getStorageConnection();
676
+
677
+ const { name, usedConnections, usedUsers, permissions } = encryptUser(role);
678
+ let id = role.id;
679
+
680
+ if (id == null) {
681
+ await runQueryFmt(driver, conn, 'insert into ~roles (~name) values (%v)', name);
682
+ id = await selectStorageIdentity('roles');
683
+ } else {
684
+ if (id > 0) {
685
+ await runQueryFmt(driver, conn, 'update ~roles set ~name=%v where ~id = %v', name, id);
686
+ }
687
+ }
688
+
689
+ await storageSaveRelationDiff('role_permissions', 'role_id', 'permission', id, permissions);
690
+ await storageSaveRelationDiff('user_roles', 'role_id', 'user_id', id, usedUsers);
691
+ await storageSaveRelationDiff('role_connections', 'role_id', 'connection_id', id, usedConnections);
692
+
693
+ return true;
694
+ },
412
695
  };
@@ -1,7 +1,13 @@
1
1
  const requireEngineDriver = require('../utility/requireEngineDriver');
2
2
  const storageModel = require('../storageModel');
3
3
  const dbgateApi = require('../shell');
4
- const { getPredefinedPermissions, getLogger, extractErrorLogData } = require('dbgate-tools');
4
+ const {
5
+ getPredefinedPermissions,
6
+ getLogger,
7
+ extractErrorLogData,
8
+ runQueryFmt,
9
+ runQueryOnDriver,
10
+ } = require('dbgate-tools');
5
11
  const _ = require('lodash');
6
12
  const logger = getLogger('storageDb');
7
13
  const { extractConnectionSslParams } = require('../utility/connectUtility');
@@ -238,10 +244,60 @@ async function storageWriteConfig(group, config) {
238
244
  }
239
245
  }
240
246
 
247
+ async function storageSaveRelationDiff(table, idColumn, valueColumn, idValue, newValueSet) {
248
+ const [conn, driver] = await getStorageConnection();
249
+ if (!conn) {
250
+ return null;
251
+ }
252
+
253
+ const oldValueSet = await storageSelectFmt(`select %i from %i where %i = %v`, valueColumn, table, idColumn, idValue);
254
+
255
+ const oldValues = oldValueSet.map(x => x[valueColumn]);
256
+
257
+ for (const added of _.difference(newValueSet, oldValues)) {
258
+ await runQueryFmt(
259
+ driver,
260
+ conn,
261
+ 'insert into %i (%i, %i) values (%v, %v)',
262
+ table,
263
+ idColumn,
264
+ valueColumn,
265
+ idValue,
266
+ added
267
+ );
268
+ }
269
+
270
+ for (const removed of _.difference(oldValues, newValueSet)) {
271
+ await runQueryFmt(
272
+ driver,
273
+ conn,
274
+ 'delete from %i where %i = %v and %i = %v',
275
+ table,
276
+ idColumn,
277
+ idValue,
278
+ valueColumn,
279
+ removed
280
+ );
281
+ }
282
+ }
283
+
241
284
  function getStorageConnectionError() {
242
285
  return storageConnectionError;
243
286
  }
244
287
 
288
+ async function selectStorageIdentity(tableName) {
289
+ const [conn, driver] = await getStorageConnection();
290
+
291
+ const resp = await runQueryOnDriver(conn, driver, dmp =>
292
+ dmp.selectScopeIdentity(
293
+ // @ts-ignore
294
+ { pureName: tableName }
295
+ )
296
+ );
297
+
298
+ return Object.entries(resp.rows[0])[0][1];
299
+ }
300
+
245
301
  module.exports = {
246
302
  getStorageConnection,
247
303
  storageSelectFmt,
@@ -254,4 +310,6 @@ module.exports = {
254
310
  storageWriteConfig,
255
311
  getStorageConnectionError,
256
312
  storageSqlCommandFmt,
313
+ storageSaveRelationDiff,
314
+ selectStorageIdentity,
257
315
  };
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '6.3.0',
4
- buildTime: '2025-03-18T11:15:02.094Z'
3
+ version: '6.3.3',
4
+ buildTime: '2025-04-09T10:29:52.002Z'
5
5
  };
package/src/main.js CHANGED
@@ -38,7 +38,7 @@ const { getLogger } = require('dbgate-tools');
38
38
  const { getDefaultAuthProvider } = require('./auth/authProvider');
39
39
  const startCloudUpgradeTimer = require('./utility/cloudUpgrade');
40
40
  const { isProApp } = require('./utility/checkLicense');
41
- const getHealthStatus = require('./utility/healthStatus');
41
+ const { getHealthStatus, getHealthStatusSprinx } = require('./utility/healthStatus');
42
42
 
43
43
  const logger = getLogger('main');
44
44
 
@@ -124,6 +124,12 @@ function start() {
124
124
  res.end(JSON.stringify(health, null, 2));
125
125
  });
126
126
 
127
+ app.get(getExpressPath('/__health'), async function (req, res) {
128
+ res.setHeader('Content-Type', 'application/json');
129
+ const health = await getHealthStatusSprinx();
130
+ res.end(JSON.stringify(health, null, 2));
131
+ });
132
+
127
133
  app.use(bodyParser.json({ limit: '50mb' }));
128
134
 
129
135
  app.use(
@@ -38,6 +38,8 @@ function start() {
38
38
  detail: formatErrorDetail(e, connection),
39
39
  });
40
40
  }
41
+
42
+ process.exit(0);
41
43
  });
42
44
  }
43
45
 
@@ -60,6 +60,10 @@ class DatastoreProxy {
60
60
  // if (this.disconnected) return;
61
61
  this.subprocess = null;
62
62
  });
63
+ this.subprocess.on('error', err => {
64
+ logger.error(extractErrorLogData(err), 'Error in data store subprocess');
65
+ this.subprocess = null;
66
+ });
63
67
  this.subprocess.send({ msgtype: 'open', file: this.file });
64
68
  }
65
69
  return this.subprocess;
@@ -96,7 +96,9 @@ async function connectUtility(driver, storedConnection, connectionMode, addition
96
96
  ...decryptConnection(connectionLoaded),
97
97
  };
98
98
 
99
- if (!connection.port && driver.defaultPort) connection.port = driver.defaultPort.toString();
99
+ if (!connection.port && driver.defaultPort) {
100
+ connection.port = driver.defaultPort.toString();
101
+ }
100
102
 
101
103
  if (connection.useSshTunnel) {
102
104
  const tunnel = await getSshTunnelProxy(connection);
@@ -5,12 +5,16 @@ const path = require('path');
5
5
  const _ = require('lodash');
6
6
 
7
7
  const { datadir } = require('./directories');
8
+ const { encryptionKeyArg } = require('./processArgs');
8
9
 
9
10
  const defaultEncryptionKey = 'mQAUaXhavRGJDxDTXSCg7Ej0xMmGCrx6OKA07DIMBiDcYYkvkaXjTAzPUEHEHEf9';
10
11
 
11
12
  let _encryptionKey = null;
12
13
 
13
14
  function loadEncryptionKey() {
15
+ if (encryptionKeyArg) {
16
+ return encryptionKeyArg;
17
+ }
14
18
  if (_encryptionKey) {
15
19
  return _encryptionKey;
16
20
  }
@@ -33,6 +37,26 @@ function loadEncryptionKey() {
33
37
  return _encryptionKey;
34
38
  }
35
39
 
40
+ async function loadEncryptionKeyFromExternal(storedValue, setStoredValue) {
41
+ const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey);
42
+
43
+ if (!storedValue) {
44
+ const generatedKey = crypto.randomBytes(32);
45
+ const newKey = generatedKey.toString('hex');
46
+ const result = {
47
+ encryptionKey: newKey,
48
+ };
49
+ await setStoredValue(encryptor.encrypt(result));
50
+
51
+ setEncryptionKey(newKey);
52
+
53
+ return;
54
+ }
55
+
56
+ const data = encryptor.decrypt(storedValue);
57
+ setEncryptionKey(data['encryptionKey']);
58
+ }
59
+
36
60
  let _encryptor = null;
37
61
 
38
62
  function getEncryptor() {
@@ -43,35 +67,46 @@ function getEncryptor() {
43
67
  return _encryptor;
44
68
  }
45
69
 
46
- function encryptPasswordField(connection, field) {
47
- if (
48
- connection &&
49
- connection[field] &&
50
- !connection[field].startsWith('crypt:') &&
51
- connection.passwordMode != 'saveRaw'
52
- ) {
70
+ function encryptPasswordString(password) {
71
+ if (password && !password.startsWith('crypt:')) {
72
+ return 'crypt:' + getEncryptor().encrypt(password);
73
+ }
74
+ return password;
75
+ }
76
+
77
+ function decryptPasswordString(password) {
78
+ if (password && password.startsWith('crypt:')) {
79
+ return getEncryptor().decrypt(password.substring('crypt:'.length));
80
+ }
81
+ return password;
82
+ }
83
+
84
+ function encryptObjectPasswordField(obj, field) {
85
+ if (obj && obj[field] && !obj[field].startsWith('crypt:')) {
53
86
  return {
54
- ...connection,
55
- [field]: 'crypt:' + getEncryptor().encrypt(connection[field]),
87
+ ...obj,
88
+ [field]: 'crypt:' + getEncryptor().encrypt(obj[field]),
56
89
  };
57
90
  }
58
- return connection;
91
+ return obj;
59
92
  }
60
93
 
61
- function decryptPasswordField(connection, field) {
62
- if (connection && connection[field] && connection[field].startsWith('crypt:')) {
94
+ function decryptObjectPasswordField(obj, field) {
95
+ if (obj && obj[field] && obj[field].startsWith('crypt:')) {
63
96
  return {
64
- ...connection,
65
- [field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)),
97
+ ...obj,
98
+ [field]: getEncryptor().decrypt(obj[field].substring('crypt:'.length)),
66
99
  };
67
100
  }
68
- return connection;
101
+ return obj;
69
102
  }
70
103
 
71
104
  function encryptConnection(connection) {
72
- connection = encryptPasswordField(connection, 'password');
73
- connection = encryptPasswordField(connection, 'sshPassword');
74
- connection = encryptPasswordField(connection, 'sshKeyfilePassword');
105
+ if (connection.passwordMode != 'saveRaw') {
106
+ connection = encryptObjectPasswordField(connection, 'password');
107
+ connection = encryptObjectPasswordField(connection, 'sshPassword');
108
+ connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword');
109
+ }
75
110
  return connection;
76
111
  }
77
112
 
@@ -81,12 +116,24 @@ function maskConnection(connection) {
81
116
  }
82
117
 
83
118
  function decryptConnection(connection) {
84
- connection = decryptPasswordField(connection, 'password');
85
- connection = decryptPasswordField(connection, 'sshPassword');
86
- connection = decryptPasswordField(connection, 'sshKeyfilePassword');
119
+ connection = decryptObjectPasswordField(connection, 'password');
120
+ connection = decryptObjectPasswordField(connection, 'sshPassword');
121
+ connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword');
87
122
  return connection;
88
123
  }
89
124
 
125
+ function encryptUser(user) {
126
+ if (user.encryptPassword) {
127
+ user = encryptObjectPasswordField(user, 'password');
128
+ }
129
+ return user;
130
+ }
131
+
132
+ function decryptUser(user) {
133
+ user = decryptObjectPasswordField(user, 'password');
134
+ return user;
135
+ }
136
+
90
137
  function pickSafeConnectionInfo(connection) {
91
138
  if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) {
92
139
  return connection;
@@ -99,10 +146,27 @@ function pickSafeConnectionInfo(connection) {
99
146
  });
100
147
  }
101
148
 
149
+ function setEncryptionKey(encryptionKey) {
150
+ _encryptionKey = encryptionKey;
151
+ _encryptor = null;
152
+ global.ENCRYPTION_KEY = encryptionKey;
153
+ }
154
+
155
+ function getEncryptionKey() {
156
+ return _encryptionKey;
157
+ }
158
+
102
159
  module.exports = {
103
160
  loadEncryptionKey,
104
161
  encryptConnection,
162
+ encryptUser,
163
+ decryptUser,
105
164
  decryptConnection,
106
165
  maskConnection,
107
166
  pickSafeConnectionInfo,
167
+ loadEncryptionKeyFromExternal,
168
+ getEncryptionKey,
169
+ setEncryptionKey,
170
+ encryptPasswordString,
171
+ decryptPasswordString,
108
172
  };
@@ -1,4 +1,11 @@
1
- const getDiagramExport = (html, css, themeType, themeClassName) => {
1
+ const getDiagramExport = (html, css, themeType, themeClassName, watermark) => {
2
+ const watermarkHtml = watermark
3
+ ? `
4
+ <div style="position: fixed; bottom: 0; right: 0; padding: 5px; font-size: 12px; color: var(--theme-font-2); background-color: var(--theme-bg-2); border-top-left-radius: 5px; border: 1px solid var(--theme-border);">
5
+ ${watermark}
6
+ </div>
7
+ `
8
+ : '';
2
9
  return `<html>
3
10
  <meta charset='utf-8'>
4
11
 
@@ -13,10 +20,44 @@ const getDiagramExport = (html, css, themeType, themeClassName) => {
13
20
  </style>
14
21
 
15
22
  <link rel="stylesheet" href='https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.css' />
23
+
24
+ <script>
25
+ let lastX = null;
26
+ let lastY = null;
27
+
28
+ const handleMoveDown = e => {
29
+ lastX = e.clientX;
30
+ lastY = e.clientY;
31
+ document.addEventListener('mousemove', handleMoveMove, true);
32
+ document.addEventListener('mouseup', handleMoveEnd, true);
33
+ };
34
+
35
+ const handleMoveMove = e => {
36
+ e.preventDefault();
37
+
38
+ document.body.scrollLeft -= e.clientX - lastX;
39
+ document.body.scrollTop -= e.clientY - lastY;
40
+
41
+ lastX = e.clientX;
42
+ lastY = e.clientY;
43
+ };
44
+ const handleMoveEnd = e => {
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+
48
+ lastX = null;
49
+ lastY = null;
50
+ document.removeEventListener('mousemove', handleMoveMove, true);
51
+ document.removeEventListener('mouseup', handleMoveEnd, true);
52
+ };
53
+
54
+ document.addEventListener('mousedown', handleMoveDown);
55
+ </script>
16
56
  </head>
17
57
 
18
- <body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}'>
58
+ <body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}' style='user-select:none; cursor:pointer'>
19
59
  ${html}
60
+ ${watermarkHtml}
20
61
  </body>
21
62
 
22
63
  </html>`;
@@ -22,6 +22,8 @@ const getMapExport = (geoJson) => {
22
22
  })
23
23
  .addTo(map);
24
24
 
25
+ leaflet.control.scale().addTo(map);
26
+
25
27
  const geoJsonObj = leaflet
26
28
  .geoJSON(${JSON.stringify(geoJson)}, {
27
29
  style: function () {
@@ -24,4 +24,15 @@ async function getHealthStatus() {
24
24
  };
25
25
  }
26
26
 
27
- module.exports = getHealthStatus;
27
+ async function getHealthStatusSprinx() {
28
+ return {
29
+ overallStatus: 'OK',
30
+ timeStamp: new Date().toISOString(),
31
+ timeStampUnix: Math.floor(Date.now() / 1000),
32
+ };
33
+ }
34
+
35
+ module.exports = {
36
+ getHealthStatus,
37
+ getHealthStatusSprinx,
38
+ };
@@ -17,6 +17,7 @@ const processDisplayName = getNamedArg('--process-display-name');
17
17
  const listenApi = process.argv.includes('--listen-api');
18
18
  const listenApiChild = process.argv.includes('--listen-api-child') || listenApi;
19
19
  const runE2eTests = process.argv.includes('--run-e2e-tests');
20
+ const encryptionKeyArg = getNamedArg('--encryption-key');
20
21
 
21
22
  function getPassArgs() {
22
23
  const res = [];
@@ -31,6 +32,9 @@ function getPassArgs() {
31
32
  if (runE2eTests) {
32
33
  res.push('--run-e2e-tests');
33
34
  }
35
+ if (global['ENCRYPTION_KEY']) {
36
+ res.push('--encryption-key', global['ENCRYPTION_KEY']);
37
+ }
34
38
  return res;
35
39
  }
36
40
 
@@ -45,4 +49,5 @@ module.exports = {
45
49
  listenApiChild,
46
50
  processDisplayName,
47
51
  runE2eTests,
52
+ encryptionKeyArg,
48
53
  };
@@ -57,10 +57,21 @@ function callForwardProcess(connection, tunnelConfig, tunnelCacheKey) {
57
57
  }
58
58
  });
59
59
  subprocess.on('exit', code => {
60
- logger.info('SSH forward process exited');
60
+ logger.info(`SSH forward process exited with code ${code}`);
61
61
  delete sshTunnelCache[tunnelCacheKey];
62
62
  if (!promiseHandled) {
63
- reject(new Error('SSH forward process exited, try to change "Local host address for SSH connections" in Settings/Connections'));
63
+ reject(
64
+ new Error(
65
+ 'SSH forward process exited, try to change "Local host address for SSH connections" in Settings/Connections'
66
+ )
67
+ );
68
+ }
69
+ });
70
+ subprocess.on('error', error => {
71
+ logger.error(extractErrorLogData(error), 'SSH forward process error');
72
+ delete sshTunnelCache[tunnelCacheKey];
73
+ if (!promiseHandled) {
74
+ reject(error);
64
75
  }
65
76
  });
66
77
  });