From 4dc2627da2768e3a00df746f028997f83f795ac8 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 15 May 2025 16:01:51 +0200 Subject: [PATCH 01/39] cloud login WIP --- packages/api/.env | 1 + packages/api/src/controllers/auth.js | 9 +++++ packages/api/src/utility/authProxy.js | 9 +++++ packages/api/src/utility/cloudIntf.js | 34 +++++++++++++++++++ packages/web/src/icons/FontIcon.svelte | 3 ++ .../web/src/widgets/WidgetIconPanel.svelte | 14 +++++++- 6 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/utility/cloudIntf.js diff --git a/packages/api/.env b/packages/api/.env index dc654e025..790defd5d 100644 --- a/packages/api/.env +++ b/packages/api/.env @@ -1,5 +1,6 @@ DEVMODE=1 SHELL_SCRIPTING=1 +# LOCAL_DBGATE_IDENTITY=1 # CLOUD_UPGRADE_FILE=c:\test\upg\upgrade.zip diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 15600f156..0d3b81a00 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -13,6 +13,7 @@ const { } = require('../auth/authProvider'); const storage = require('./storage'); const { decryptPasswordString } = require('../utility/crypting'); +const { createDbGateIdentitySession, getIdentitySigninUrl } = require('../utility/cloudIntf'); const logger = getLogger('auth'); @@ -135,5 +136,13 @@ module.exports = { return getAuthProviderById(amoid).redirect(params); }, + createCloudLoginSession_meta: true, + async createCloudLoginSession({ client }) { + const sid = await createDbGateIdentitySession(client); + return { + url: getIdentitySigninUrl(sid), + }; + }, + authMiddleware, }; diff --git a/packages/api/src/utility/authProxy.js b/packages/api/src/utility/authProxy.js index 0d998fec2..744536177 100644 --- a/packages/api/src/utility/authProxy.js +++ b/packages/api/src/utility/authProxy.js @@ -36,6 +36,14 @@ async function callRefactorSqlQueryApi(query, task, structure, dialect) { return null; } +function getExternalParamsWithLicense() { + return { + headers: { + 'Content-Type': 'application/json', + }, + }; +} + module.exports = { isAuthProxySupported, authProxyGetRedirectUrl, @@ -47,4 +55,5 @@ module.exports = { callTextToSqlApi, callCompleteOnCursorApi, callRefactorSqlQueryApi, + getExternalParamsWithLicense, }; diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js new file mode 100644 index 000000000..3258167e3 --- /dev/null +++ b/packages/api/src/utility/cloudIntf.js @@ -0,0 +1,34 @@ +const axios = require('axios'); +const { getExternalParamsWithLicense } = require('./authProxy'); + +const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY + ? 'http://localhost:3001' + : process.env.DEVWEB || process.env.DEVMODE + ? 'https://identity.dbgate.udolni.net' + : 'https://identity.dbgate.io'; + +const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD + ? 'http://localhost:3109' + : process.env.DEVWEB || process.env.DEVMODE + ? 'https://cloud.dbgate.udolni.net' + : 'https://cloud.dbgate.io'; + +async function createDbGateIdentitySession(client) { + const resp = await axios.default.post( + `${DBGATE_IDENTITY_URL}/api/create-session`, + { + client, + }, + getExternalParamsWithLicense() + ); + return resp.data.sid; +} + +function getIdentitySigninUrl(sid) { + return `${DBGATE_IDENTITY_URL}/signin/${sid}`; +} + +module.exports = { + createDbGateIdentitySession, + getIdentitySigninUrl, +}; diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 86bee43eb..1dc0f9e98 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -112,6 +112,9 @@ 'icon square': 'mdi mdi-square', 'icon data-deploy': 'mdi mdi-database-settings', + 'icon cloud-account': 'mdi mdi-account-remove-outline', + 'icon cloud-account-connected': 'mdi mdi-account-check-outline', + 'icon edit': 'mdi mdi-pencil', 'icon delete': 'mdi mdi-delete', 'icon arrow-up': 'mdi mdi-arrow-up', diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte index dec384ed0..0245a5f28 100644 --- a/packages/web/src/widgets/WidgetIconPanel.svelte +++ b/packages/web/src/widgets/WidgetIconPanel.svelte @@ -13,6 +13,9 @@ import mainMenuDefinition from '../../../../app/src/mainMenuDefinition'; import hasPermission from '../utility/hasPermission'; import { isProApp } from '../utility/proTools'; + import { openWebLink } from '../utility/simpleTools'; + import { apiCall } from '../utility/api'; + import getElectron from '../utility/getElectron'; let domSettings; let domMainMenu; @@ -103,6 +106,11 @@ const items = mainMenuDefinition({ editMenu: false }); currentDropDownMenu.set({ left, top, items }); } + + async function handleOpenCloudLogin() { + const { url, sid } = await apiCall('auth/create-cloud-login-session', { client: getElectron() ? 'app' : 'web' }); + openWebLink(url); + }
@@ -129,7 +137,7 @@
 
-
{ @@ -138,6 +146,10 @@ data-testid="WidgetIconPanel_lockDb" > +
--> + +
+
From 9329345d98c0259cf7a4b67ba51a2d13249e39fc Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 12:19:26 +0200 Subject: [PATCH 02/39] basic cloud signin workflow --- packages/api/src/controllers/auth.js | 12 +++-- packages/api/src/utility/cloudIntf.js | 36 ++++++++++++-- packages/web/src/App.svelte | 3 +- packages/web/src/commands/stdCommands.ts | 10 ++++ packages/web/src/icons/FontIcon.svelte | 1 + packages/web/src/stores.ts | 2 + packages/web/src/utility/api.ts | 7 +++ .../web/src/widgets/WidgetIconPanel.svelte | 48 ++++++++++++++++--- 8 files changed, 101 insertions(+), 18 deletions(-) diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 0d3b81a00..fbaa74f3d 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -13,7 +13,8 @@ const { } = require('../auth/authProvider'); const storage = require('./storage'); const { decryptPasswordString } = require('../utility/crypting'); -const { createDbGateIdentitySession, getIdentitySigninUrl } = require('../utility/cloudIntf'); +const { createDbGateIdentitySession, startCloudTokenChecking } = require('../utility/cloudIntf'); +const socket = require('../utility/socket'); const logger = getLogger('auth'); @@ -138,10 +139,11 @@ module.exports = { createCloudLoginSession_meta: true, async createCloudLoginSession({ client }) { - const sid = await createDbGateIdentitySession(client); - return { - url: getIdentitySigninUrl(sid), - }; + const res = await createDbGateIdentitySession(client); + startCloudTokenChecking(res.sid, token => { + socket.emit('got-cloud-token', { token }); + }); + return res; }, authMiddleware, diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 3258167e3..ee2b8301f 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -1,8 +1,11 @@ const axios = require('axios'); const { getExternalParamsWithLicense } = require('./authProxy'); +const { getLogger, extractErrorLogData } = require('dbgate-tools'); + +const logger = getLogger('cloudIntf'); const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY - ? 'http://localhost:3001' + ? 'http://localhost:3103' : process.env.DEVWEB || process.env.DEVMODE ? 'https://identity.dbgate.udolni.net' : 'https://identity.dbgate.io'; @@ -21,14 +24,37 @@ async function createDbGateIdentitySession(client) { }, getExternalParamsWithLicense() ); - return resp.data.sid; + return { + sid: resp.data.sid, + url: `${DBGATE_IDENTITY_URL}/api/signin/${resp.data.sid}`, + }; } -function getIdentitySigninUrl(sid) { - return `${DBGATE_IDENTITY_URL}/signin/${sid}`; +function startCloudTokenChecking(sid, callback) { + const started = Date.now(); + const interval = setInterval(async () => { + if (Date.now() - started > 60 * 1000) { + clearInterval(interval); + return; + } + + try { + const resp = await axios.default.get( + `${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, + getExternalParamsWithLicense() + ); + + if (resp.data.status == 'ok') { + clearInterval(interval); + callback(resp.data.token); + } + } catch (err) { + logger.error(extractErrorLogData(err), 'Error checking cloud token'); + } + }, 500); } module.exports = { createDbGateIdentitySession, - getIdentitySigninUrl, + startCloudTokenChecking, }; diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 487880df7..a0787efa0 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -14,7 +14,7 @@ // import { shouldWaitForElectronInitialize } from './utility/getElectron'; import { subscribeConnectionPingers } from './utility/connectionsPinger'; import { subscribePermissionCompiler } from './utility/hasPermission'; - import { apiCall, installNewVolatileConnectionListener } from './utility/api'; + import { apiCall, installNewCloudTokenListener, installNewVolatileConnectionListener } from './utility/api'; import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders'; import AppTitleProvider from './utility/AppTitleProvider.svelte'; import getElectron from './utility/getElectron'; @@ -51,6 +51,7 @@ subscribeConnectionPingers(); subscribePermissionCompiler(); installNewVolatileConnectionListener(); + installNewCloudTokenListener(); initializeAppUpdates(); } diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 85c97b632..8b3418422 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -1,4 +1,5 @@ import { + cloudSigninToken, currentDatabase, currentTheme, emptyConnectionGroupNames, @@ -662,6 +663,15 @@ if (hasPermission('settings/change')) { }); } +registerCommand({ + id: 'cloud.logout', + category: 'Cloud', + name: 'Logout', + onClick: () => { + cloudSigninToken.set(null); + }, +}); + registerCommand({ id: 'file.exit', category: 'File', diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 1dc0f9e98..22dc64d7b 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -39,6 +39,7 @@ 'icon minus-thick': 'mdi mdi-minus-thick', 'icon invisible-box': 'mdi mdi-minus-box-outline icon-invisible', 'icon cloud-upload': 'mdi mdi-cloud-upload', + 'icon cloud': 'mdi mdi-cloud', 'icon import': 'mdi mdi-application-import', 'icon export': 'mdi mdi-application-export', 'icon new-connection': 'mdi mdi-database-plus', diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index dac8c4f02..c79e0de7f 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -182,6 +182,8 @@ export const focusedConnectionOrDatabase = writable<{ conid: string; database?: export const focusedTreeDbKey = writable<{ key: string; root: string; type: string; text: string }>(null); +export const cloudSigninToken = writableWithStorage(null, 'cloudSigninToken'); + export const DEFAULT_OBJECT_SEARCH_SETTINGS = { pureName: true, schemaName: false, diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index bfe827cfe..0314cb047 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -14,6 +14,7 @@ import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache'; import { isAdminPage, isOneOfPage } from './pageDefs'; import { openWebLink } from './simpleTools'; import { serializeJsTypesReplacer } from 'dbgate-tools'; +import { cloudSigninToken } from '../stores'; export const strmid = uuidv1(); @@ -279,6 +280,12 @@ export function installNewVolatileConnectionListener() { }); } +export function installNewCloudTokenListener() { + apiOn('got-cloud-token', async ({ token }) => { + cloudSigninToken.set(token); + }); +} + export function getAuthCategory(config) { if (config.isBasicAuth) { return 'basic'; diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte index 0245a5f28..778f5eb8f 100644 --- a/packages/web/src/widgets/WidgetIconPanel.svelte +++ b/packages/web/src/widgets/WidgetIconPanel.svelte @@ -9,6 +9,7 @@ visibleHamburgerMenuWidget, lockedDatabaseMode, getCurrentConfig, + cloudSigninToken, } from '../stores'; import mainMenuDefinition from '../../../../app/src/mainMenuDefinition'; import hasPermission from '../utility/hasPermission'; @@ -18,6 +19,7 @@ import getElectron from '../utility/getElectron'; let domSettings; + let domCloudAccount; let domMainMenu; const widgets = [ @@ -61,9 +63,10 @@ title: 'Selected cell data detail view', }, { - icon: 'icon app', - name: 'app', - title: 'Application layers', + icon: 'icon cloud', + name: 'cloud', + title: 'DbGate Cloud', + isCloud: true, }, { icon: 'icon premium', @@ -95,7 +98,26 @@ const rect = domSettings.getBoundingClientRect(); const left = rect.right; const top = rect.bottom; - const items = [{ command: 'settings.show' }, { command: 'theme.changeTheme' }, { command: 'settings.commands' }]; + const items = [ + { command: 'settings.show' }, + { command: 'theme.changeTheme' }, + { command: 'settings.commands' }, + { + text: 'View applications', + onClick: () => { + $selectedWidget = 'app'; + $visibleWidgetSideBar = true; + }, + }, + ]; + currentDropDownMenu.set({ left, top, items }); + } + + function handleCloudAccountMenu() { + const rect = domCloudAccount.getBoundingClientRect(); + const left = rect.right; + const top = rect.bottom; + const items = [{ command: 'cloud.logout' }]; currentDropDownMenu.set({ left, top, items }); } @@ -121,6 +143,7 @@ {/if} {#each widgets .filter(x => x && hasPermission(`widgets/${x.name}`)) + .filter(x => !x.isCloud || $cloudSigninToken) .filter(x => !x.isPremiumPromo || !isProApp()) as item}
--> -
- -
+ {#if $cloudSigninToken} +
+ +
+ {:else} +
+ +
+ {/if}
From a50f223fe3785b5b56741e850c72b66e9a558e9a Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 13:54:08 +0200 Subject: [PATCH 03/39] cloud icons WIP --- packages/web/src/utility/simpleTools.ts | 33 +++++++++++++++++-- .../web/src/widgets/WidgetIconPanel.svelte | 2 +- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/web/src/utility/simpleTools.ts b/packages/web/src/utility/simpleTools.ts index f655445ab..77164e211 100644 --- a/packages/web/src/utility/simpleTools.ts +++ b/packages/web/src/utility/simpleTools.ts @@ -1,11 +1,40 @@ import getElectron from './getElectron'; -export function openWebLink(href) { +export function openWebLink(href, usePopup = false) { const electron = getElectron(); if (electron) { electron.send('open-link', href); } else { - window.open(href, '_blank'); + if (usePopup) { + const w = 500; + const h = 650; + + const dualScreenLeft = window.screenLeft ?? window.screenX; // X of parent + const dualScreenTop = window.screenTop ?? window.screenY; // Y of parent + + // 2. How big is the parent window? + const parentWidth = window.outerWidth; + const parentHeight = window.outerHeight; + + // 3. Centre the popup inside that rectangle + const left = dualScreenLeft + (parentWidth - w) / 2; + const top = dualScreenTop + (parentHeight - h) / 2; + + const features = [ + `width=${w}`, + `height=${h}`, + `left=${left}`, + `top=${top}`, + 'scrollbars=yes', + 'resizable=yes', + 'noopener', + 'noreferrer', + ]; + + window.open(href, 'dbgateCloudLoginPopup', features.join(',')); + } else { + window.open(href, '_blank'); + } } } diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte index 778f5eb8f..16a7aaaf9 100644 --- a/packages/web/src/widgets/WidgetIconPanel.svelte +++ b/packages/web/src/widgets/WidgetIconPanel.svelte @@ -131,7 +131,7 @@ async function handleOpenCloudLogin() { const { url, sid } = await apiCall('auth/create-cloud-login-session', { client: getElectron() ? 'app' : 'web' }); - openWebLink(url); + openWebLink(url, true); } From 281de5196e771951f192dcea127c69aa2dff3641 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 19 May 2025 10:39:35 +0200 Subject: [PATCH 04/39] update cloud files --- packages/api/src/controllers/connections.js | 13 +++ packages/api/src/controllers/storage.js | 4 + packages/api/src/main.js | 3 + packages/api/src/utility/cloudIntf.js | 103 +++++++++++++++++++- 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 13a452a64..e221e8411 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -239,6 +239,19 @@ module.exports = { return (await this.datastore.find()).filter(x => connectionHasPermission(x, req)); }, + async getUsedEngines() { + const storage = require('./storage'); + + const storageEngines = await storage.getUsedEngines(); + if (storageEngines) { + return storageEngines; + } + if (portalConnections) { + return _.uniq(_.compact(portalConnections.map(x => x.engine))); + } + return _.uniq((await this.datastore.find()).map(x => x.engine)); + }, + test_meta: true, test({ connection, requestDbList = false }) { const subprocess = fork( diff --git a/packages/api/src/controllers/storage.js b/packages/api/src/controllers/storage.js index 6d498f869..f7066eb22 100644 --- a/packages/api/src/controllers/storage.js +++ b/packages/api/src/controllers/storage.js @@ -32,4 +32,8 @@ module.exports = { }, startRefreshLicense() {}, + + async getUsedEngines() { + return null; + }, }; diff --git a/packages/api/src/main.js b/packages/api/src/main.js index ac0c33ef5..3304d6a08 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -39,6 +39,7 @@ const { getDefaultAuthProvider } = require('./auth/authProvider'); const startCloudUpgradeTimer = require('./utility/cloudUpgrade'); const { isProApp } = require('./utility/checkLicense'); const { getHealthStatus, getHealthStatusSprinx } = require('./utility/healthStatus'); +const { startCloudFiles } = require('./utility/cloudIntf'); const logger = getLogger('main'); @@ -200,6 +201,8 @@ function start() { if (process.env.CLOUD_UPGRADE_FILE) { startCloudUpgradeTimer(); } + + startCloudFiles(); } function useAllControllers(app, electron) { diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index ee2b8301f..9d56f8edf 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -1,8 +1,16 @@ const axios = require('axios'); +const fs = require('fs-extra'); +const _ = require('lodash'); +const path = require('path'); const { getExternalParamsWithLicense } = require('./authProxy'); -const { getLogger, extractErrorLogData } = require('dbgate-tools'); +const { getLogger, extractErrorLogData, jsonLinesParse } = require('dbgate-tools'); +const { datadir } = require('./directories'); +const platformInfo = require('./platformInfo'); +const connections = require('../controllers/connections'); +const { isProApp } = require('./checkLicense'); const logger = getLogger('cloudIntf'); +let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY ? 'http://localhost:3103' @@ -54,7 +62,100 @@ function startCloudTokenChecking(sid, callback) { }, 500); } +async function loadCloudFiles() { + try { + const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files.jsonl'), 'utf-8'); + const parsedJson = jsonLinesParse(fileContent); + cloudFiles = parsedJson; + } catch (err) { + cloudFiles = []; + } +} + +async function collectCloudFilesSearchTags() { + const res = []; + if (platformInfo.isElectron) { + res.push('app'); + } else { + res.push('web'); + } + if (platformInfo.isWindows) { + res.push('windows'); + } + if (platformInfo.isMac) { + res.push('mac'); + } + if (platformInfo.isLinux) { + res.push('linux'); + } + if (platformInfo.isAwsUbuntuLayout) { + res.push('aws'); + } + if (platformInfo.isAzureUbuntuLayout) { + res.push('azure'); + } + if (platformInfo.isSnap) { + res.push('snap'); + } + const engines = await connections.getUsedEngines(); + const engineTags = engines.map(engine => engine.split('@')[0]); + res.push(...engineTags); + + // team-premium and trials will return the same cloud files as premium - no need to check + res.push(isProApp() ? 'premium' : 'community'); + + return res; +} + +async function updateCloudFiles() { + let lastCloudFilesTags; + try { + const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files-tags.json'), 'utf-8'); + cloudFiles = JSON.parse(fileContent); + } catch (err) { + lastCloudFilesTags = []; + } + + let lastCheckedTm = 0; + if (_.isEqual(cloudFiles, lastCloudFilesTags) && cloudFiles.length > 0) { + lastCheckedTm = _.max(cloudFiles.map(x => x.modifiedTm)); + } + const tags = await collectCloudFilesSearchTags(); + + const resp = await axios.default.post( + `${DBGATE_CLOUD_URL}/public-cloud-updates`, + { + lastCheckedTm, + tags, + }, + getExternalParamsWithLicense() + ); + + const filesByPath = _.keyBy(cloudFiles, 'path'); + for(const file of resp.data) { + filesByPath[file.path] = file; + } + + cloudFiles = Object.values(filesByPath); + + await fs.writeFile( + path.join(datadir(), 'cloud-files.jsonl'), + cloudFiles.map(x => JSON.stringify(x)).join('\n') + ); + + await fs.writeFile( + path.join(datadir(), 'cloud-files-tags.json'), + JSON.stringify(tags) + ); +} + +async function startCloudFiles() { + await loadCloudFiles(); + await updateCloudFiles(); +} + module.exports = { createDbGateIdentitySession, startCloudTokenChecking, + startCloudFiles, }; From 310f8bf6f7e8a4d96550615518159b2495043610 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 19 May 2025 16:33:04 +0200 Subject: [PATCH 05/39] public cloud widget --- packages/api/src/controllers/cloud.js | 15 +++++ packages/api/src/main.js | 2 + packages/api/src/utility/authProxy.js | 10 ++-- packages/api/src/utility/cloudIntf.js | 58 +++++++++++-------- .../web/src/appobj/CloudFileAppObject.svelte | 29 ++++++++++ packages/web/src/icons/FontIcon.svelte | 2 + packages/web/src/utility/metadataLoaders.ts | 13 +++++ .../web/src/widgets/CloudItemsWidget.svelte | 25 ++++++++ .../web/src/widgets/WidgetContainer.svelte | 4 ++ .../web/src/widgets/WidgetIconPanel.svelte | 6 +- 10 files changed, 133 insertions(+), 31 deletions(-) create mode 100644 packages/api/src/controllers/cloud.js create mode 100644 packages/web/src/appobj/CloudFileAppObject.svelte create mode 100644 packages/web/src/widgets/CloudItemsWidget.svelte diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js new file mode 100644 index 000000000..3ecc8a3ae --- /dev/null +++ b/packages/api/src/controllers/cloud.js @@ -0,0 +1,15 @@ +const fs = require('fs-extra'); +const _ = require('lodash'); +const path = require('path'); +const { appdir } = require('../utility/directories'); +const socket = require('../utility/socket'); +const connections = require('./connections'); +const { getPublicCloudFiles } = require('../utility/cloudIntf'); + +module.exports = { + publicFiles_meta: true, + async publicFiles() { + const res = await getPublicCloudFiles(); + return res; + }, +}; diff --git a/packages/api/src/main.js b/packages/api/src/main.js index 3304d6a08..571593bd1 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -27,6 +27,7 @@ const plugins = require('./controllers/plugins'); const files = require('./controllers/files'); const scheduler = require('./controllers/scheduler'); const queryHistory = require('./controllers/queryHistory'); +const cloud = require('./controllers/cloud'); const onFinished = require('on-finished'); const processArgs = require('./utility/processArgs'); @@ -223,6 +224,7 @@ function useAllControllers(app, electron) { useController(app, electron, '/query-history', queryHistory); useController(app, electron, '/apps', apps); useController(app, electron, '/auth', auth); + useController(app, electron, '/cloud', cloud); } function setElectronSender(electronSender) { diff --git a/packages/api/src/utility/authProxy.js b/packages/api/src/utility/authProxy.js index 744536177..913c8c82f 100644 --- a/packages/api/src/utility/authProxy.js +++ b/packages/api/src/utility/authProxy.js @@ -36,11 +36,13 @@ async function callRefactorSqlQueryApi(query, task, structure, dialect) { return null; } -function getExternalParamsWithLicense() { +function getExternalParamsWithLicense(isPost = false) { return { - headers: { - 'Content-Type': 'application/json', - }, + headers: isPost + ? { + 'Content-Type': 'application/json', + } + : {}, }; } diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 9d56f8edf..b39fab441 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -8,8 +8,10 @@ const { datadir } = require('./directories'); const platformInfo = require('./platformInfo'); const connections = require('../controllers/connections'); const { isProApp } = require('./checkLicense'); +const socket = require('./socket'); const logger = getLogger('cloudIntf'); + let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY @@ -30,7 +32,7 @@ async function createDbGateIdentitySession(client) { { client, }, - getExternalParamsWithLicense() + getExternalParamsWithLicense(true) ); return { sid: resp.data.sid, @@ -49,7 +51,7 @@ function startCloudTokenChecking(sid, callback) { try { const resp = await axios.default.get( `${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, - getExternalParamsWithLicense() + getExternalParamsWithLicense(false) ); if (resp.data.status == 'ok') { @@ -110,52 +112,58 @@ async function collectCloudFilesSearchTags() { async function updateCloudFiles() { let lastCloudFilesTags; try { - const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files-tags.json'), 'utf-8'); - cloudFiles = JSON.parse(fileContent); + lastCloudFilesTags = await fs.readFile(path.join(datadir(), 'cloud-files-tags.txt'), 'utf-8'); } catch (err) { - lastCloudFilesTags = []; + lastCloudFilesTags = ''; } + const tags = (await collectCloudFilesSearchTags()).join(','); let lastCheckedTm = 0; - if (_.isEqual(cloudFiles, lastCloudFilesTags) && cloudFiles.length > 0) { - lastCheckedTm = _.max(cloudFiles.map(x => x.modifiedTm)); + if (tags == lastCloudFilesTags && cloudFiles.length > 0) { + lastCheckedTm = _.max(cloudFiles.map(x => parseInt(x.modifiedTm))); } - const tags = await collectCloudFilesSearchTags(); - const resp = await axios.default.post( - `${DBGATE_CLOUD_URL}/public-cloud-updates`, - { - lastCheckedTm, - tags, - }, - getExternalParamsWithLicense() + logger.info({ tags, lastCheckedTm }, 'Downloading cloud files'); + + const resp = await axios.default.get( + `${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}`, + getExternalParamsWithLicense(false) ); + logger.info(`Downloaded ${resp.data.length} cloud files`); + const filesByPath = _.keyBy(cloudFiles, 'path'); - for(const file of resp.data) { + for (const file of resp.data) { filesByPath[file.path] = file; } cloudFiles = Object.values(filesByPath); - await fs.writeFile( - path.join(datadir(), 'cloud-files.jsonl'), - cloudFiles.map(x => JSON.stringify(x)).join('\n') - ); + await fs.writeFile(path.join(datadir(), 'cloud-files.jsonl'), cloudFiles.map(x => JSON.stringify(x)).join('\n')); + await fs.writeFile(path.join(datadir(), 'cloud-files-tags.txt'), tags); - await fs.writeFile( - path.join(datadir(), 'cloud-files-tags.json'), - JSON.stringify(tags) - ); + socket.emitChanged(`public-cloud-changed`); } async function startCloudFiles() { await loadCloudFiles(); - await updateCloudFiles(); + try { + await updateCloudFiles(); + } catch (err) { + logger.error(extractErrorLogData(err), 'Error updating cloud files'); + } +} + +async function getPublicCloudFiles() { + if (!loadCloudFiles) { + await loadCloudFiles(); + } + return cloudFiles; } module.exports = { createDbGateIdentitySession, startCloudTokenChecking, startCloudFiles, + getPublicCloudFiles, }; diff --git a/packages/web/src/appobj/CloudFileAppObject.svelte b/packages/web/src/appobj/CloudFileAppObject.svelte new file mode 100644 index 000000000..de547b2a2 --- /dev/null +++ b/packages/web/src/appobj/CloudFileAppObject.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 22dc64d7b..babcd45ed 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -40,6 +40,8 @@ 'icon invisible-box': 'mdi mdi-minus-box-outline icon-invisible', 'icon cloud-upload': 'mdi mdi-cloud-upload', 'icon cloud': 'mdi mdi-cloud', + 'icon cloud-public': 'mdi mdi-cloud-search', + 'icon cloud-logged': 'mdi mdi-cloud-key', 'icon import': 'mdi mdi-application-import', 'icon export': 'mdi mdi-application-export', 'icon new-connection': 'mdi mdi-database-plus', diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts index 2fc374a02..fd3fd2a21 100644 --- a/packages/web/src/utility/metadataLoaders.ts +++ b/packages/web/src/utility/metadataLoaders.ts @@ -166,6 +166,12 @@ const authTypesLoader = ({ engine }) => ({ errorValue: null, }); +const publicCloudFilesLoader = () => ({ + url: 'cloud/public-files', + params: {}, + reloadTrigger: { key: `public-cloud-changed` }, +}); + async function getCore(loader, args) { const { url, params, reloadTrigger, transform, onLoaded, errorValue } = loader(args); const key = stableStringify({ url, ...params }); @@ -456,3 +462,10 @@ export function getSchemaList(args) { export function useSchemaList(args) { return useCore(schemaListLoader, args); } + +export function getPublicCloudFiles(args) { + return getCore(publicCloudFilesLoader, args); +} +export function usePublicCloudFiles(args = {}) { + return useCore(publicCloudFilesLoader, args); +} diff --git a/packages/web/src/widgets/CloudItemsWidget.svelte b/packages/web/src/widgets/CloudItemsWidget.svelte new file mode 100644 index 000000000..6c6a2036e --- /dev/null +++ b/packages/web/src/widgets/CloudItemsWidget.svelte @@ -0,0 +1,25 @@ + + + + + + data.folder} /> + + + + + diff --git a/packages/web/src/widgets/WidgetContainer.svelte b/packages/web/src/widgets/WidgetContainer.svelte index 814a60c96..38c1de040 100644 --- a/packages/web/src/widgets/WidgetContainer.svelte +++ b/packages/web/src/widgets/WidgetContainer.svelte @@ -9,6 +9,7 @@ import AppWidget from './AppWidget.svelte'; import AdminMenuWidget from './AdminMenuWidget.svelte'; import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte'; + import CloudItemsWidget from './CloudItemsWidget.svelte';
{/if} - {#if $currentArchive} + {#if $currentArchive && $currentArchive != 'default'}
{/if} + {#if $cloudSigninTokenHolder?.email} +
+ + {$cloudSigninTokenHolder?.email} +
+ {/if} + {#if $appUpdateStatus}
From cc930a3ff92fd079d9c5a3ea249edc818970857e Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 15:50:48 +0200 Subject: [PATCH 27/39] cloud connections expansion fix --- packages/web/src/appobj/CloudContentAppObject.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/web/src/appobj/CloudContentAppObject.svelte b/packages/web/src/appobj/CloudContentAppObject.svelte index 656626057..8ed50b909 100644 --- a/packages/web/src/appobj/CloudContentAppObject.svelte +++ b/packages/web/src/appobj/CloudContentAppObject.svelte @@ -43,6 +43,8 @@ ...$cloudConnectionsStore[data.conid], status: data.status, }} + on:dblclick + on:expand /> {:else} {/if} From afde0a742390606457a4536305ff23bb8ec36647 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 16:46:04 +0200 Subject: [PATCH 28/39] cloud connection save --- packages/api/src/controllers/cloud.js | 20 +++++++- packages/api/src/proc/connectProcess.js | 9 +--- packages/api/src/utility/cloudIntf.js | 8 +++- packages/web/src/tabs/ConnectionTab.svelte | 48 ++++++++++++++----- .../web/src/widgets/PrivateCloudWidget.svelte | 27 ++++++++--- 5 files changed, 83 insertions(+), 29 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index 50cfd61c5..c5db7fb37 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -7,6 +7,7 @@ const { getCloudFolderEncryptor, getCloudContent, putCloudContent, + removeCloudCachedConnection, } = require('../utility/cloudIntf'); const connections = require('./connections'); const socket = require('../utility/socket'); @@ -128,17 +129,32 @@ module.exports = { saveConnection_meta: true, async saveConnection({ folid, connection }) { + let cntid = undefined; + if (connection._id) { + const m = connection._id.match(/^cloud\:\/\/(.+)\/(.+)$/); + if (!m) { + throw new Error('Invalid cloud connection ID format'); + } + folid = m[1]; + cntid = m[2]; + } + + if (!folid) { + throw new Error('Missing cloud folder ID'); + } + const folderEncryptor = await getCloudFolderEncryptor(folid); const recryptedConn = encryptConnection(connection, folderEncryptor); const resp = await putCloudContent( folid, - undefined, + cntid, JSON.stringify(recryptedConn), getConnectionLabel(recryptedConn), 'connection' ); - const { cntid } = resp; + removeCloudCachedConnection(folid, resp.cntid); + cntid = resp.cntid; socket.emitChanged('cloud-content-changed'); return { ...recryptedConn, diff --git a/packages/api/src/proc/connectProcess.js b/packages/api/src/proc/connectProcess.js index 6d375c596..58d2ac454 100644 --- a/packages/api/src/proc/connectProcess.js +++ b/packages/api/src/proc/connectProcess.js @@ -28,14 +28,7 @@ function start() { let version = { version: 'Unknown', }; - try { - version = await driver.getVersion(dbhan); - } catch (err) { - logger.error(extractErrorLogData(err), 'Error getting DB server version'); - version = { - version: 'Unknown', - }; - } + version = await driver.getVersion(dbhan); let databases = undefined; if (requestDbList) { databases = await driver.listDatabases(dbhan); diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index a12980bf6..d1dfbdc7d 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -271,7 +271,7 @@ async function callCloudApiPost(endpoint, body, signinHolder = null) { async function getCloudFolderEncryptor(folid) { const { encryptionKey } = await callCloudApiGet(`folder-key/${folid}`); if (!encryptionKey) { - throw new Error('No encryption key'); + throw new Error('No encryption key for folder: ' + folid); } return simpleEncryptor.createEncryptor(encryptionKey); } @@ -336,6 +336,11 @@ async function loadCachedCloudConnection(folid, cntid) { return cloudConnectionCache[cacheKey]; } +function removeCloudCachedConnection(folid, cntid) { + const cacheKey = `${folid}|${cntid}`; + delete cloudConnectionCache[cacheKey]; +} + module.exports = { createDbGateIdentitySession, startCloudTokenChecking, @@ -349,4 +354,5 @@ module.exports = { getCloudContent, loadCachedCloudConnection, putCloudContent, + removeCloudCachedConnection, }; diff --git a/packages/web/src/tabs/ConnectionTab.svelte b/packages/web/src/tabs/ConnectionTab.svelte index ebc5d2fa3..5df80d76f 100644 --- a/packages/web/src/tabs/ConnectionTab.svelte +++ b/packages/web/src/tabs/ConnectionTab.svelte @@ -159,7 +159,7 @@ $: currentConnection = getCurrentConnectionCore($values, driver); async function handleSave() { - if (saveOnCloud) { + if (saveOnCloud && !getCurrentConnection()?._id) { showModal(ChooseCloudFolderModal, { requiredRoleVariants: ['write', 'admin'], message: 'Choose cloud folder to saved connection', @@ -184,6 +184,17 @@ } }, }); + } else if ( + // @ts-ignore + getCurrentConnection()?._id?.startsWith('cloud://') + ) { + let connection = getCurrentConnection(); + await apiCall('cloud/save-connection', { connection }); + showSnackbarSuccess('Connection saved'); + changeTab(tabid, tab => ({ + ...tab, + title: getConnectionLabel(connection), + })); } else { let connection = getCurrentConnection(); connection = { @@ -210,19 +221,32 @@ async function handleConnect() { let connection = getCurrentConnection(); - if (!connection._id) { - connection = { - ...connection, - unsaved: true, + + if ( + // @ts-ignore + connection?._id?.startsWith('cloud://') + ) { + const saved = await apiCall('cloud/save-connection', { connection }); + changeTab(tabid, tab => ({ + ...tab, + title: getConnectionLabel(connection), + })); + openConnection(saved); + } else { + if (!connection._id) { + connection = { + ...connection, + unsaved: true, + }; + } + const saved = await apiCall('connections/save', connection); + $values = { + ...$values, + unsaved: connection.unsaved, + _id: saved._id, }; + openConnection(saved); } - const saved = await apiCall('connections/save', connection); - $values = { - ...$values, - unsaved: connection.unsaved, - _id: saved._id, - }; - openConnection(saved); // closeMultipleTabs(x => x.tabid == tabid, true); } diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index e67dcb5c6..49d2c01e9 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -20,6 +20,7 @@ currentDatabase, expandedConnections, openedConnections, + openedSingleDatabaseConnections, } from '../stores'; import _ from 'lodash'; import { plusExpandIcon } from '../icons/expandIcons'; @@ -74,12 +75,26 @@ }; } - onMount(() => { - const currentConid = $currentDatabase?.connection?._id; - if (currentConid?.startsWith('cloud://') && !$cloudConnectionsStore[currentConid]) { - loadCloudConnection(currentConid); - } - }); + function ensureCloudConnectionsLoaded(...conids) { + _.uniq(conids).forEach(conid => { + if (conid?.startsWith('cloud://') && !$cloudConnectionsStore[conid]) { + loadCloudConnection(conid); + } + }); + } + + $: ensureCloudConnectionsLoaded( + $currentDatabase?.connection?._id, + ...$openedSingleDatabaseConnections, + ...$openedConnections + ); + + // onMount(() => { + // const currentConid = $currentDatabase?.connection?._id; + // if (currentConid?.startsWith('cloud://') && !$cloudConnectionsStore[currentConid]) { + // loadCloudConnection(currentConid); + // } + // }); function createAddMenu() { return [ From d26db7096d4284f5631ddfca90aced8f405fd7df Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 17:02:09 +0200 Subject: [PATCH 29/39] refactor - handle cloud listeners --- packages/api/src/controllers/cloud.js | 7 +++ packages/api/src/utility/cloudIntf.js | 1 + packages/web/src/App.svelte | 2 + packages/web/src/stores.ts | 6 +++ packages/web/src/utility/cloudListeners.ts | 48 +++++++++++++++++++ .../web/src/widgets/PrivateCloudWidget.svelte | 38 +++++++-------- 6 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 packages/web/src/utility/cloudListeners.ts diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index c5db7fb37..4888cc826 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -59,6 +59,7 @@ module.exports = { async putContent({ folid, cntid, content, name, type }) { const resp = await putCloudContent(folid, cntid, content, name, type); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -66,6 +67,7 @@ module.exports = { async createFolder({ name }) { const resp = await callCloudApiPost(`folders/create`, { name }); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -80,6 +82,7 @@ module.exports = { const resp = await callCloudApiPost(`folders/grant/${mode}`, { invite }); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -87,6 +90,7 @@ module.exports = { async renameFolder({ folid, name }) { const resp = await callCloudApiPost(`folders/rename`, { folid, name }); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -94,6 +98,7 @@ module.exports = { async deleteFolder({ folid }) { const resp = await callCloudApiPost(`folders/delete`, { folid }); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -106,6 +111,7 @@ module.exports = { refreshContent_meta: true, async refreshContent() { socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return { status: 'ok', }; @@ -156,6 +162,7 @@ module.exports = { removeCloudCachedConnection(folid, resp.cntid); cntid = resp.cntid; socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return { ...recryptedConn, _id: `cloud://${folid}/${cntid}`, diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index d1dfbdc7d..6e845b212 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -320,6 +320,7 @@ async function putCloudContent(folid, cntid, content, name, type) { signinHolder ); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; } diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index e5acac607..124be6c23 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -28,6 +28,7 @@ import { handleAuthOnStartup } from './clientAuth'; import { initializeAppUpdates } from './utility/appUpdate'; import { _t } from './translations'; + import { installCloudListeners } from './utility/cloudListeners'; export let isAdminPage = false; @@ -58,6 +59,7 @@ installNewVolatileConnectionListener(); installNewCloudTokenListener(); initializeAppUpdates(); + installCloudListeners(); } refreshPublicCloudFiles(); diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index 1de2cdc71..5dfdb4c99 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -457,4 +457,10 @@ focusedTreeDbKey.subscribe(value => { }); export const getFocusedTreeDbKey = () => focusedTreeDbKeyValue; +let cloudConnectionsStoreValue = {}; +cloudConnectionsStore.subscribe(value => { + cloudConnectionsStoreValue = value; +}); +export const getCloudConnectionsStore = () => cloudConnectionsStoreValue; + window['__changeCurrentTheme'] = theme => currentTheme.set(theme); diff --git a/packages/web/src/utility/cloudListeners.ts b/packages/web/src/utility/cloudListeners.ts new file mode 100644 index 000000000..501cc2f65 --- /dev/null +++ b/packages/web/src/utility/cloudListeners.ts @@ -0,0 +1,48 @@ +import { derived } from 'svelte/store'; +import { + cloudConnectionsStore, + currentDatabase, + getCloudConnectionsStore, + openedConnections, + openedSingleDatabaseConnections, +} from '../stores'; +import { apiCall, apiOn } from './api'; +import _ from 'lodash'; + +export const possibleCloudConnectionSources = derived( + [currentDatabase, openedSingleDatabaseConnections, openedConnections], + ([$currentDatabase, $openedSingleDatabaseConnections, $openedConnections]) => { + const conids = new Set(); + if ($currentDatabase?.connection?._id) { + conids.add($currentDatabase.connection._id); + } + $openedSingleDatabaseConnections.forEach(x => conids.add(x)); + $openedConnections.forEach(x => conids.add(x)); + return Array.from(conids).filter(x => x?.startsWith('cloud://')); + } +); + +async function loadCloudConnection(conid) { + const conn = await apiCall('connections/get', { conid }); + cloudConnectionsStore.update(store => ({ + ...store, + [conid]: conn, + })); +} + +function ensureCloudConnectionsLoaded(...conids) { + const conns = getCloudConnectionsStore(); + _.uniq(conids).forEach(conid => { + if (!conns[conid]) { + loadCloudConnection(conid); + } + }); +} + +export function installCloudListeners() { + possibleCloudConnectionSources.subscribe(conids => { + ensureCloudConnectionsLoaded(...conids); + }); + + apiOn('cloud-content-updated', () => cloudConnectionsStore.set({})); +} diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index 49d2c01e9..5b6cf38c0 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -67,27 +67,27 @@ await apiCall('cloud/refresh-content'); } - async function loadCloudConnection(conid) { - const conn = await apiCall('connections/get', { conid }); - $cloudConnectionsStore = { - ...$cloudConnectionsStore, - [conid]: conn, - }; - } + // async function loadCloudConnection(conid) { + // const conn = await apiCall('connections/get', { conid }); + // $cloudConnectionsStore = { + // ...$cloudConnectionsStore, + // [conid]: conn, + // }; + // } - function ensureCloudConnectionsLoaded(...conids) { - _.uniq(conids).forEach(conid => { - if (conid?.startsWith('cloud://') && !$cloudConnectionsStore[conid]) { - loadCloudConnection(conid); - } - }); - } + // function ensureCloudConnectionsLoaded(...conids) { + // _.uniq(conids).forEach(conid => { + // if (conid?.startsWith('cloud://') && !$cloudConnectionsStore[conid]) { + // loadCloudConnection(conid); + // } + // }); + // } - $: ensureCloudConnectionsLoaded( - $currentDatabase?.connection?._id, - ...$openedSingleDatabaseConnections, - ...$openedConnections - ); + // $: ensureCloudConnectionsLoaded( + // $currentDatabase?.connection?._id, + // ...$openedSingleDatabaseConnections, + // ...$openedConnections + // ); // onMount(() => { // const currentConid = $currentDatabase?.connection?._id; From f94bf3f8ce9efd082c62873e7c48771a45894467 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 17:24:13 +0200 Subject: [PATCH 30/39] cloud fixes --- packages/api/src/controllers/cloud.js | 4 ++++ packages/web/src/tabs/ConnectionTab.svelte | 14 ++++++++------ packages/web/src/widgets/PrivateCloudWidget.svelte | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index 4888cc826..b1e92ae4d 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -159,6 +159,10 @@ module.exports = { 'connection' ); + if (resp.apiErrorMessage) { + return resp; + } + removeCloudCachedConnection(folid, resp.cntid); cntid = resp.cntid; socket.emitChanged('cloud-content-changed'); diff --git a/packages/web/src/tabs/ConnectionTab.svelte b/packages/web/src/tabs/ConnectionTab.svelte index 5df80d76f..d15209e07 100644 --- a/packages/web/src/tabs/ConnectionTab.svelte +++ b/packages/web/src/tabs/ConnectionTab.svelte @@ -189,12 +189,14 @@ getCurrentConnection()?._id?.startsWith('cloud://') ) { let connection = getCurrentConnection(); - await apiCall('cloud/save-connection', { connection }); - showSnackbarSuccess('Connection saved'); - changeTab(tabid, tab => ({ - ...tab, - title: getConnectionLabel(connection), - })); + const resp = await apiCall('cloud/save-connection', { connection }); + if (resp?._id) { + showSnackbarSuccess('Connection saved'); + changeTab(tabid, tab => ({ + ...tab, + title: getConnectionLabel(connection), + })); + } } else { let connection = getCurrentConnection(); connection = { diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index 5b6cf38c0..6527af042 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -222,7 +222,7 @@ module={cloudContentAppObject} emptyGroupNames={emptyCloudContent} groupFunc={data => data.folid} - mapGroupTitle={folid => contentGroupMap[folid]?.name} + mapGroupTitle={folid => `${contentGroupMap[folid]?.name} - ${contentGroupMap[folid]?.role}`} filter={publicFilter} subItemsComponent={() => SubCloudItemsList} expandIconFunc={plusExpandIcon} From 74560c3289cf1a3152c66afa562933c5c3423695 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 17:59:03 +0200 Subject: [PATCH 31/39] duplicate cloud connection --- packages/api/src/controllers/cloud.js | 18 +++++++++ .../src/appobj/CloudContentAppObject.svelte | 38 +++++++++++++++---- .../web/src/appobj/ConnectionAppObject.svelte | 4 +- packages/web/src/utility/cloudListeners.ts | 5 ++- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index b1e92ae4d..775582539 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -172,4 +172,22 @@ module.exports = { _id: `cloud://${folid}/${cntid}`, }; }, + + duplicateConnection_meta: true, + async duplicateConnection({ conid }) { + const m = conid.match(/^cloud\:\/\/(.+)\/(.+)$/); + if (!m) { + throw new Error('Invalid cloud connection ID format'); + } + const folid = m[1]; + const cntid = m[2]; + const respGet = await getCloudContent(folid, cntid); + const conn = JSON.parse(respGet.content); + const conn2 = { + ...conn, + displayName: getConnectionLabel(conn) + ' - copy', + }; + const respPut = await putCloudContent(folid, undefined, JSON.stringify(conn2), conn2.displayName, 'connection'); + return respPut; + }, }; diff --git a/packages/web/src/appobj/CloudContentAppObject.svelte b/packages/web/src/appobj/CloudContentAppObject.svelte index 8ed50b909..b2a915b6f 100644 --- a/packages/web/src/appobj/CloudContentAppObject.svelte +++ b/packages/web/src/appobj/CloudContentAppObject.svelte @@ -13,23 +13,47 @@ - + Save file + {#if $cloudSigninTokenHolder} + + {/if} + {#if electron} @@ -79,4 +116,4 @@ {/if} - + From 741b942dea3402f11fca2b2209f0e93add7a8f70 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 08:25:10 +0200 Subject: [PATCH 33/39] cloud files WIP --- packages/api/src/controllers/cloud.js | 9 ++- packages/api/src/utility/cloudIntf.js | 20 ++++-- .../src/appobj/CloudContentAppObject.svelte | 14 ++++ .../web/src/appobj/SavedFileAppObject.svelte | 17 ++++- .../src/forms/FormCloudFolderSelect.svelte | 16 +++-- packages/web/src/modals/SaveFileModal.svelte | 70 ++++++++++--------- .../web/src/widgets/PrivateCloudWidget.svelte | 9 ++- .../web/src/widgets/PublicCloudWidget.svelte | 8 +-- 8 files changed, 109 insertions(+), 54 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index 821470add..37fd9856f 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -205,6 +205,11 @@ module.exports = { return resp; }, - // saveFile_meta: true, - // async saveFile({folid, file, data, folder, format}) + saveFile_meta: true, + async saveFile({ folid, cntid, fileName, data, contentFolder, format }) { + const resp = await putCloudContent(folid, cntid, data, fileName, 'file', contentFolder, format); + socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); + return resp; + }, }; diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 6e845b212..da67425b8 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -284,9 +284,13 @@ async function getCloudContent(folid, cntid) { const encryptor = simpleEncryptor.createEncryptor(signinHolder.encryptionKey); - const { content, name, type, apiErrorMessage } = await callCloudApiGet(`content/${folid}/${cntid}`, signinHolder, { - 'x-kehid': signinHolder.kehid, - }); + const { content, name, type, contentFolder, contentType, apiErrorMessage } = await callCloudApiGet( + `content/${folid}/${cntid}`, + signinHolder, + { + 'x-kehid': signinHolder.kehid, + } + ); if (apiErrorMessage) { return { apiErrorMessage }; @@ -296,10 +300,16 @@ async function getCloudContent(folid, cntid) { content: encryptor.decrypt(content), name, type, + contentFolder, + contentType, }; } -async function putCloudContent(folid, cntid, content, name, type) { +/** + * + * @returns Promise<{ cntid: string } | { apiErrorMessage: string }> + */ +async function putCloudContent(folid, cntid, content, name, type, contentFolder = null, contentType = null) { const signinHolder = await getCloudSigninHolder(); if (!signinHolder) { throw new Error('No signed in'); @@ -316,6 +326,8 @@ async function putCloudContent(folid, cntid, content, name, type) { type, kehid: signinHolder.kehid, content: encryptor.encrypt(content), + contentFolder, + contentType, }, signinHolder ); diff --git a/packages/web/src/appobj/CloudContentAppObject.svelte b/packages/web/src/appobj/CloudContentAppObject.svelte index aaff32814..819949e6e 100644 --- a/packages/web/src/appobj/CloudContentAppObject.svelte +++ b/packages/web/src/appobj/CloudContentAppObject.svelte @@ -17,6 +17,7 @@ import openNewTab from '../utility/openNewTab'; import { showModal } from '../modals/modalTools'; import ConfirmModal from '../modals/ConfirmModal.svelte'; + import SavedFileAppObject from './SavedFileAppObject.svelte'; export let data; export let passProps; @@ -102,6 +103,19 @@ on:dblclick on:expand /> +{:else if data.type == 'file'} + {:else} diff --git a/packages/web/src/forms/FormCloudFolderSelect.svelte b/packages/web/src/forms/FormCloudFolderSelect.svelte index a6c215743..28d5bf08f 100644 --- a/packages/web/src/forms/FormCloudFolderSelect.svelte +++ b/packages/web/src/forms/FormCloudFolderSelect.svelte @@ -6,14 +6,22 @@ export let name; export let requiredRoleVariants = ['read', 'write', 'admin']; + export let prependFolders = []; + const cloudContentList = useCloudContentList(); - $: folderOptions = ($cloudContentList || []) - .filter(folder => requiredRoleVariants.find(role => folder.role == role)) - .map(folder => ({ + $: folderOptions = [ + ...prependFolders.map(folder => ({ value: folder.folid, label: folder.name, - })); + })), + ...($cloudContentList || []) + .filter(folder => requiredRoleVariants.find(role => folder.role == role)) + .map(folder => ({ + value: folder.folid, + label: folder.name, + })), + ]; diff --git a/packages/web/src/modals/SaveFileModal.svelte b/packages/web/src/modals/SaveFileModal.svelte index 16b9d7dd2..4665880fa 100644 --- a/packages/web/src/modals/SaveFileModal.svelte +++ b/packages/web/src/modals/SaveFileModal.svelte @@ -23,20 +23,42 @@ export let filePath; export let onSave = undefined; - const values = writable({ name }); + const values = writable({ name, cloudFolder: '__local' }); const electron = getElectron(); const handleSubmit = async e => { - const { name } = e.detail; - await apiCall('files/save', { folder, file: name, data, format }); - closeCurrentModal(); - if (onSave) { - onSave(name, { - savedFile: name, - savedFolder: folder, - savedFilePath: null, + const { name, cloudFolder } = e.detail; + if (cloudFolder === '__local') { + await apiCall('files/save', { folder, file: name, data, format }); + closeCurrentModal(); + if (onSave) { + onSave(name, { + savedFile: name, + savedFolder: folder, + savedFilePath: null, + }); + } + } else { + const resp = await apiCall('cloud/save-file', { + folid: cloudFolder, + fileName: name, + data, + contentFolder: folder, + format, }); + if (resp.cntid) { + closeCurrentModal(); + if (onSave) { + onSave(name, { + savedFile: name, + savedFolder: folder, + savedFilePath: null, + savedCloudFolderId: cloudFolder, + savedCloudContentId: resp.cntid, + }); + } + } } }; @@ -56,28 +78,6 @@ }); } }; - - const handleSaveToCloud = async folid => { - const resp = await apiCall('cloud/save-file', { - folid, - fileName: $values.name, - data, - contentFolder: folder, - format, - }); - if (resp.cntid) { - closeCurrentModal(); - if (onSave) { - onSave(name, { - savedFile: name, - savedFolder: folder, - savedFilePath: null, - savedCloudFolderId: folid, - savedCloudContentId: resp.cntid, - }); - } - } - }; @@ -86,10 +86,16 @@ {#if $cloudSigninTokenHolder} {/if} diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index 6527af042..8fdb35f5d 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -34,8 +34,7 @@ import ConfirmModal from '../modals/ConfirmModal.svelte'; import { showSnackbarInfo } from '../utility/snackbar'; - let publicFilter = ''; - let cloudFilter = ''; + let filter = ''; let domSqlObjectList = null; const cloudContentList = useCloudContentList(); @@ -205,8 +204,8 @@ skip={!$cloudSigninTokenHolder} > - - + + data.folid} mapGroupTitle={folid => `${contentGroupMap[folid]?.name} - ${contentGroupMap[folid]?.role}`} - filter={publicFilter} + {filter} subItemsComponent={() => SubCloudItemsList} expandIconFunc={plusExpandIcon} isExpandable={data => diff --git a/packages/web/src/widgets/PublicCloudWidget.svelte b/packages/web/src/widgets/PublicCloudWidget.svelte index e0383c94a..9b137f2fb 100644 --- a/packages/web/src/widgets/PublicCloudWidget.svelte +++ b/packages/web/src/widgets/PublicCloudWidget.svelte @@ -18,7 +18,7 @@ import FontIcon from '../icons/FontIcon.svelte'; import { apiCall } from '../utility/api'; import _ from 'lodash'; - let publicFilter = ''; + let filter = ''; const publicFiles = usePublicCloudFiles(); @@ -31,8 +31,8 @@ - - + + data.folder || undefined} - filter={publicFilter} + {filter} /> From 7b50a19b2c7648ec12185c2b7680acfb6ff35a4e Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 10:46:35 +0200 Subject: [PATCH 34/39] cloud file, folder operations --- packages/api/src/controllers/cloud.js | 35 +++++++++++++ .../web/src/appobj/ConnectionAppObject.svelte | 14 ++++-- .../web/src/appobj/SavedFileAppObject.svelte | 49 +++++++++++++++---- packages/web/src/modals/SaveFileModal.svelte | 12 +++-- packages/web/src/utility/cloudListeners.ts | 8 ++- packages/web/src/utility/openElectronFile.ts | 2 + packages/web/src/utility/openNewTab.ts | 7 ++- packages/web/src/utility/saveTabFile.ts | 33 ++++++++++--- .../web/src/widgets/PrivateCloudWidget.svelte | 31 ++++++------ 9 files changed, 150 insertions(+), 41 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index 37fd9856f..c2cc1af2d 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -15,6 +15,7 @@ const { recryptConnection, getInternalEncryptor, encryptConnection } = require(' const { getConnectionLabel, getLogger, extractErrorLogData } = require('dbgate-tools'); const logger = getLogger('cloud'); const _ = require('lodash'); +const fs = require('fs-extra'); module.exports = { publicFiles_meta: true, @@ -205,6 +206,22 @@ module.exports = { return resp; }, + deleteContent_meta: true, + async deleteContent({ folid, cntid }) { + const resp = await callCloudApiPost(`content/delete/${folid}/${cntid}`); + socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); + return resp; + }, + + renameContent_meta: true, + async renameContent({ folid, cntid, name }) { + const resp = await callCloudApiPost(`content/rename/${folid}/${cntid}`, { name }); + socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); + return resp; + }, + saveFile_meta: true, async saveFile({ folid, cntid, fileName, data, contentFolder, format }) { const resp = await putCloudContent(folid, cntid, data, fileName, 'file', contentFolder, format); @@ -212,4 +229,22 @@ module.exports = { socket.emit('cloud-content-updated'); return resp; }, + + copyFile_meta: true, + async copyFile({ folid, cntid, name }) { + const resp = await callCloudApiPost(`content/duplicate/${folid}/${cntid}`, { name }); + socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); + return resp; + }, + + exportFile_meta: true, + async exportFile({ folid, cntid, filePath }, req) { + const { content } = await getCloudContent(folid, cntid); + if (!content) { + throw new Error('File not found'); + } + await fs.writeFile(filePath, content); + return true; + }, }; diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte index 531bb0788..7ed4d83ff 100644 --- a/packages/web/src/appobj/ConnectionAppObject.svelte +++ b/packages/web/src/appobj/ConnectionAppObject.svelte @@ -262,11 +262,15 @@ }); }; const handleDuplicate = () => { - apiCall('connections/save', { - ...data, - _id: undefined, - displayName: `${getConnectionLabel(data)} - copy`, - }); + if (data._id.startsWith('cloud://')) { + apiCall('cloud/duplicate-connection', { conid: data._id }); + } else { + apiCall('connections/save', { + ...data, + _id: undefined, + displayName: `${getConnectionLabel(data)} - copy`, + }); + } }; const handleCreateDatabase = () => { showModal(InputTextModal, { diff --git a/packages/web/src/appobj/SavedFileAppObject.svelte b/packages/web/src/appobj/SavedFileAppObject.svelte index 89f2ce970..2cd5e70bc 100644 --- a/packages/web/src/appobj/SavedFileAppObject.svelte +++ b/packages/web/src/appobj/SavedFileAppObject.svelte @@ -206,7 +206,14 @@ showModal(ConfirmModal, { message: `Really delete file ${data.file}?`, onConfirm: () => { - apiCall('files/delete', data); + if (data.folid && data.cntid) { + apiCall('cloud/delete-content', { + folid: data.folid, + cntid: data.cntid, + }); + } else { + apiCall('files/delete', data); + } }, }); }; @@ -217,7 +224,15 @@ label: 'New file name', header: 'Rename file', onConfirm: newFile => { - apiCall('files/rename', { ...data, newFile }); + if (data.folid && data.cntid) { + apiCall('cloud/rename-content', { + folid: data.folid, + cntid: data.cntid, + name: newFile, + }); + } else { + apiCall('files/rename', { ...data, newFile }); + } }, }); }; @@ -226,9 +241,17 @@ showModal(InputTextModal, { value: data.file, label: 'New file name', - header: 'Rename file', + header: 'Copy file', onConfirm: newFile => { - apiCall('files/copy', { ...data, newFile }); + if (data.folid && data.cntid) { + apiCall('cloud/copy-file', { + folid: data.folid, + cntid: data.cntid, + name: newFile, + }); + } else { + apiCall('files/copy', { ...data, newFile }); + } }, }); }; @@ -236,11 +259,19 @@ const handleDownload = () => { saveFileToDisk( async filePath => { - await apiCall('files/export-file', { - folder, - file: data.file, - filePath, - }); + if (data.folid && data.cntid) { + await apiCall('cloud/export-file', { + folid: data.folid, + cntid: data.cntid, + filePath, + }); + } else { + await apiCall('files/export-file', { + folder, + file: data.file, + filePath, + }); + } }, { formatLabel: handler.label, formatExtension: handler.format, defaultFileName: data.file } ); diff --git a/packages/web/src/modals/SaveFileModal.svelte b/packages/web/src/modals/SaveFileModal.svelte index 4665880fa..15ebd67e1 100644 --- a/packages/web/src/modals/SaveFileModal.svelte +++ b/packages/web/src/modals/SaveFileModal.svelte @@ -10,7 +10,6 @@ import { writable } from 'svelte/store'; import getElectron from '../utility/getElectron'; - import ChooseCloudFolderModal from './ChooseCloudFolderModal.svelte'; import ModalBase from './ModalBase.svelte'; import { closeCurrentModal, showModal } from './modalTools'; import FormCloudFolderSelect from '../forms/FormCloudFolderSelect.svelte'; @@ -22,8 +21,10 @@ export let fileExtension; export let filePath; export let onSave = undefined; + export let folid; + // export let cntid; - const values = writable({ name, cloudFolder: '__local' }); + const values = writable({ name, cloudFolder: folid ?? '__local' }); const electron = getElectron(); @@ -37,6 +38,8 @@ savedFile: name, savedFolder: folder, savedFilePath: null, + savedCloudFolderId: null, + savedCloudContentId: null, }); } } else { @@ -46,6 +49,7 @@ data, contentFolder: folder, format, + // cntid, }); if (resp.cntid) { closeCurrentModal(); @@ -55,7 +59,7 @@ savedFolder: folder, savedFilePath: null, savedCloudFolderId: cloudFolder, - savedCloudContentId: resp.cntid, + // savedCloudContentId: resp.cntid, }); } } @@ -75,6 +79,8 @@ savedFile: null, savedFolder: null, savedFilePath: filePath, + savedCloudFolderId: null, + savedCloudContentId: null, }); } }; diff --git a/packages/web/src/utility/cloudListeners.ts b/packages/web/src/utility/cloudListeners.ts index 7eef1cd46..bdb3b1bc6 100644 --- a/packages/web/src/utility/cloudListeners.ts +++ b/packages/web/src/utility/cloudListeners.ts @@ -47,5 +47,11 @@ export function installCloudListeners() { ensureCloudConnectionsLoaded(...conids); }); - apiOn('cloud-content-updated', () => cloudConnectionsStore.set({})); + apiOn('cloud-content-updated', () => { + const conids = Object.keys(getCloudConnectionsStore()); + cloudConnectionsStore.set({}); + for (const conn of conids) { + loadCloudConnection(conn); + } + }); } diff --git a/packages/web/src/utility/openElectronFile.ts b/packages/web/src/utility/openElectronFile.ts index f771cdcdf..51c265f8e 100644 --- a/packages/web/src/utility/openElectronFile.ts +++ b/packages/web/src/utility/openElectronFile.ts @@ -111,6 +111,8 @@ async function openSavedElectronFile(filePath, parsed, folder) { props: { savedFile: null, savedFolder: null, + savedCloudFolderId: null, + savedCloudContentId: null, savedFilePath: filePath, savedFormat: handler.format, ...connProps, diff --git a/packages/web/src/utility/openNewTab.ts b/packages/web/src/utility/openNewTab.ts index 59fab9db4..6b4070ded 100644 --- a/packages/web/src/utility/openNewTab.ts +++ b/packages/web/src/utility/openNewTab.ts @@ -30,7 +30,8 @@ export default async function openNewTab(newTab, initialData: any = undefined, o }; } - const { savedFile, savedFolder, savedFilePath, conid, database } = newTab.props || {}; + const { savedFile, savedFolder, savedFilePath, savedCloudFolderId, savedCloudContentId, conid, database } = + newTab.props || {}; if (conid && database) { const connection = await getConnectionInfo({ conid }); @@ -49,7 +50,9 @@ export default async function openNewTab(newTab, initialData: any = undefined, o x.closedTime == null && x.props.savedFile == savedFile && x.props.savedFolder == savedFolder && - x.props.savedFilePath == savedFilePath + x.props.savedFilePath == savedFilePath && + x.props.savedCloudFolderId == savedCloudFolderId && + x.props.savedCloudContentId == savedCloudContentId ); } diff --git a/packages/web/src/utility/saveTabFile.ts b/packages/web/src/utility/saveTabFile.ts index 60351ecdb..b4519fc9d 100644 --- a/packages/web/src/utility/saveTabFile.ts +++ b/packages/web/src/utility/saveTabFile.ts @@ -15,16 +15,31 @@ export default async function saveTabFile(editor, saveMode, folder, format, file const tabs = get(openedTabs); const tabid = editor.activator.tabid; const data = editor.getData(); - const { savedFile, savedFilePath, savedFolder } = tabs.find(x => x.tabid == tabid).props || {}; + const { savedFile, savedFilePath, savedFolder, savedCloudFolderId, savedCloudContentId } = + tabs.find(x => x.tabid == tabid).props || {}; const handleSave = async () => { - if (savedFile) { - await apiCall('files/save', { folder: savedFolder || folder, file: savedFile, data, format }); + if (savedCloudFolderId && savedCloudContentId) { + const resp = await apiCall('cloud/save-file', { + folid: savedCloudFolderId, + fileName: savedFile, + data, + contentFolder: folder, + format, + cntid: savedCloudContentId, + }); + if (resp.cntid) { + markTabSaved(tabid); + } + } else { + if (savedFile) { + await apiCall('files/save', { folder: savedFolder || folder, file: savedFile, data, format }); + } + if (savedFilePath) { + await apiCall('files/save-as', { filePath: savedFilePath, data, format }); + } + markTabSaved(tabid); } - if (savedFilePath) { - await apiCall('files/save-as', { filePath: savedFilePath, data, format }); - } - markTabSaved(tabid); }; const onSave = (title, newProps) => { @@ -60,6 +75,8 @@ export default async function saveTabFile(editor, saveMode, folder, format, file savedFile: null, savedFolder: null, savedFilePath: file, + savedCloudFolderId: null, + savedCloudContentId: null, }); } } else if ((savedFile || savedFilePath) && saveMode == 'save') { @@ -73,6 +90,8 @@ export default async function saveTabFile(editor, saveMode, folder, format, file name: savedFile || 'newFile', filePath: savedFilePath, onSave, + folid: savedCloudFolderId, + // cntid: savedCloudContentId, }); } } diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index 8fdb35f5d..1a53663d5 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -41,22 +41,25 @@ const serverStatus = useServerStatus(); $: emptyCloudContent = ($cloudContentList || []).filter(x => !x.items?.length).map(x => x.folid); - $: cloudContentFlat = ($cloudContentList || []) - .flatMap(fld => fld.items ?? []) - .map(data => { - if (data.type == 'connection') { - const conid = `cloud://${data.folid}/${data.cntid}`; - const status = $serverStatus ? $serverStatus[$volatileConnectionMapStore[conid] || conid] : undefined; + $: cloudContentFlat = _.sortBy( + ($cloudContentList || []) + .flatMap(fld => fld.items ?? []) + .map(data => { + if (data.type == 'connection') { + const conid = `cloud://${data.folid}/${data.cntid}`; + const status = $serverStatus ? $serverStatus[$volatileConnectionMapStore[conid] || conid] : undefined; - return { - ...data, - conid, - status, - }; - } + return { + ...data, + conid, + status, + }; + } - return data; - }); + return data; + }), + 'name' + ); $: contentGroupMap = _.keyBy($cloudContentList || [], x => x.folid); // $: console.log('cloudContentFlat', cloudContentFlat); From 7a3b27227ad5ef3a3c961a1f75f1dd9d66464908 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 13:21:52 +0200 Subject: [PATCH 35/39] stats fixed --- packages/api/src/utility/cloudIntf.js | 7 ++++++- packages/api/src/utility/hardwareFingerprint.js | 1 + packages/web/src/commands/stdCommands.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index da67425b8..3e64de812 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -11,6 +11,8 @@ const { isProApp } = require('./checkLicense'); const socket = require('./socket'); const config = require('../controllers/config'); const simpleEncryptor = require('simple-encryptor'); +const currentVersion = require('../currentVersion'); +const { getPublicIpInfo } = require('./hardwareFingerprint'); const logger = getLogger('cloudIntf'); @@ -151,6 +153,8 @@ async function updateCloudFiles(isRefresh) { lastCloudFilesTags = ''; } + const ipInfo = await getPublicIpInfo(); + const tags = (await collectCloudFilesSearchTags()).join(','); let lastCheckedTm = 0; if (tags == lastCloudFilesTags && cloudFiles.length > 0) { @@ -162,11 +166,12 @@ async function updateCloudFiles(isRefresh) { const resp = await axios.default.get( `${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}&isRefresh=${ isRefresh ? 1 : 0 - }`, + }&country=${ipInfo?.country || ''}`, { headers: { ...getLicenseHttpHeaders(), ...(await getCloudSigninHeaders()), + 'x-app-version': currentVersion.version, }, } ); diff --git a/packages/api/src/utility/hardwareFingerprint.js b/packages/api/src/utility/hardwareFingerprint.js index 1be04fbb2..c99d86967 100644 --- a/packages/api/src/utility/hardwareFingerprint.js +++ b/packages/api/src/utility/hardwareFingerprint.js @@ -87,4 +87,5 @@ module.exports = { getHardwareFingerprint, getHardwareFingerprintHash, getPublicHardwareFingerprint, + getPublicIpInfo, }; diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 4c1d822de..10f22e56c 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -132,7 +132,7 @@ registerCommand({ category: 'New', toolbarOrder: 1, name: 'Connection on Cloud', - testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase, + testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase && isProApp(), onClick: () => { openNewTab({ title: 'New Connection on Cloud', From 45d82dce041a41e8a55f651f6a79f85bd098606c Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 15:55:53 +0200 Subject: [PATCH 36/39] tmp change --- packages/api/src/utility/cloudIntf.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 3e64de812..c51a4d910 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -21,13 +21,13 @@ let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY ? 'http://localhost:3103' : process.env.DEVWEB || process.env.DEVMODE - ? 'https://identity.dbgate.udolni.net' + ? 'https://identity.dbgate.io' // 'https://identity.dbgate.udolni.net' : 'https://identity.dbgate.io'; const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD ? 'http://localhost:3110' : process.env.DEVWEB || process.env.DEVMODE - ? 'https://cloud.dbgate.udolni.net' + ? 'https://cloud.dbgate.io' // 'https://cloud.dbgate.udolni.net' : 'https://cloud.dbgate.io'; async function createDbGateIdentitySession(client) { From aff7125914da771c1f2617b7a1beef091b02f08e Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 16:44:58 +0200 Subject: [PATCH 37/39] Revert "tmp change" This reverts commit 45d82dce041a41e8a55f651f6a79f85bd098606c. --- packages/api/src/utility/cloudIntf.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index c51a4d910..3e64de812 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -21,13 +21,13 @@ let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY ? 'http://localhost:3103' : process.env.DEVWEB || process.env.DEVMODE - ? 'https://identity.dbgate.io' // 'https://identity.dbgate.udolni.net' + ? 'https://identity.dbgate.udolni.net' : 'https://identity.dbgate.io'; const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD ? 'http://localhost:3110' : process.env.DEVWEB || process.env.DEVMODE - ? 'https://cloud.dbgate.io' // 'https://cloud.dbgate.udolni.net' + ? 'https://cloud.dbgate.udolni.net' : 'https://cloud.dbgate.io'; async function createDbGateIdentitySession(client) { From cb50d2838aec648de5b7ef4a7162801e6ec7f5a8 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 17:37:55 +0200 Subject: [PATCH 38/39] license limit modal --- packages/api/src/utility/cloudIntf.js | 4 +- .../modals/LicenseLimitMessageModal.svelte | 62 +++++++++++++++++++ packages/web/src/utility/api.ts | 9 ++- 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 packages/web/src/modals/LicenseLimitMessageModal.svelte diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 3e64de812..78d28a53a 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -266,9 +266,9 @@ async function callCloudApiPost(endpoint, body, signinHolder = null) { }, validateStatus: status => status < 500, }); - const { errorMessage } = resp.data; + const { errorMessage, isLicenseLimit } = resp.data; if (errorMessage) { - return { apiErrorMessage: errorMessage }; + return { apiErrorMessage: errorMessage, apiErrorIsLicenseLimit: isLicenseLimit }; } return resp.data; } diff --git a/packages/web/src/modals/LicenseLimitMessageModal.svelte b/packages/web/src/modals/LicenseLimitMessageModal.svelte new file mode 100644 index 000000000..624b632b5 --- /dev/null +++ b/packages/web/src/modals/LicenseLimitMessageModal.svelte @@ -0,0 +1,62 @@ + + + + +
License limit error
+ +
+
+ +
+
+

+ Cloud operation ended with error:
+ {message} +

+ +

+ This is a limitation of the free version of DbGate. To continue using cloud operations, please purchase DbGate + Premium. +

+

Free version limit:

+
    +
  • max 5 connections
  • +
  • plus max 5 files
  • +
+
+
+ +
+ + +
+
+
+ + diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 8b80c794a..96b88c060 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -15,6 +15,7 @@ import { isAdminPage, isOneOfPage } from './pageDefs'; import { openWebLink } from './simpleTools'; import { serializeJsTypesReplacer } from 'dbgate-tools'; import { cloudSigninTokenHolder } from '../stores'; +import LicenseLimitMessageModal from '../modals/LicenseLimitMessageModal.svelte'; export const strmid = uuidv1(); @@ -121,7 +122,13 @@ async function processApiResponse(route, args, resp) { // missingCredentials: true, // }; } else if (resp?.apiErrorMessage) { - showSnackbarError('API error:' + resp?.apiErrorMessage); + if (resp?.apiErrorIsLicenseLimit) { + showModal(LicenseLimitMessageModal, { + message: resp.apiErrorMessage, + }); + } else { + showSnackbarError('API error:' + resp?.apiErrorMessage); + } return { errorMessage: resp.apiErrorMessage, }; From e836fa3d3811837511ccb3bcc62c4de84ce0194d Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 12:44:31 +0200 Subject: [PATCH 39/39] show license - better UX --- packages/api/src/utility/cloudIntf.js | 8 +++-- .../modals/LicenseLimitMessageModal.svelte | 29 ++++++++++++------- packages/web/src/utility/api.ts | 1 + 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 78d28a53a..329a55fc0 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -266,9 +266,13 @@ async function callCloudApiPost(endpoint, body, signinHolder = null) { }, validateStatus: status => status < 500, }); - const { errorMessage, isLicenseLimit } = resp.data; + const { errorMessage, isLicenseLimit, limitedLicenseLimits } = resp.data; if (errorMessage) { - return { apiErrorMessage: errorMessage, apiErrorIsLicenseLimit: isLicenseLimit }; + return { + apiErrorMessage: errorMessage, + apiErrorIsLicenseLimit: isLicenseLimit, + apiErrorLimitedLicenseLimits: limitedLicenseLimits, + }; } return resp.data; } diff --git a/packages/web/src/modals/LicenseLimitMessageModal.svelte b/packages/web/src/modals/LicenseLimitMessageModal.svelte index 624b632b5..4121159cc 100644 --- a/packages/web/src/modals/LicenseLimitMessageModal.svelte +++ b/packages/web/src/modals/LicenseLimitMessageModal.svelte @@ -3,17 +3,14 @@ import FormProvider from '../forms/FormProvider.svelte'; import FormSubmit from '../forms/FormSubmit.svelte'; import FontIcon from '../icons/FontIcon.svelte'; + import { isProApp } from '../utility/proTools'; import { openWebLink } from '../utility/simpleTools'; import ModalBase from './ModalBase.svelte'; import { closeCurrentModal } from './modalTools'; export let message; - - function handlePurchase() { - closeCurrentModal(); - openWebLink('https://dbgate.io/purchase/premium/', '_blank'); - } + export let licenseLimits; @@ -31,20 +28,32 @@

- This is a limitation of the free version of DbGate. To continue using cloud operations, please purchase DbGate - Premium. + This is a limitation of the free version of DbGate. To continue using cloud operations, please {#if !isProApp()}download + and{/if} purchase DbGate Premium.

Free version limit:

    -
  • max 5 connections
  • -
  • plus max 5 files
  • + {#each licenseLimits || [] as limit} +
  • {limit}
  • + {/each}
- + {#if !isProApp()} + openWebLink('https://dbgate.io/download/')} + skipWidth + /> + {/if} + openWebLink('https://dbgate.io/purchase/premium/')} + skipWidth + />
diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 96b88c060..043699ff0 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -125,6 +125,7 @@ async function processApiResponse(route, args, resp) { if (resp?.apiErrorIsLicenseLimit) { showModal(LicenseLimitMessageModal, { message: resp.apiErrorMessage, + licenseLimits: resp.apiErrorLimitedLicenseLimits, }); } else { showSnackbarError('API error:' + resp?.apiErrorMessage);