Merge branch 'develop'

This commit is contained in:
Jan Prochazka
2024-08-06 15:01:16 +02:00
108 changed files with 1783 additions and 462 deletions

View File

@@ -121,6 +121,7 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"better-sqlite3": "9.6.0", "better-sqlite3": "9.6.0",
"msnodesqlv8": "^4.2.1" "msnodesqlv8": "^4.2.1",
"oracledb": "^6.6.0"
} }
} }

View File

@@ -365,11 +365,13 @@ function createWindow() {
console.log('Error saving config-root:', err.message); console.log('Error saving config-root:', err.message);
} }
}); });
// mainWindow.webContents.toggleDevTools();
mainWindow.loadURL(startUrl); mainWindow.loadURL(startUrl);
if (os.platform() == 'linux') { if (os.platform() == 'linux') {
mainWindow.setIcon(path.resolve(__dirname, '../icon.png')); mainWindow.setIcon(path.resolve(__dirname, '../icon.png'));
} }
// mainWindow.webContents.toggleDevTools();
mainWindow.on('maximize', () => { mainWindow.on('maximize', () => {
mainWindow.webContents.send('setIsMaximized', true); mainWindow.webContents.send('setIsMaximized', true);

View File

@@ -1944,6 +1944,11 @@ open@^7.4.2:
is-docker "^2.0.0" is-docker "^2.0.0"
is-wsl "^2.1.1" is-wsl "^2.1.1"
oracledb@^6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/oracledb/-/oracledb-6.6.0.tgz#bb40adbe81a84a1e544c48af9f120c61f030e936"
integrity sha512-T3dx+o3j+tVN53wQyr4yGTmoPHLy+a2V8yb1T2PmWrsj3ZlSt2Yu1BgV2yTDqnmBZYpRi/I3yJXRCOHHD7PiyA==
os-tmpdir@~1.0.2: os-tmpdir@~1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"

View File

@@ -3,9 +3,10 @@ const fs = require('fs');
let fillContent = ''; let fillContent = '';
if (process.platform == 'win32') { if (process.platform == 'win32') {
fillContent += `content.msnodesqlv8 = () => require('msnodesqlv8');`; fillContent += `content.msnodesqlv8 = () => require('msnodesqlv8');\n`;
} }
fillContent += `content['better-sqlite3'] = () => require('better-sqlite3');`; fillContent += `content['better-sqlite3'] = () => require('better-sqlite3');\n`;
fillContent += `content['oracledb'] = () => require('oracledb');\n`;
const getContent = empty => ` const getContent = empty => `
// this file is generated automatically by script fillNativeModules.js, do not edit it manually // this file is generated automatically by script fillNativeModules.js, do not edit it manually

View File

@@ -20,6 +20,8 @@
"start:api:singledb": "yarn workspace dbgate-api start:singledb | pino-pretty", "start:api:singledb": "yarn workspace dbgate-api start:singledb | pino-pretty",
"start:api:auth": "yarn workspace dbgate-api start:auth | pino-pretty", "start:api:auth": "yarn workspace dbgate-api start:auth | pino-pretty",
"start:api:dblogin": "yarn workspace dbgate-api start:dblogin | pino-pretty", "start:api:dblogin": "yarn workspace dbgate-api start:dblogin | pino-pretty",
"start:api:storage": "yarn workspace dbgate-api start:storage | pino-pretty",
"sync:pro": "cd sync && yarn start",
"start:web": "yarn workspace dbgate-web dev", "start:web": "yarn workspace dbgate-web dev",
"start:sqltree": "yarn workspace dbgate-sqltree start", "start:sqltree": "yarn workspace dbgate-sqltree start",
"start:tools": "yarn workspace dbgate-tools start", "start:tools": "yarn workspace dbgate-tools start",
@@ -35,6 +37,7 @@
"build:web:docker": "yarn workspace dbgate-web build", "build:web:docker": "yarn workspace dbgate-web build",
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend", "build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
"build:plugins:frontend:watch": "workspaces-run --parallel --only=\"dbgate-plugin-*\" -- yarn build:frontend:watch", "build:plugins:frontend:watch": "workspaces-run --parallel --only=\"dbgate-plugin-*\" -- yarn build:frontend:watch",
"storage-json": "dbmodel model-to-json storage-db packages/api/src/storageModel.js --commonjs",
"plugins:copydist": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn copydist", "plugins:copydist": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn copydist",
"build:app:local": "yarn plugins:copydist && cd app && yarn build:local", "build:app:local": "yarn plugins:copydist && cd app && yarn build:local",
"start:app:local": "cd app && yarn start:local", "start:app:local": "cd app && yarn start:local",
@@ -48,8 +51,8 @@
"resetPackagedPlugins": "node resetPackagedPlugins", "resetPackagedPlugins": "node resetPackagedPlugins",
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src", "prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2 && copyfiles \"plugins/dist/**/*\" docker/plugins -u 2", "copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2 && copyfiles \"plugins/dist/**/*\" docker/plugins -u 2",
"install:sqlite:docker": "cd docker && yarn init --yes && yarn add better-sqlite3 && cd ..", "install:drivers:docker": "cd docker && yarn init --yes && yarn add better-sqlite3 && yarn add oracledb && cd ..",
"prepare:docker": "yarn plugins:copydist && yarn build:web:docker && yarn build:api && yarn copy:docker:build && yarn install:sqlite:docker", "prepare:docker": "yarn plugins:copydist && yarn build:web:docker && yarn build:api && yarn copy:docker:build && yarn install:drivers:docker",
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"", "start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
"lib": "concurrently --kill-others-on-fail \"yarn start:sqltree\" \"yarn start:filterparser\" \"yarn start:datalib\" \"yarn start:tools\" \"yarn build:plugins:frontend:watch\"", "lib": "concurrently --kill-others-on-fail \"yarn start:sqltree\" \"yarn start:filterparser\" \"yarn start:datalib\" \"yarn start:tools\" \"yarn build:plugins:frontend:watch\"",
"ts:api": "yarn workspace dbgate-api ts", "ts:api": "yarn workspace dbgate-api ts",

View File

@@ -66,6 +66,7 @@
"start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api", "start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api",
"start:dblogin": "env-cmd -f env/dblogin/.env node src/index.js --listen-api", "start:dblogin": "env-cmd -f env/dblogin/.env node src/index.js --listen-api",
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api", "start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
"start:storage": "env-cmd -f env/storage/.env node src/index.js --listen-api",
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api", "start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
"ts": "tsc", "ts": "tsc",
"build": "webpack" "build": "webpack"
@@ -83,6 +84,7 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"better-sqlite3": "9.6.0", "better-sqlite3": "9.6.0",
"msnodesqlv8": "^4.2.1" "msnodesqlv8": "^4.2.1",
"oracledb": "^6.6.0"
} }
} }

View File

@@ -0,0 +1,16 @@
const crypto = require('crypto');
const tokenSecret = crypto.randomUUID();
function getTokenLifetime() {
return process.env.TOKEN_LIFETIME || '1d';
}
function getTokenSecret() {
return tokenSecret;
}
module.exports = {
getTokenLifetime,
getTokenSecret,
};

View File

@@ -0,0 +1,251 @@
const { getTokenSecret, getTokenLifetime } = require('./authCommon');
const _ = require('lodash');
const axios = require('axios');
const { getLogger } = require('dbgate-tools');
const AD = require('activedirectory2').promiseWrapper;
const jwt = require('jsonwebtoken');
const logger = getLogger('authProvider');
class AuthProviderBase {
async login(login, password, options = undefined) {
return {};
}
shouldAuthorizeApi() {
return false;
}
oauthToken(params) {
return {};
}
getCurrentLogin(req) {
const login = req?.user?.login ?? req?.auth?.user ?? null;
return login;
}
isUserLoggedIn(req) {
return !!req?.user || !!req?.auth;
}
getCurrentPermissions(req) {
const login = this.getCurrentLogin(req);
const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
return permissions || process.env.PERMISSIONS;
}
isLoginForm() {
return false;
}
getAdditionalConfigProps() {
return {};
}
getLoginPageConnections() {
return null;
}
getSingleConnectionId(req) {
return null;
}
}
class OAuthProvider extends AuthProviderBase {
shouldAuthorizeApi() {
return true;
}
async oauthToken(params) {
const { redirectUri, code } = params;
const scopeParam = process.env.OAUTH_SCOPE ? `&scope=${process.env.OAUTH_SCOPE}` : '';
const resp = await axios.default.post(
`${process.env.OAUTH_TOKEN}`,
`grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(
redirectUri
)}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}${scopeParam}`
);
const { access_token, refresh_token } = resp.data;
const payload = jwt.decode(access_token);
logger.info({ payload }, 'User payload returned from OAUTH');
const login =
process.env.OAUTH_LOGIN_FIELD && payload && payload[process.env.OAUTH_LOGIN_FIELD]
? payload[process.env.OAUTH_LOGIN_FIELD]
: 'oauth';
if (
process.env.OAUTH_ALLOWED_LOGINS &&
!process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
) {
return { error: `Username ${login} not allowed to log in` };
}
const groups =
process.env.OAUTH_GROUP_FIELD && payload && payload[process.env.OAUTH_GROUP_FIELD]
? payload[process.env.OAUTH_GROUP_FIELD]
: [];
const allowedGroups = process.env.OAUTH_ALLOWED_GROUPS
? process.env.OAUTH_ALLOWED_GROUPS.split(',').map(group => group.toLowerCase().trim())
: [];
if (process.env.OAUTH_ALLOWED_GROUPS && !groups.some(group => allowedGroups.includes(group.toLowerCase().trim()))) {
return { error: `Username ${login} does not belong to an allowed group` };
}
if (access_token) {
return {
accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }),
};
}
return { error: 'Token not found' };
}
getAdditionalConfigProps() {
return {
oauth: process.env.OAUTH_AUTH,
oauthClient: process.env.OAUTH_CLIENT_ID,
oauthScope: process.env.OAUTH_SCOPE,
oauthLogout: process.env.OAUTH_LOGOUT,
};
}
}
class ADProvider extends AuthProviderBase {
async login(login, password) {
const adConfig = {
url: process.env.AD_URL,
baseDN: process.env.AD_BASEDN,
username: process.env.AD_USERNAME,
password: process.env.AD_PASSWORD,
};
const ad = new AD(adConfig);
try {
const res = await ad.authenticate(login, password);
if (!res) {
return { error: 'Login failed' };
}
if (
process.env.AD_ALLOWED_LOGINS &&
!process.env.AD_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
) {
return { error: `Username ${login} not allowed to log in` };
}
return {
accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }),
};
} catch (e) {
return { error: 'Login failed' };
}
}
shouldAuthorizeApi() {
return !process.env.BASIC_AUTH;
}
isLoginForm() {
return !process.env.BASIC_AUTH;
}
}
class LoginsProvider extends AuthProviderBase {
async login(login, password) {
if (password == process.env[`LOGIN_PASSWORD_${login}`]) {
return {
accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }),
};
}
return { error: 'Invalid credentials' };
}
shouldAuthorizeApi() {
return !process.env.BASIC_AUTH;
}
isLoginForm() {
return !process.env.BASIC_AUTH;
}
}
class DenyAllProvider extends AuthProviderBase {
shouldAuthorizeApi() {
return true;
}
async login(login, password) {
return { error: 'Login not allowed' };
}
}
function hasEnvLogins() {
if (process.env.LOGIN && process.env.PASSWORD) {
return true;
}
for (const key in process.env) {
if (key.startsWith('LOGIN_PASSWORD_')) {
return true;
}
}
return false;
}
function detectEnvAuthProvider() {
if (process.env.AUTH_PROVIDER) {
return process.env.AUTH_PROVIDER;
}
if (process.env.STORAGE_DATABASE) {
return 'denyall';
}
if (process.env.OAUTH_AUTH) {
return 'oauth';
}
if (process.env.AD_URL) {
return 'ad';
}
if (hasEnvLogins()) {
return 'logins';
}
return 'none';
}
function createEnvAuthProvider() {
const authProvider = detectEnvAuthProvider();
switch (authProvider) {
case 'oauth':
return new OAuthProvider();
case 'ad':
return new ADProvider();
case 'logins':
return new LoginsProvider();
case 'denyall':
return new DenyAllProvider();
default:
return new AuthProviderBase();
}
}
let authProvider = createEnvAuthProvider();
function getAuthProvider() {
return authProvider;
}
function setAuthProvider(value) {
authProvider = value;
}
module.exports = {
AuthProviderBase,
detectEnvAuthProvider,
getAuthProvider,
setAuthProvider,
};

View File

@@ -1,24 +1,15 @@
const axios = require('axios'); const axios = require('axios');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const getExpressPath = require('../utility/getExpressPath'); const getExpressPath = require('../utility/getExpressPath');
const { getLogins } = require('../utility/hasPermission');
const { getLogger } = require('dbgate-tools'); const { getLogger } = require('dbgate-tools');
const AD = require('activedirectory2').promiseWrapper; const AD = require('activedirectory2').promiseWrapper;
const crypto = require('crypto'); const crypto = require('crypto');
const { getTokenSecret, getTokenLifetime } = require('../auth/authCommon');
const { getAuthProvider } = require('../auth/authProvider');
const storage = require('./storage');
const logger = getLogger('auth'); const logger = getLogger('auth');
const tokenSecret = crypto.randomUUID();
function shouldAuthorizeApi() {
const logins = getLogins();
return !!process.env.OAUTH_AUTH || !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH);
}
function getTokenLifetime() {
return process.env.TOKEN_LIFETIME || '1d';
}
function unauthorizedResponse(req, res, text) { function unauthorizedResponse(req, res, text) {
// if (req.path == getExpressPath('/config/get-settings')) { // if (req.path == getExpressPath('/config/get-settings')) {
// return res.json({}); // return res.json({});
@@ -30,9 +21,23 @@ function unauthorizedResponse(req, res, text) {
} }
function authMiddleware(req, res, next) { function authMiddleware(req, res, next) {
const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', '/auth/login', '/stream']; const SKIP_AUTH_PATHS = [
'/config/get',
'/config/get-settings',
'/auth/oauth-token',
'/auth/login',
'/stream',
'storage/get-connections-for-login-page',
'/connections/dblogin',
'/connections/dblogin-auth',
'/connections/dblogin-auth-token',
];
if (!shouldAuthorizeApi()) { // console.log('********************* getAuthProvider()', getAuthProvider());
const isAdminPage = req.headers['x-is-admin-page'] == 'true';
if (!isAdminPage && !getAuthProvider().shouldAuthorizeApi()) {
return next(); return next();
} }
let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x)); let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x));
@@ -46,7 +51,7 @@ function authMiddleware(req, res, next) {
} }
const token = authHeader.split(' ')[1]; const token = authHeader.split(' ')[1];
try { try {
const decoded = jwt.verify(token, tokenSecret); const decoded = jwt.verify(token, getTokenSecret());
req.user = decoded; req.user = decoded;
return next(); return next();
} catch (err) { } catch (err) {
@@ -63,106 +68,34 @@ function authMiddleware(req, res, next) {
module.exports = { module.exports = {
oauthToken_meta: true, oauthToken_meta: true,
async oauthToken(params) { async oauthToken(params) {
const { redirectUri, code } = params; return getAuthProvider().oauthToken(params);
const scopeParam = process.env.OAUTH_SCOPE ? `&scope=${process.env.OAUTH_SCOPE}` : '';
const resp = await axios.default.post(
`${process.env.OAUTH_TOKEN}`,
`grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(
redirectUri
)}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}${scopeParam}`
);
const { access_token, refresh_token } = resp.data;
const payload = jwt.decode(access_token);
logger.info({ payload }, 'User payload returned from OAUTH');
const login =
process.env.OAUTH_LOGIN_FIELD && payload && payload[process.env.OAUTH_LOGIN_FIELD]
? payload[process.env.OAUTH_LOGIN_FIELD]
: 'oauth';
if (
process.env.OAUTH_ALLOWED_LOGINS &&
!process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
) {
return { error: `Username ${login} not allowed to log in` };
}
const groups =
process.env.OAUTH_GROUP_FIELD && payload && payload[process.env.OAUTH_GROUP_FIELD]
? payload[process.env.OAUTH_GROUP_FIELD]
: [];
const allowedGroups =
process.env.OAUTH_ALLOWED_GROUPS
? process.env.OAUTH_ALLOWED_GROUPS.split(',').map(group => group.toLowerCase().trim())
: [];
if (
process.env.OAUTH_ALLOWED_GROUPS &&
!groups.some(group => allowedGroups.includes(group.toLowerCase().trim()))
) {
return { error: `Username ${login} does not belong to an allowed group` };
}
if (access_token) {
return {
accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
};
}
return { error: 'Token not found' };
}, },
login_meta: true, login_meta: true,
async login(params) { async login(params) {
const { login, password } = params; const { login, password, isAdminPage } = params;
if (process.env.AD_URL) { if (isAdminPage) {
const adConfig = { if (process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD == password) {
url: process.env.AD_URL,
baseDN: process.env.AD_BASEDN,
username: process.env.AD_USERNAME,
password: process.env.AD_PASSOWRD,
};
const ad = new AD(adConfig);
try {
const res = await ad.authenticate(login, password);
if (!res) {
return { error: 'Login failed' };
}
if (
process.env.AD_ALLOWED_LOGINS &&
!process.env.AD_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
) {
return { error: `Username ${login} not allowed to log in` };
}
return { return {
accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }), accessToken: jwt.sign(
}; {
} catch (err) { login: 'superadmin',
logger.error({ err }, 'Failed active directory authentization'); permissions: await storage.loadSuperadminPermissions(),
return { roleId: -3,
error: err.message, },
getTokenSecret(),
{
expiresIn: getTokenLifetime(),
}
),
}; };
} }
return { error: 'Login failed' };
} }
const logins = getLogins(); return getAuthProvider().login(login, password);
if (!logins) {
return { error: 'Logins not configured' };
}
const foundLogin = logins.find(x => x.login == login);
if (foundLogin && foundLogin.password && foundLogin.password == password) {
return {
accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
};
}
return { error: 'Invalid credentials' };
}, },
authMiddleware, authMiddleware,
shouldAuthorizeApi,
}; };

View File

@@ -3,7 +3,7 @@ const os = require('os');
const path = require('path'); const path = require('path');
const axios = require('axios'); const axios = require('axios');
const { datadir, getLogsFilePath } = require('../utility/directories'); const { datadir, getLogsFilePath } = require('../utility/directories');
const { hasPermission, getLogins } = require('../utility/hasPermission'); const { hasPermission } = require('../utility/hasPermission');
const socket = require('../utility/socket'); const socket = require('../utility/socket');
const _ = require('lodash'); const _ = require('lodash');
const AsyncLock = require('async-lock'); const AsyncLock = require('async-lock');
@@ -11,6 +11,7 @@ const AsyncLock = require('async-lock');
const currentVersion = require('../currentVersion'); const currentVersion = require('../currentVersion');
const platformInfo = require('../utility/platformInfo'); const platformInfo = require('../utility/platformInfo');
const connections = require('../controllers/connections'); const connections = require('../controllers/connections');
const { getAuthProvider } = require('../auth/authProvider');
const lock = new AsyncLock(); const lock = new AsyncLock();
@@ -27,27 +28,37 @@ module.exports = {
get_meta: true, get_meta: true,
async get(_params, req) { async get(_params, req) {
const logins = getLogins(); const authProvider = getAuthProvider();
const loginName = const login = authProvider.getCurrentLogin(req);
req && req.user && req.user.login ? req.user.login : req && req.auth && req.auth.user ? req.auth.user : null; const permissions = authProvider.getCurrentPermissions(req);
const login = logins && loginName ? logins.find(x => x.login == loginName) : null; const isLoginForm = authProvider.isLoginForm();
const permissions = login ? login.permissions : process.env.PERMISSIONS; const additionalConfigProps = authProvider.getAdditionalConfigProps();
const isUserLoggedIn = authProvider.isUserLoggedIn(req);
const singleConid = authProvider.getSingleConnectionId(req);
const singleConnection = singleConid
? await connections.getCore({ conid: singleConid })
: connections.singleConnection;
return { return {
runAsPortal: !!connections.portalConnections, runAsPortal: !!connections.portalConnections,
singleDbConnection: connections.singleDbConnection, singleDbConnection: connections.singleDbConnection,
singleConnection: connections.singleConnection, singleConnection: singleConnection,
isUserLoggedIn,
// hideAppEditor: !!process.env.HIDE_APP_EDITOR, // hideAppEditor: !!process.env.HIDE_APP_EDITOR,
allowShellConnection: platformInfo.allowShellConnection, allowShellConnection: platformInfo.allowShellConnection,
allowShellScripting: platformInfo.allowShellScripting, allowShellScripting: platformInfo.allowShellScripting,
isDocker: platformInfo.isDocker, isDocker: platformInfo.isDocker,
isElectron: platformInfo.isElectron,
isLicenseValid: platformInfo.isLicenseValid,
licenseError: platformInfo.licenseError,
permissions, permissions,
login, login,
oauth: process.env.OAUTH_AUTH, ...additionalConfigProps,
oauthClient: process.env.OAUTH_CLIENT_ID, isLoginForm,
oauthScope: process.env.OAUTH_SCOPE, isAdminLoginForm: !!(process.env.STORAGE_DATABASE && process.env.ADMIN_PASSWORD && !process.env.BASIC_AUTH),
oauthLogout: process.env.OAUTH_LOGOUT, storageDatabase: process.env.STORAGE_DATABASE,
isLoginForm: !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH),
logsFilePath: getLogsFilePath(), logsFilePath: getLogsFilePath(),
connectionsFilePath: path.join(datadir(), 'connections.jsonl'), connectionsFilePath: path.join(datadir(), 'connections.jsonl'),
...currentVersion, ...currentVersion,

View File

@@ -16,6 +16,8 @@ const { safeJsonParse, getLogger } = require('dbgate-tools');
const platformInfo = require('../utility/platformInfo'); const platformInfo = require('../utility/platformInfo');
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission'); const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
const pipeForkLogs = require('../utility/pipeForkLogs'); const pipeForkLogs = require('../utility/pipeForkLogs');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { getAuthProvider } = require('../auth/authProvider');
const logger = getLogger('connections'); const logger = getLogger('connections');
@@ -199,6 +201,12 @@ module.exports = {
list_meta: true, list_meta: true,
async list(_params, req) { async list(_params, req) {
const storage = require('./storage');
const storageConnections = await storage.connections(req);
if (storageConnections) {
return storageConnections;
}
if (portalConnections) { if (portalConnections) {
if (platformInfo.allowShellConnection) return portalConnections; if (platformInfo.allowShellConnection) return portalConnections;
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req)); return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req));
@@ -236,14 +244,16 @@ module.exports = {
}, },
saveVolatile_meta: true, saveVolatile_meta: true,
async saveVolatile({ conid, user, password, test }) { async saveVolatile({ conid, user = undefined, password = undefined, accessToken = undefined, test = false }) {
const old = await this.getCore({ conid }); const old = await this.getCore({ conid });
const res = { const res = {
...old, ...old,
_id: crypto.randomUUID(), _id: crypto.randomUUID(),
password, password,
accessToken,
passwordMode: undefined, passwordMode: undefined,
unsaved: true, unsaved: true,
useRedirectDbLogin: false,
}; };
if (old.passwordMode == 'askUser') { if (old.passwordMode == 'askUser') {
res.user = user; res.user = user;
@@ -336,6 +346,14 @@ module.exports = {
if (volatile) { if (volatile) {
return volatile; return volatile;
} }
const storage = require('./storage');
const storageConnection = await storage.getConnection({ conid });
if (storageConnection) {
return storageConnection;
}
if (portalConnections) { if (portalConnections) {
const res = portalConnections.find(x => x._id == conid) || null; const res = portalConnections.find(x => x._id == conid) || null;
return mask && !platformInfo.allowShellConnection ? maskConnection(res) : res; return mask && !platformInfo.allowShellConnection ? maskConnection(res) : res;
@@ -365,4 +383,64 @@ module.exports = {
}); });
return res; return res;
}, },
dblogin_meta: {
raw: true,
method: 'get',
},
async dblogin(req, res) {
const { conid, state, redirectUri } = req.query;
const connection = await this.getCore({ conid });
const driver = requireEngineDriver(connection);
const authUrl = await driver.getRedirectAuthUrl(connection, { redirectUri, state });
res.redirect(authUrl);
},
dbloginToken_meta: true,
async dbloginToken({ code, conid, strmid, redirectUri }) {
try {
const connection = await this.getCore({ conid });
const driver = requireEngineDriver(connection);
const accessToken = await driver.getAuthTokenFromCode(connection, { code, redirectUri });
const volatile = await this.saveVolatile({ conid, accessToken });
// console.log('******************************** WE HAVE ACCESS TOKEN', accessToken);
socket.emit('got-volatile-token', { strmid, savedConId: conid, volatileConId: volatile._id });
return { success: true };
} catch (err) {
logger.error({ err }, 'Error getting DB token');
return { error: err.message };
}
},
dbloginAuthToken_meta: true,
async dbloginAuthToken({ code, conid, redirectUri }) {
try {
const connection = await this.getCore({ conid });
const driver = requireEngineDriver(connection);
const accessToken = await driver.getAuthTokenFromCode(connection, { code, redirectUri });
const volatile = await this.saveVolatile({ conid, accessToken });
const authProvider = getAuthProvider();
const resp = await authProvider.login(null, null, { conid: volatile._id });
return resp;
} catch (err) {
logger.error({ err }, 'Error getting DB token');
return { error: err.message };
}
},
dbloginAuth_meta: true,
async dbloginAuth({ conid, user, password }) {
if (user || password) {
const saveResp = await this.saveVolatile({ conid, user, password, test: true });
if (saveResp.msgtype == 'connected') {
const loginResp = await getAuthProvider().login(user, password, { conid: saveResp._id });
return loginResp;
}
return saveResp;
}
// user and password is stored in connection, volatile connection is not needed
const loginResp = await getAuthProvider().login(null, null, { conid });
return loginResp;
},
}; };

View File

@@ -89,6 +89,9 @@ module.exports = {
if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') { if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode }); throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
} }
if (connection.useRedirectDbLogin) {
throw new MissingCredentialsError({ conid, redirectToDbLogin: true });
}
const subprocess = fork( const subprocess = fork(
global['API_PACKAGE'] || process.argv[1], global['API_PACKAGE'] || process.argv[1],
[ [

View File

@@ -56,7 +56,10 @@ module.exports = {
if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') { if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode }); throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
} }
const subprocess = fork( if (connection.useRedirectDbLogin) {
throw new MissingCredentialsError({ conid, redirectToDbLogin: true });
}
const subprocess = fork(
global['API_PACKAGE'] || process.argv[1], global['API_PACKAGE'] || process.argv[1],
[ [
'--is-forked-api', '--is-forked-api',

View File

@@ -0,0 +1,20 @@
module.exports = {
connections_meta: true,
async connections(req) {
return null;
},
getConnection_meta: true,
async getConnection({ conid }) {
return null;
},
async loadSuperadminPermissions() {
return [];
},
getConnectionsForLoginPage_meta: true,
async getConnectionsForLoginPage() {
return null;
},
};

View File

@@ -98,6 +98,7 @@ if (processArgs.listenApi) {
const shell = require('./shell/index'); const shell = require('./shell/index');
const dbgateTools = require('dbgate-tools'); const dbgateTools = require('dbgate-tools');
const currentVersion = require('./currentVersion');
global['DBGATE_TOOLS'] = dbgateTools; global['DBGATE_TOOLS'] = dbgateTools;
@@ -116,6 +117,7 @@ module.exports = {
...shell, ...shell,
getLogger, getLogger,
configureLogger, configureLogger,
currentVersion,
// loadLogsContent, // loadLogsContent,
getMainModule: () => require('./main'), getMainModule: () => require('./main'),
}; };

View File

@@ -18,6 +18,7 @@ const sessions = require('./controllers/sessions');
const runners = require('./controllers/runners'); const runners = require('./controllers/runners');
const jsldata = require('./controllers/jsldata'); const jsldata = require('./controllers/jsldata');
const config = require('./controllers/config'); const config = require('./controllers/config');
const storage = require('./controllers/storage');
const archive = require('./controllers/archive'); const archive = require('./controllers/archive');
const apps = require('./controllers/apps'); const apps = require('./controllers/apps');
const auth = require('./controllers/auth'); const auth = require('./controllers/auth');
@@ -31,9 +32,9 @@ const onFinished = require('on-finished');
const { rundir } = require('./utility/directories'); const { rundir } = require('./utility/directories');
const platformInfo = require('./utility/platformInfo'); const platformInfo = require('./utility/platformInfo');
const getExpressPath = require('./utility/getExpressPath'); const getExpressPath = require('./utility/getExpressPath');
const { getLogins } = require('./utility/hasPermission');
const _ = require('lodash'); const _ = require('lodash');
const { getLogger } = require('dbgate-tools'); const { getLogger } = require('dbgate-tools');
const { getAuthProvider } = require('./auth/authProvider');
const logger = getLogger('main'); const logger = getLogger('main');
@@ -44,11 +45,23 @@ function start() {
const server = http.createServer(app); const server = http.createServer(app);
const logins = getLogins(); if (process.env.BASIC_AUTH) {
if (logins && process.env.BASIC_AUTH) { async function authorizer(username, password, cb) {
try {
const resp = await getAuthProvider().login(username, password);
if (resp.accessToken) {
cb(null, true);
} else {
cb(null, false);
}
} catch (err) {
cb(err, false);
}
}
app.use( app.use(
basicAuth({ basicAuth({
users: _.fromPairs(logins.filter(x => x.password).map(x => [x.login, x.password])), authorizer,
authorizeAsync: true,
challenge: true, challenge: true,
realm: 'DbGate Web App', realm: 'DbGate Web App',
}) })
@@ -72,9 +85,7 @@ function start() {
}); });
} }
if (auth.shouldAuthorizeApi()) { app.use(auth.authMiddleware);
app.use(auth.authMiddleware);
}
app.get(getExpressPath('/stream'), async function (req, res) { app.get(getExpressPath('/stream'), async function (req, res) {
const strmid = req.query.strmid; const strmid = req.query.strmid;
@@ -162,6 +173,7 @@ function useAllControllers(app, electron) {
useController(app, electron, '/runners', runners); useController(app, electron, '/runners', runners);
useController(app, electron, '/jsldata', jsldata); useController(app, electron, '/jsldata', jsldata);
useController(app, electron, '/config', config); useController(app, electron, '/config', config);
useController(app, electron, '/storage', storage);
useController(app, electron, '/archive', archive); useController(app, electron, '/archive', archive);
useController(app, electron, '/uploads', uploads); useController(app, electron, '/uploads', uploads);
useController(app, electron, '/plugins', plugins); useController(app, electron, '/plugins', plugins);

View File

@@ -15,10 +15,12 @@ async function getSshConnection(connection) {
agentForward: connection.sshMode == 'agent', agentForward: connection.sshMode == 'agent',
passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyfilePassword : undefined, passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyfilePassword : undefined,
username: connection.sshLogin, username: connection.sshLogin,
password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined, password: (connection.sshMode || 'userPassword') == 'userPassword' ? connection.sshPassword : undefined,
agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined, agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined,
privateKey: privateKey:
connection.sshMode == 'keyFile' && connection.sshKeyfile ? await fs.readFile(connection.sshKeyfile) : undefined, connection.sshMode == 'keyFile' && (connection.sshKeyfile || platformInfo?.defaultKeyfile)
? await fs.readFile(connection.sshKeyfile || platformInfo?.defaultKeyfile)
: undefined,
skipAutoPrivateKey: true, skipAutoPrivateKey: true,
noReadline: true, noReadline: true,
}; };

View File

@@ -0,0 +1,16 @@
const importDbModel = require('../utility/importDbModel');
const fs = require('fs');
async function dbModelToJson({ modelFolder, outputFile, commonjs }) {
const dbInfo = await importDbModel(modelFolder);
const json = JSON.stringify(dbInfo, null, 2);
if (commonjs) {
fs.writeFileSync(outputFile, `module.exports = ${json};`);
return;
} else {
fs.writeFileSync(outputFile, json);
}
}
module.exports = dbModelToJson;

View File

@@ -27,6 +27,8 @@ const loadDatabase = require('./loadDatabase');
const generateModelSql = require('./generateModelSql'); const generateModelSql = require('./generateModelSql');
const modifyJsonLinesReader = require('./modifyJsonLinesReader'); const modifyJsonLinesReader = require('./modifyJsonLinesReader');
const dataDuplicator = require('./dataDuplicator'); const dataDuplicator = require('./dataDuplicator');
const dbModelToJson = require('./dbModelToJson');
const jsonToDbModel = require('./jsonToDbModel');
const dbgateApi = { const dbgateApi = {
queryReader, queryReader,
@@ -57,6 +59,8 @@ const dbgateApi = {
generateModelSql, generateModelSql,
modifyJsonLinesReader, modifyJsonLinesReader,
dataDuplicator, dataDuplicator,
dbModelToJson,
jsonToDbModel,
}; };
requirePlugin.initializeDbgateApi(dbgateApi); requirePlugin.initializeDbgateApi(dbgateApi);

View File

@@ -0,0 +1,9 @@
const exportDbModel = require('../utility/exportDbModel');
const fs = require('fs');
async function jsonToDbModel({ modelFile, outputDir }) {
const dbInfo = JSON.parse(fs.readFileSync(modelFile, 'utf-8'));
await exportDbModel(dbInfo, outputDir);
}
module.exports = jsonToDbModel;

View File

@@ -11,6 +11,7 @@ const loadedPlugins = {};
const dbgateEnv = { const dbgateEnv = {
dbgateApi: null, dbgateApi: null,
nativeModules, nativeModules,
platformInfo,
}; };
function requirePlugin(packageName, requiredPlugin = null) { function requirePlugin(packageName, requiredPlugin = null) {
if (!packageName) throw new Error('Missing packageName in plugin'); if (!packageName) throw new Error('Missing packageName in plugin');

View File

@@ -0,0 +1,7 @@
function checkLicense() {
return null;
}
module.exports = {
checkLicense,
};

View File

@@ -1,72 +1,81 @@
const { compilePermissions, testPermission } = require('dbgate-tools'); const { compilePermissions, testPermission } = require('dbgate-tools');
const _ = require('lodash'); const _ = require('lodash');
const { getAuthProvider } = require('../auth/authProvider');
const userPermissions = {}; const cachedPermissions = {};
function hasPermission(tested, req) { function hasPermission(tested, req) {
if (!req) { if (!req) {
// request object not available, allow all // request object not available, allow all
return true; return true;
} }
const { user } = (req && req.auth) || {};
const { login } = (process.env.OAUTH_PERMISSIONS && req && req.user) || {};
const key = user || login || '';
const logins = getLogins();
if (!userPermissions[key]) { const permissions = getAuthProvider().getCurrentPermissions(req);
if (logins) {
const login = logins.find(x => x.login == user); if (!cachedPermissions[permissions]) {
userPermissions[key] = compilePermissions(login ? login.permissions : null); cachedPermissions[permissions] = compilePermissions(permissions);
} else {
userPermissions[key] = compilePermissions(process.env.PERMISSIONS);
}
} }
return testPermission(tested, userPermissions[key]);
return testPermission(tested, cachedPermissions[permissions]);
// const { user } = (req && req.auth) || {};
// const { login } = (process.env.OAUTH_PERMISSIONS && req && req.user) || {};
// const key = user || login || '';
// const logins = getLogins();
// if (!userPermissions[key]) {
// if (logins) {
// const login = logins.find(x => x.login == user);
// userPermissions[key] = compilePermissions(login ? login.permissions : null);
// } else {
// userPermissions[key] = compilePermissions(process.env.PERMISSIONS);
// }
// }
// return testPermission(tested, userPermissions[key]);
} }
let loginsCache = null; // let loginsCache = null;
let loginsLoaded = false; // let loginsLoaded = false;
function getLogins() { // function getLogins() {
if (loginsLoaded) { // if (loginsLoaded) {
return loginsCache; // return loginsCache;
} // }
const res = []; // const res = [];
if (process.env.LOGIN && process.env.PASSWORD) { // if (process.env.LOGIN && process.env.PASSWORD) {
res.push({ // res.push({
login: process.env.LOGIN, // login: process.env.LOGIN,
password: process.env.PASSWORD, // password: process.env.PASSWORD,
permissions: process.env.PERMISSIONS, // permissions: process.env.PERMISSIONS,
}); // });
} // }
if (process.env.LOGINS) { // if (process.env.LOGINS) {
const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim())); // const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim()));
for (const login of logins) { // for (const login of logins) {
const password = process.env[`LOGIN_PASSWORD_${login}`]; // const password = process.env[`LOGIN_PASSWORD_${login}`];
const permissions = process.env[`LOGIN_PERMISSIONS_${login}`]; // const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
if (password) { // if (password) {
res.push({ // res.push({
login, // login,
password, // password,
permissions, // permissions,
}); // });
} // }
} // }
} // } else if (process.env.OAUTH_PERMISSIONS) {
else if (process.env.OAUTH_PERMISSIONS) { // const login_permission_keys = Object.keys(process.env).filter(key => _.startsWith(key, 'LOGIN_PERMISSIONS_'));
const login_permission_keys = Object.keys(process.env).filter((key) => _.startsWith(key, 'LOGIN_PERMISSIONS_')) // for (const permissions_key of login_permission_keys) {
for (const permissions_key of login_permission_keys) { // const login = permissions_key.replace('LOGIN_PERMISSIONS_', '');
const login = permissions_key.replace('LOGIN_PERMISSIONS_', ''); // const permissions = process.env[permissions_key];
const permissions = process.env[permissions_key]; // userPermissions[login] = compilePermissions(permissions);
userPermissions[login] = compilePermissions(permissions); // }
} // }
}
loginsCache = res.length > 0 ? res : null; // loginsCache = res.length > 0 ? res : null;
loginsLoaded = true; // loginsLoaded = true;
return loginsCache; // return loginsCache;
} // }
function connectionHasPermission(connection, req) { function connectionHasPermission(connection, req) {
if (!connection) { if (!connection) {
@@ -87,7 +96,6 @@ function testConnectionPermission(connection, req) {
module.exports = { module.exports = {
hasPermission, hasPermission,
getLogins,
connectionHasPermission, connectionHasPermission,
testConnectionPermission, testConnectionPermission,
}; };

View File

@@ -3,6 +3,7 @@ const os = require('os');
const path = require('path'); const path = require('path');
const processArgs = require('./processArgs'); const processArgs = require('./processArgs');
const isElectron = require('is-electron'); const isElectron = require('is-electron');
const { checkLicense } = require('./checkLicense');
const platform = process.env.OS_OVERRIDE ? process.env.OS_OVERRIDE : process.platform; const platform = process.env.OS_OVERRIDE ? process.env.OS_OVERRIDE : process.platform;
const isWindows = platform === 'win32'; const isWindows = platform === 'win32';
@@ -12,6 +13,8 @@ const isDocker = fs.existsSync('/home/dbgate-docker/public');
const isDevMode = process.env.DEVMODE == '1'; const isDevMode = process.env.DEVMODE == '1';
const isNpmDist = !!global['IS_NPM_DIST']; const isNpmDist = !!global['IS_NPM_DIST'];
const isForkedApi = processArgs.isForkedApi; const isForkedApi = processArgs.isForkedApi;
const licenseError = checkLicense();
const isLicenseValid = licenseError == null;
// function moduleAvailable(name) { // function moduleAvailable(name) {
// try { // try {
@@ -30,6 +33,8 @@ const platformInfo = {
isElectronBundle: isElectron() && !isDevMode, isElectronBundle: isElectron() && !isDevMode,
isForkedApi, isForkedApi,
isElectron: isElectron(), isElectron: isElectron(),
isLicenseValid,
licenseError,
isDevMode, isDevMode,
isNpmDist, isNpmDist,
isSnap: process.env.ELECTRON_SNAP == 'true', isSnap: process.env.ELECTRON_SNAP == 'true',

View File

@@ -31,6 +31,9 @@ module.exports = {
electronSender.send(message, data == null ? null : data); electronSender.send(message, data == null ? null : data);
} }
for (const strmid in sseResponses) { for (const strmid in sseResponses) {
if (data?.strmid && data?.strmid != strmid) {
continue;
}
let skipThisStream = false; let skipThisStream = false;
if (sseResponses[strmid].filter) { if (sseResponses[strmid].filter) {
for (const key in sseResponses[strmid].filter) { for (const key in sseResponses[strmid].filter) {
@@ -47,7 +50,7 @@ module.exports = {
} }
sseResponses[strmid].response?.write( sseResponses[strmid].response?.write(
`event: ${message}\ndata: ${stableStringify(data == null ? null : data)}\n\n` `event: ${message}\ndata: ${stableStringify(data == null ? null : _.omit(data, ['strmid']))}\n\n`
); );
} }
}, },

View File

@@ -67,7 +67,7 @@ module.exports = function useController(app, electron, route, controller) {
} }
if (raw) { if (raw) {
router[method](routeAction, controller[key]); router[method](routeAction, (req, res) => controller[key](req, res));
} else { } else {
router[method](routeAction, async (req, res) => { router[method](routeAction, async (req, res) => {
// if (controller._init && !controller._init_called) { // if (controller._init && !controller._init_called) {

View File

@@ -47,6 +47,8 @@ var config = {
], ],
externals: { externals: {
'better-sqlite3': 'commonjs better-sqlite3', 'better-sqlite3': 'commonjs better-sqlite3',
'oracledb': 'commonjs oracledb',
'msnodesqlv8': 'commonjs msnodesqlv8',
}, },
}; };

View File

@@ -20,6 +20,7 @@ export interface ChangeSetItem {
document?: any; document?: any;
condition?: { [column: string]: string }; condition?: { [column: string]: string };
fields?: { [column: string]: string }; fields?: { [column: string]: string };
insertIfNotExistsFields?: { [column: string]: string };
} }
export interface ChangeSetItemFields { export interface ChangeSetItemFields {
@@ -229,13 +230,23 @@ export function batchUpdateChangeSet(
return changeSet; return changeSet;
} }
function extractFields(item: ChangeSetItem, allowNulls = true): UpdateField[] { function extractFields(item: ChangeSetItem, allowNulls = true, allowedDocumentColumns: string[] = []): UpdateField[] {
return _.keys(item.fields) const allFields = {
.filter(targetColumn => allowNulls || item.fields[targetColumn] != null) ...item.fields,
};
for (const docField in item.document || {}) {
if (allowedDocumentColumns.includes(docField)) {
allFields[docField] = item.document[docField];
}
}
return _.keys(allFields)
.filter(targetColumn => allowNulls || allFields[targetColumn] != null)
.map(targetColumn => ({ .map(targetColumn => ({
targetColumn, targetColumn,
exprType: 'value', exprType: 'value',
value: item.fields[targetColumn], value: allFields[targetColumn],
})); }));
} }
@@ -243,17 +254,19 @@ function changeSetInsertToSql(
item: ChangeSetItem, item: ChangeSetItem,
dbinfo: DatabaseInfo = null dbinfo: DatabaseInfo = null
): [AllowIdentityInsert, Insert, AllowIdentityInsert] { ): [AllowIdentityInsert, Insert, AllowIdentityInsert] {
const fields = extractFields(item, false); const table = dbinfo?.tables?.find(x => x.schemaName == item.schemaName && x.pureName == item.pureName);
const fields = extractFields(
item,
false,
table?.columns?.map(x => x.columnName)
);
if (fields.length == 0) return null; if (fields.length == 0) return null;
let autoInc = false; let autoInc = false;
if (dbinfo) { if (table) {
const table = dbinfo.tables.find(x => x.schemaName == item.schemaName && x.pureName == item.pureName); const autoIncCol = table.columns.find(x => x.autoIncrement);
if (table) { // console.log('autoIncCol', autoIncCol);
const autoIncCol = table.columns.find(x => x.autoIncrement); if (autoIncCol && fields.find(x => x.targetColumn == autoIncCol.columnName)) {
// console.log('autoIncCol', autoIncCol); autoInc = true;
if (autoIncCol && fields.find(x => x.targetColumn == autoIncCol.columnName)) {
autoInc = true;
}
} }
} }
const targetTable = { const targetTable = {
@@ -272,6 +285,9 @@ function changeSetInsertToSql(
targetTable, targetTable,
commandType: 'insert', commandType: 'insert',
fields, fields,
insertWhereNotExistsCondition: item.insertIfNotExistsFields
? compileSimpleChangeSetCondition(item.insertIfNotExistsFields)
: null,
}, },
autoInc autoInc
? { ? {
@@ -320,7 +336,39 @@ export function extractChangeSetCondition(item: ChangeSetItem, alias?: string):
}; };
} }
function changeSetUpdateToSql(item: ChangeSetItem): Update { function compileSimpleChangeSetCondition(fields: { [column: string]: string }): Condition {
function getColumnCondition(columnName: string): Condition {
const value = fields[columnName];
const expr: Expression = {
exprType: 'column',
columnName,
};
if (value == null) {
return {
conditionType: 'isNull',
expr,
};
} else {
return {
conditionType: 'binary',
operator: '=',
left: expr,
right: {
exprType: 'value',
value,
},
};
}
}
return {
conditionType: 'and',
conditions: _.keys(fields).map(columnName => getColumnCondition(columnName)),
};
}
function changeSetUpdateToSql(item: ChangeSetItem, dbinfo: DatabaseInfo = null): Update {
const table = dbinfo?.tables?.find(x => x.schemaName == item.schemaName && x.pureName == item.pureName);
return { return {
from: { from: {
name: { name: {
@@ -329,7 +377,11 @@ function changeSetUpdateToSql(item: ChangeSetItem): Update {
}, },
}, },
commandType: 'update', commandType: 'update',
fields: extractFields(item), fields: extractFields(
item,
true,
table?.columns?.map(x => x.columnName)
),
where: extractChangeSetCondition(item), where: extractChangeSetCondition(item),
}; };
} }
@@ -351,7 +403,7 @@ export function changeSetToSql(changeSet: ChangeSet, dbinfo: DatabaseInfo): Comm
return _.compact( return _.compact(
_.flatten([ _.flatten([
...(changeSet.inserts.map(item => changeSetInsertToSql(item, dbinfo)) as any), ...(changeSet.inserts.map(item => changeSetInsertToSql(item, dbinfo)) as any),
...changeSet.updates.map(changeSetUpdateToSql), ...changeSet.updates.map(item => changeSetUpdateToSql(item, dbinfo)),
...changeSet.deletes.map(changeSetDeleteToSql), ...changeSet.deletes.map(changeSetDeleteToSql),
]) ])
); );
@@ -446,7 +498,12 @@ export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectIn
}; };
} }
export function changeSetInsertDocuments(changeSet: ChangeSet, documents: any[], name?: NamedObjectInfo): ChangeSet { export function changeSetInsertDocuments(
changeSet: ChangeSet,
documents: any[],
name?: NamedObjectInfo,
insertIfNotExistsFieldNames?: string[]
): ChangeSet {
const insertedRows = getChangeSetInsertedRows(changeSet, name); const insertedRows = getChangeSetInsertedRows(changeSet, name);
return { return {
...changeSet, ...changeSet,
@@ -456,6 +513,7 @@ export function changeSetInsertDocuments(changeSet: ChangeSet, documents: any[],
...name, ...name,
insertedRowIndex: insertedRows.length + index, insertedRowIndex: insertedRows.length + index,
fields: doc, fields: doc,
insertIfNotExistsFields: insertIfNotExistsFieldNames ? _.pick(doc, insertIfNotExistsFieldNames) : null,
})), })),
], ],
}; };

View File

@@ -0,0 +1,91 @@
import _ from 'lodash';
import { filterName, isTableColumnUnique } from 'dbgate-tools';
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
import type {
TableInfo,
EngineDriver,
ViewInfo,
ColumnInfo,
NamedObjectInfo,
DatabaseInfo,
ForeignKeyInfo,
} from 'dbgate-types';
import { GridConfig, GridCache, createGridCache } from './GridConfig';
import { Expression, Select, treeToSql, dumpSqlSelect, ColumnRefExpression, Condition } from 'dbgate-sqltree';
export interface CustomGridColumn {
columnName: string;
columnLabel: string;
isPrimaryKey?: boolean;
}
export class CustomGridDisplay extends GridDisplay {
customColumns: CustomGridColumn[];
constructor(
public tableName: NamedObjectInfo,
columns: CustomGridColumn[],
driver: EngineDriver,
config: GridConfig,
setConfig: ChangeConfigFunc,
cache: GridCache,
setCache: ChangeCacheFunc,
dbinfo: DatabaseInfo,
serverVersion,
isReadOnly = false,
public additionalcondition: Condition = null
) {
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
this.customColumns = columns;
this.columns = columns.map(col => ({
columnName: col.columnName,
headerText: col.columnLabel,
uniqueName: col.columnName,
uniquePath: [col.columnName],
isPrimaryKey: col.isPrimaryKey,
isForeignKeyUnique: false,
schemaName: tableName.schemaName,
pureName: tableName.pureName,
}));
this.changeSetKeyFields = columns.filter(x => x.isPrimaryKey).map(x => x.columnName);
this.baseTable = {
...tableName,
columns: this.columns.map(x => ({ ...tableName, columnName: x.columnName, dataType: 'string' })),
foreignKeys: [],
};
this.filterable = true;
this.sortable = true;
this.groupable = false;
this.editable = !isReadOnly;
this.supportsReload = true;
}
createSelect(options = {}) {
const select = this.createSelectBase(
this.tableName,
[],
// @ts-ignore
// this.columns.map(col => ({
// columnName: col.columnName,
// })),
options,
this.customColumns.find(x => x.isPrimaryKey)?.columnName
);
select.selectAll = true;
if (this.additionalcondition) {
if (select.where) {
select.where = {
conditionType: 'and',
conditions: [select.where, this.additionalcondition],
};
} else {
select.where = this.additionalcondition;
}
}
return select;
}
}

View File

@@ -556,9 +556,9 @@ export abstract class GridDisplay {
}; };
} }
createSelectBase(name: NamedObjectInfo, columns: ColumnInfo[], options) { createSelectBase(name: NamedObjectInfo, columns: ColumnInfo[], options, defaultOrderColumnName?: string) {
if (!columns) return null; if (!columns) return null;
const orderColumnName = columns[0].columnName; const orderColumnName = defaultOrderColumnName ?? columns[0]?.columnName;
const select: Select = { const select: Select = {
commandType: 'select', commandType: 'select',
from: { from: {
@@ -734,6 +734,7 @@ export abstract class GridDisplay {
alias: 'count', alias: 'count',
}, },
]; ];
select.selectAll = false;
} }
return select; return select;
// const sql = treeToSql(this.driver, select, dumpSqlSelect); // const sql = treeToSql(this.driver, select, dumpSqlSelect);

View File

@@ -21,3 +21,4 @@ export * from './perspectiveTools';
export * from './DataDuplicator'; export * from './DataDuplicator';
export * from './FreeTableGridDisplay'; export * from './FreeTableGridDisplay';
export * from './FreeTableModel'; export * from './FreeTableModel';
export * from './CustomGridDisplay';

View File

@@ -26,6 +26,8 @@ async function runAndExit(promise) {
} }
} }
program.version(dbgateApi.currentVersion.version);
program program
.option('-s, --server <server>', 'server host') .option('-s, --server <server>', 'server host')
.option('-u, --user <user>', 'user name') .option('-u, --user <user>', 'user name')
@@ -36,7 +38,8 @@ program
'--load-data-condition <condition>', '--load-data-condition <condition>',
'regex, which table data will be loaded and stored in model (in load command)' 'regex, which table data will be loaded and stored in model (in load command)'
) )
.requiredOption('-e, --engine <engine>', 'engine name, eg. mysql@dbgate-plugin-mysql'); .option('-e, --engine <engine>', 'engine name, eg. mysql@dbgate-plugin-mysql')
.option('--commonjs', 'Creates CommonJS module');
program program
.command('deploy <modelFolder>') .command('deploy <modelFolder>')
@@ -115,4 +118,30 @@ program
); );
}); });
program
.command('json-to-model <jsonFile> <modelFolder>')
.description('Converts JSON file to model')
.action((jsonFile, modelFolder) => {
runAndExit(
dbgateApi.jsonToDbModel({
modelFile: jsonFile,
outputDir: modelFolder,
})
);
});
program
.command('model-to-json <modelFolder> <jsonFile>')
.description('Converts model to JSON file')
.action((modelFolder, jsonFile) => {
const { commonjs } = program.opts();
runAndExit(
dbgateApi.dbModelToJson({
modelFolder,
outputFile: jsonFile,
commonjs,
})
);
});
program.parse(process.argv); program.parse(process.argv);

View File

@@ -15,7 +15,7 @@ export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
if (cmd.selectAll) { if (cmd.selectAll) {
dmp.put('* '); dmp.put('* ');
} }
if (cmd.columns) { if (cmd.columns && cmd.columns.length > 0) {
if (cmd.selectAll) dmp.put('&n,'); if (cmd.selectAll) dmp.put('&n,');
dmp.put('&>&n'); dmp.put('&>&n');
dmp.putCollection(',&n', cmd.columns, fld => { dmp.putCollection(',&n', cmd.columns, fld => {
@@ -92,13 +92,28 @@ export function dumpSqlDelete(dmp: SqlDumper, cmd: Delete) {
} }
export function dumpSqlInsert(dmp: SqlDumper, cmd: Insert) { export function dumpSqlInsert(dmp: SqlDumper, cmd: Insert) {
dmp.put( if (cmd.insertWhereNotExistsCondition) {
'^insert ^into %f (%,i) ^values (', dmp.put(
cmd.targetTable, '^insert ^into %f (%,i) ^select ',
cmd.fields.map(x => x.targetColumn) cmd.targetTable,
); cmd.fields.map(x => x.targetColumn)
dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x)); );
dmp.put(')'); dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x));
if (dmp.dialect.requireFromDual) {
dmp.put(' ^from ^dual ');
}
dmp.put(' ^where ^not ^exists (^select * ^from %f ^where ', cmd.targetTable);
dumpSqlCondition(dmp, cmd.insertWhereNotExistsCondition);
dmp.put(')');
} else {
dmp.put(
'^insert ^into %f (%,i) ^values (',
cmd.targetTable,
cmd.fields.map(x => x.targetColumn)
);
dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x));
dmp.put(')');
}
} }
export function dumpSqlCommand(dmp: SqlDumper, cmd: Command) { export function dumpSqlCommand(dmp: SqlDumper, cmd: Command) {

View File

@@ -72,6 +72,10 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
dumpSqlExpression(dmp, condition.expr); dumpSqlExpression(dmp, condition.expr);
dmp.put(' ^in (%,v)', condition.values); dmp.put(' ^in (%,v)', condition.values);
break; break;
case 'notIn':
dumpSqlExpression(dmp, condition.expr);
dmp.put(' ^not ^in (%,v)', condition.values);
break;
case 'rawTemplate': case 'rawTemplate':
let was = false; let was = false;
for (const item of condition.templateSql.split('$$')) { for (const item of condition.templateSql.split('$$')) {

View File

@@ -38,6 +38,7 @@ export interface Insert {
commandType: 'insert'; commandType: 'insert';
fields: UpdateField[]; fields: UpdateField[];
targetTable: NamedObjectInfo; targetTable: NamedObjectInfo;
insertWhereNotExistsCondition?: Condition;
} }
export interface AllowIdentityInsert { export interface AllowIdentityInsert {
@@ -105,6 +106,12 @@ export interface InCondition {
values: any[]; values: any[];
} }
export interface NotInCondition {
conditionType: 'notIn';
expr: Expression;
values: any[];
}
export interface RawTemplateCondition { export interface RawTemplateCondition {
conditionType: 'rawTemplate'; conditionType: 'rawTemplate';
templateSql: string; templateSql: string;
@@ -126,6 +133,7 @@ export type Condition =
| NotExistsCondition | NotExistsCondition
| BetweenCondition | BetweenCondition
| InCondition | InCondition
| NotInCondition
| RawTemplateCondition | RawTemplateCondition
| AnyColumnPassEvalOnlyCondition; | AnyColumnPassEvalOnlyCondition;

View File

@@ -31,7 +31,7 @@ function getConnectionLabelCore(connection, { allowExplicitDatabase = true } = {
return ''; return '';
} }
export default function getConnectionLabel(connection, { allowExplicitDatabase = true, showUnsaved = false } = {}) { export function getConnectionLabel(connection, { allowExplicitDatabase = true, showUnsaved = false } = {}) {
const res = getConnectionLabelCore(connection, { allowExplicitDatabase }); const res = getConnectionLabelCore(connection, { allowExplicitDatabase });
if (res && showUnsaved && connection?.unsaved) { if (res && showUnsaved && connection?.unsaved) {

View File

@@ -20,3 +20,4 @@ export * from './computeDiffRows';
export * from './preloadedRowsTools'; export * from './preloadedRowsTools';
export * from './ScriptWriter'; export * from './ScriptWriter';
export * from './getLogger'; export * from './getLogger';
export * from './getConnectionLabel';

View File

@@ -73,3 +73,44 @@ export function testPermission(tested: string, permissions: CompiledPermissions)
return allow; return allow;
} }
export function testSubPermission(
tested: string,
permissions: string[],
allowSamePermission = true
): true | false | null {
let result = null;
for (const permWithSign of permissions) {
const perm = permWithSign.startsWith('~') ? permWithSign.substring(1) : permWithSign;
const deny = permWithSign.startsWith('~');
if (perm.endsWith('*')) {
const prefix = perm.substring(0, perm.length - 1);
if (tested.startsWith(prefix)) {
result = !deny;
}
} else {
if (allowSamePermission && tested == perm) {
result = !deny;
}
}
}
return result;
}
export function getPredefinedPermissions(predefinedRoleName: string) {
switch (predefinedRoleName) {
case 'superadmin':
return ['*', '~widgets/*', 'widgets/admin', 'widgets/database', '~all-connections'];
case 'logged-user':
return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections'];
case 'anonymous-user':
return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections'];
default:
return null;
}
}
export function sortPermissionsFromTheSameLevel(permissions: string[]) {
return [...permissions.filter(x => x.startsWith('~')), ...permissions.filter(x => !x.startsWith('~'))];
}

View File

@@ -11,6 +11,8 @@ export interface ColumnInfoYaml {
length?: number; length?: number;
autoIncrement?: boolean; autoIncrement?: boolean;
references?: string; references?: string;
refDeleteAction?: string;
refUpdateAction?: string;
primaryKey?: boolean; primaryKey?: boolean;
default?: string; default?: string;
} }
@@ -104,6 +106,8 @@ function convertForeignKeyFromYaml(
constraintType: 'foreignKey', constraintType: 'foreignKey',
pureName: table.name, pureName: table.name,
refTableName: col.references, refTableName: col.references,
deleteAction: col.refDeleteAction,
updateAction: col.refUpdateAction,
columns: [ columns: [
{ {
columnName: col.name, columnName: col.name,
@@ -134,11 +138,15 @@ export function tableInfoFromYaml(table: TableInfoYaml, allTables: TableInfoYaml
return res; return res;
} }
export function databaseInfoFromYamlModel(files: DatabaseModelFile[]): DatabaseInfo { export function databaseInfoFromYamlModel(filesOrDbInfo: DatabaseModelFile[] | DatabaseInfo): DatabaseInfo {
if (!Array.isArray(filesOrDbInfo)) {
return filesOrDbInfo;
}
const model = DatabaseAnalyser.createEmptyStructure(); const model = DatabaseAnalyser.createEmptyStructure();
const tablesYaml = []; const tablesYaml = [];
for (const file of files) { for (const file of filesOrDbInfo) {
if (file.name.endsWith('.table.yaml') || file.name.endsWith('.sql')) { if (file.name.endsWith('.table.yaml') || file.name.endsWith('.sql')) {
if (file.name.endsWith('.table.yaml')) { if (file.name.endsWith('.table.yaml')) {
tablesYaml.push(file.json); tablesYaml.push(file.json);

View File

@@ -34,6 +34,7 @@ export interface SqlDialect {
dropCheck?: boolean; dropCheck?: boolean;
dropReferencesWhenDropTable?: boolean; dropReferencesWhenDropTable?: boolean;
requireFromDual?: boolean;
predefinedDataTypes: string[]; predefinedDataTypes: string[];

View File

@@ -90,7 +90,13 @@ export interface EngineDriver {
profilerChartMeasures?: { label: string; field: string }[]; profilerChartMeasures?: { label: string; field: string }[];
isElectronOnly?: boolean; isElectronOnly?: boolean;
supportedCreateDatabase?: boolean; supportedCreateDatabase?: boolean;
showConnectionField?: (field: string, values: any) => boolean; showConnectionField?: (
field: string,
values: any,
{
config: {},
}
) => boolean;
showConnectionTab?: (tab: 'ssl' | 'sshTunnel', values: any) => boolean; showConnectionTab?: (tab: 'ssl' | 'sshTunnel', values: any) => boolean;
beforeConnectionSave?: (values: any) => any; beforeConnectionSave?: (values: any) => any;
databaseUrlPlaceholder?: string; databaseUrlPlaceholder?: string;
@@ -143,6 +149,8 @@ export interface EngineDriver {
summaryCommand(pool, command, row): Promise<void>; summaryCommand(pool, command, row): Promise<void>;
startProfiler(pool, options): Promise<any>; startProfiler(pool, options): Promise<any>;
stopProfiler(pool, profiler): Promise<void>; stopProfiler(pool, profiler): Promise<void>;
getRedirectAuthUrl(connection, options): Promise<string>;
getAuthTokenFromCode(connection, options): Promise<string>;
analyserClass?: any; analyserClass?: any;
dumperClass?: any; dumperClass?: any;

View File

@@ -14,13 +14,15 @@
// import { shouldWaitForElectronInitialize } from './utility/getElectron'; // import { shouldWaitForElectronInitialize } from './utility/getElectron';
import { subscribeConnectionPingers } from './utility/connectionsPinger'; import { subscribeConnectionPingers } from './utility/connectionsPinger';
import { subscribePermissionCompiler } from './utility/hasPermission'; import { subscribePermissionCompiler } from './utility/hasPermission';
import { apiCall } from './utility/api'; import { apiCall, installNewVolatileConnectionListener } from './utility/api';
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders'; import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
import AppTitleProvider from './utility/AppTitleProvider.svelte'; import AppTitleProvider from './utility/AppTitleProvider.svelte';
import getElectron from './utility/getElectron'; import getElectron from './utility/getElectron';
import AppStartInfo from './widgets/AppStartInfo.svelte'; import AppStartInfo from './widgets/AppStartInfo.svelte';
import SettingsListener from './utility/SettingsListener.svelte'; import SettingsListener from './utility/SettingsListener.svelte';
import { handleAuthOnStartup, handleOauthCallback } from './clientAuth'; import { handleAuthOnStartup } from './clientAuth';
export let isAdminPage = false;
let loadedApi = false; let loadedApi = false;
let loadedPlugins = false; let loadedPlugins = false;
@@ -35,19 +37,22 @@
// console.log('************** LOADING API'); // console.log('************** LOADING API');
const config = await getConfig(); const config = await getConfig();
await handleAuthOnStartup(config); await handleAuthOnStartup(config, isAdminPage);
const connections = await apiCall('connections/list'); const connections = await apiCall('connections/list');
const settings = await getSettings(); const settings = await getSettings();
const apps = await getUsedApps(); const apps = await getUsedApps();
loadedApi = settings && connections && config && apps; const loadedApiValue = !!(settings && connections && config && apps);
if (loadedApi) { if (loadedApiValue) {
subscribeApiDependendStores(); subscribeApiDependendStores();
subscribeConnectionPingers(); subscribeConnectionPingers();
subscribePermissionCompiler(); subscribePermissionCompiler();
installNewVolatileConnectionListener();
} }
loadedApi = loadedApiValue;
if (!loadedApi) { if (!loadedApi) {
console.log('API not initialized correctly, trying again in 1s'); console.log('API not initialized correctly, trying again in 1s');
setTimeout(loadApi, 1000); setTimeout(loadApi, 1000);

View File

@@ -0,0 +1,95 @@
<script lang="ts">
import { onMount } from 'svelte';
import { useConfig } from './utility/metadataLoaders';
import ErrorInfo from './elements/ErrorInfo.svelte';
import Link from './elements/Link.svelte';
import { internalRedirectTo } from './clientAuth';
const config = useConfig();
const params = new URLSearchParams(location.search);
const error = params.get('error');
onMount(() => {
const removed = document.getElementById('starting_dbgate_zero');
if (removed) removed.remove();
});
</script>
<div class="root theme-light theme-type-light">
<div class="text">DbGate</div>
<div class="wrap">
<div class="logo">
<img class="img" src="logo192.png" />
</div>
<div class="box">
<div class="heading">Configuration error</div>
{#if $config?.isLicenseValid == false}
<ErrorInfo
message={`Invalid license. Please contact sales@dbgate.eu for more details. ${$config?.licenseError}`}
/>
{:else if error}
<ErrorInfo message={error} />
{:else}
<ErrorInfo message="No error found, try to open app again" />
<div class="m-2">
<Link onClick={() => internalRedirectTo('/')}>Back to app</Link>
</div>
{/if}
</div>
</div>
</div>
<style>
.logo {
display: flex;
margin-bottom: 1rem;
align-items: center;
justify-content: center;
}
.img {
width: 80px;
}
.text {
position: fixed;
top: 1rem;
left: 1rem;
font-size: 30pt;
font-family: monospace;
color: var(--theme-bg-2);
text-transform: uppercase;
}
.root {
color: var(--theme-font-1);
display: flex;
justify-content: center;
background-color: var(--theme-bg-1);
align-items: baseline;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.box {
width: 600px;
max-width: 80vw;
/* max-width: 600px;
width: 40vw; */
border: 1px solid var(--theme-border);
border-radius: 4px;
background-color: var(--theme-bg-0);
}
.wrap {
margin-top: 20vh;
}
.heading {
text-align: center;
margin: 1em;
font-size: xx-large;
}
</style>

View File

@@ -1,16 +1,46 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { internalRedirectTo } from './clientAuth'; import { internalRedirectTo } from './clientAuth';
import FormButton from './forms/FormButton.svelte';
import FormPasswordField from './forms/FormPasswordField.svelte'; import FormPasswordField from './forms/FormPasswordField.svelte';
import FormProvider from './forms/FormProvider.svelte';
import FormSubmit from './forms/FormSubmit.svelte'; import FormSubmit from './forms/FormSubmit.svelte';
import FormTextField from './forms/FormTextField.svelte'; import FormTextField from './forms/FormTextField.svelte';
import { apiCall, enableApi } from './utility/api'; import { apiCall, enableApi, strmid } from './utility/api';
import { useConfig } from './utility/metadataLoaders';
import ErrorInfo from './elements/ErrorInfo.svelte';
import FormSelectField from './forms/FormSelectField.svelte';
import { writable } from 'svelte/store';
import FormProviderCore from './forms/FormProviderCore.svelte';
import { openWebLink } from './utility/exportFileTools';
import FontIcon from './icons/FontIcon.svelte';
import createRef from './utility/createRef';
export let isAdminPage;
const config = useConfig();
let availableConnections = null;
let isTesting = false;
const testIdRef = createRef(0);
let sqlConnectResult;
const values = writable({ databaseServer: null });
$: selectedConnection = availableConnections?.find(x => x.conid == $values.databaseServer);
async function loadAvailableServers() {
availableConnections = await apiCall('storage/get-connections-for-login-page');
if (availableConnections?.length > 0) {
values.set({ databaseServer: availableConnections[0].conid });
}
}
onMount(() => { onMount(() => {
const removed = document.getElementById('starting_dbgate_zero'); const removed = document.getElementById('starting_dbgate_zero');
if (removed) removed.remove(); if (removed) removed.remove();
if (!isAdminPage) {
loadAvailableServers();
}
}); });
</script> </script>
@@ -22,31 +52,129 @@
</div> </div>
<div class="box"> <div class="box">
<div class="heading">Log In</div> <div class="heading">Log In</div>
<FormProvider> <FormProviderCore {values}>
<FormTextField label="Username" name="login" autocomplete="username" saveOnInput /> {#if !isAdminPage && availableConnections}
<FormPasswordField label="Password" name="password" autocomplete="current-password" saveOnInput /> <FormSelectField
label="Database server"
name="databaseServer"
isNative
options={availableConnections.map(conn => ({ value: conn.conid, label: conn.label }))}
/>
{/if}
{#if selectedConnection}
{#if selectedConnection.passwordMode == 'askUser'}
<FormTextField label="Username" name="login" autocomplete="username" saveOnInput />
{/if}
{#if selectedConnection.passwordMode == 'askUser' || selectedConnection.passwordMode == 'askPassword'}
<FormPasswordField label="Password" name="password" autocomplete="current-password" saveOnInput />
{/if}
{:else}
{#if !isAdminPage}
<FormTextField label="Username" name="login" autocomplete="username" saveOnInput />
{/if}
<FormPasswordField label="Password" name="password" autocomplete="current-password" saveOnInput />
{/if}
{#if isAdminPage && $config && !$config.isAdminLoginForm}
<ErrorInfo message="Admin login is not configured. Please set ADMIN_PASSWORD environment variable" />
{/if}
{#if isTesting}
<div class="ml-5">
<FontIcon icon="icon loading" /> Testing connection
</div>
{/if}
{#if !isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error'}
<div class="error-result ml-5">
Connect failed: <FontIcon icon="img error" />
{sqlConnectResult.error}
</div>
{/if}
<div class="submit"> <div class="submit">
<FormSubmit {#if selectedConnection?.useRedirectDbLogin}
value="Log In" <FormSubmit
on:click={async e => { value="Open database login page"
enableApi(); on:click={async e => {
const resp = await apiCall('auth/login', e.detail); const state = `dbg-dblogin:${strmid}:${selectedConnection?.conid}`;
if (resp.error) { sessionStorage.setItem('dbloginAuthState', state);
internalRedirectTo(`/?page=not-logged&error=${encodeURIComponent(resp.error)}`); // openWebLink(
return; // `connections/dblogin?conid=${selectedConnection?.conid}&state=${encodeURIComponent(state)}&redirectUri=${
} // location.origin + location.pathname
const { accessToken } = resp; // }`
if (accessToken) { // );
localStorage.setItem('accessToken', accessToken); internalRedirectTo(
internalRedirectTo('/'); `/connections/dblogin?conid=${selectedConnection?.conid}&state=${encodeURIComponent(state)}&redirectUri=${
return; location.origin + location.pathname
} }`
internalRedirectTo(`/?page=not-logged`); );
}} }}
/> />
{:else if selectedConnection}
<FormSubmit
value="Log In"
on:click={async e => {
if (selectedConnection.passwordMode == 'askUser' || selectedConnection.passwordMode == 'askPassword') {
enableApi();
isTesting = true;
testIdRef.update(x => x + 1);
const testid = testIdRef.get();
const resp = await apiCall('connections/dblogin-auth', {
conid: selectedConnection.conid,
user: $values['login'],
password: $values['password'],
});
if (testIdRef.get() != testid) return;
isTesting = false;
if (resp.accessToken) {
localStorage.setItem('accessToken', resp.accessToken);
internalRedirectTo('/');
} else {
sqlConnectResult = resp;
}
} else {
enableApi();
const resp = await apiCall('connections/dblogin-auth', {
conid: selectedConnection.conid,
});
localStorage.setItem('accessToken', resp.accessToken);
internalRedirectTo('/');
}
}}
/>
{:else}
<FormSubmit
value={isAdminPage ? 'Log In as Administrator' : 'Log In'}
on:click={async e => {
enableApi();
const resp = await apiCall('auth/login', {
isAdminPage,
...e.detail,
});
if (resp.error) {
internalRedirectTo(
`/?page=not-logged&error=${encodeURIComponent(resp.error)}&is-admin=${isAdminPage ? 'true' : ''}`
);
return;
}
const { accessToken } = resp;
if (accessToken) {
localStorage.setItem(isAdminPage ? 'adminAccessToken' : 'accessToken', accessToken);
if (isAdminPage) {
internalRedirectTo('/?page=admin');
} else {
internalRedirectTo('/');
}
return;
}
internalRedirectTo(`/?page=not-logged`);
}}
/>
{/if}
</div> </div>
</FormProvider> </FormProviderCore>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import FormStyledButton from './buttons/FormStyledButton.svelte'; import FormStyledButton from './buttons/FormStyledButton.svelte';
import { doLogout, redirectToLogin } from './clientAuth'; import { doLogout, redirectToAdminLogin, redirectToLogin } from './clientAuth';
onMount(() => { onMount(() => {
const removed = document.getElementById('starting_dbgate_zero'); const removed = document.getElementById('starting_dbgate_zero');
@@ -10,9 +10,14 @@
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const error = params.get('error'); const error = params.get('error');
const isAdmin = params.get('is-admin') == 'true';
function handleLogin() { function handleLogin() {
redirectToLogin(undefined, true); if (isAdmin) {
redirectToAdminLogin();
} else {
redirectToLogin(undefined, true);
}
} }
</script> </script>

View File

@@ -98,7 +98,6 @@
import openNewTab from '../utility/openNewTab'; import openNewTab from '../utility/openNewTab';
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte'; import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
import getElectron from '../utility/getElectron'; import getElectron from '../utility/getElectron';
import getConnectionLabel from '../utility/getConnectionLabel';
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders'; import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
import { getLocalStorage } from '../utility/storageCache'; import { getLocalStorage } from '../utility/storageCache';
import { apiCall, removeVolatileMapping } from '../utility/api'; import { apiCall, removeVolatileMapping } from '../utility/api';
@@ -106,6 +105,7 @@
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte'; import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
import AboutModal from '../modals/AboutModal.svelte'; import AboutModal from '../modals/AboutModal.svelte';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { getConnectionLabel } from 'dbgate-tools';
export let data; export let data;
export let passProps; export let passProps;

View File

@@ -340,7 +340,6 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import getConnectionLabel from '../utility/getConnectionLabel';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import _, { find } from 'lodash'; import _, { find } from 'lodash';
@@ -365,7 +364,7 @@
import openNewTab from '../utility/openNewTab'; import openNewTab from '../utility/openNewTab';
import AppObjectCore from './AppObjectCore.svelte'; import AppObjectCore from './AppObjectCore.svelte';
import { showSnackbarError, showSnackbarSuccess } from '../utility/snackbar'; import { showSnackbarError, showSnackbarSuccess } from '../utility/snackbar';
import { findEngineDriver } from 'dbgate-tools'; import { findEngineDriver, getConnectionLabel } from 'dbgate-tools';
import InputTextModal from '../modals/InputTextModal.svelte'; import InputTextModal from '../modals/InputTextModal.svelte';
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders'; import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
import { openJsonDocument } from '../tabs/JsonTab.svelte'; import { openJsonDocument } from '../tabs/JsonTab.svelte';

View File

@@ -776,7 +776,7 @@
pinnedTables, pinnedTables,
} from '../stores'; } from '../stores';
import openNewTab from '../utility/openNewTab'; import openNewTab from '../utility/openNewTab';
import { filterName, generateDbPairingId, getAlterDatabaseScript } from 'dbgate-tools'; import { filterName, generateDbPairingId, getAlterDatabaseScript, getConnectionLabel } from 'dbgate-tools';
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders'; import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
import fullDisplayName from '../utility/fullDisplayName'; import fullDisplayName from '../utility/fullDisplayName';
import ImportExportModal from '../modals/ImportExportModal.svelte'; import ImportExportModal from '../modals/ImportExportModal.svelte';
@@ -784,7 +784,6 @@
import { findEngineDriver } from 'dbgate-tools'; import { findEngineDriver } from 'dbgate-tools';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte'; import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
import getConnectionLabel from '../utility/getConnectionLabel';
import { exportQuickExportFile } from '../utility/exportFileTools'; import { exportQuickExportFile } from '../utility/exportFileTools';
import createQuickExportMenu from '../utility/createQuickExportMenu'; import createQuickExportMenu from '../utility/createQuickExportMenu';
import ConfirmSqlModal, { saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte'; import ConfirmSqlModal, { saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { filterName } from 'dbgate-tools'; import { filterName, getConnectionLabel } from 'dbgate-tools';
interface FileTypeHandler { interface FileTypeHandler {
icon: string; icon: string;
@@ -100,7 +100,6 @@
import { currentDatabase } from '../stores'; import { currentDatabase } from '../stores';
import { apiCall } from '../utility/api'; import { apiCall } from '../utility/api';
import getConnectionLabel from '../utility/getConnectionLabel';
import hasPermission from '../utility/hasPermission'; import hasPermission from '../utility/hasPermission';
import openNewTab from '../utility/openNewTab'; import openNewTab from '../utility/openNewTab';

View File

@@ -4,6 +4,7 @@
import { useDatabaseList } from '../utility/metadataLoaders'; import { useDatabaseList } from '../utility/metadataLoaders';
import AppObjectList from './AppObjectList.svelte'; import AppObjectList from './AppObjectList.svelte';
import * as databaseAppObject from './DatabaseAppObject.svelte'; import * as databaseAppObject from './DatabaseAppObject.svelte';
import { volatileConnectionMapStore } from '../utility/api';
export let filter; export let filter;
export let data; export let data;

View File

@@ -36,6 +36,7 @@
display: flex; display: flex;
flex: 1; flex: 1;
position: relative; position: relative;
max-height: 100%;
} }
.toolstrip { .toolstrip {

View File

@@ -1,5 +1,6 @@
import { apiCall, enableApi } from './utility/api'; import { apiCall, enableApi } from './utility/api';
import { getConfig } from './utility/metadataLoaders'; import { getConfig } from './utility/metadataLoaders';
import { isAdminPage } from './utility/pageDefs';
export function isOauthCallback() { export function isOauthCallback() {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
@@ -11,6 +12,29 @@ export function isOauthCallback() {
); );
} }
export function isDbLoginCallback() {
const params = new URLSearchParams(location.search);
const sentCode = params.get('code');
const sentState = params.get('state');
return (
sentCode && sentState && sentState.startsWith('dbg-dblogin:') && sentState == localStorage.getItem('dbloginState')
);
}
export function isDbLoginAuthCallback() {
const params = new URLSearchParams(location.search);
const sentCode = params.get('code');
const sentState = params.get('state');
return (
sentCode &&
sentState &&
sentState.startsWith('dbg-dblogin:') &&
sentState == sessionStorage.getItem('dbloginAuthState')
);
}
export function handleOauthCallback() { export function handleOauthCallback() {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const sentCode = params.get('code'); const sentCode = params.get('code');
@@ -36,10 +60,68 @@ export function handleOauthCallback() {
return true; return true;
} }
if (isDbLoginCallback()) {
const [_prefix, strmid, conid] = localStorage.getItem('dbloginState').split(':');
localStorage.removeItem('dbloginState');
apiCall('connections/dblogin-token', {
code: sentCode,
conid,
strmid,
redirectUri: location.origin + location.pathname,
}).then(authResp => {
if (authResp.success) {
window.close();
} else if (authResp.error) {
internalRedirectTo(`/?page=error&error=${encodeURIComponent(authResp.error)}`);
} else {
internalRedirectTo(`/?page=error`);
}
});
return true;
}
if (isDbLoginAuthCallback()) {
const [_prefix, strmid, conid] = sessionStorage.getItem('dbloginAuthState').split(':');
sessionStorage.removeItem('dbloginAuthState');
apiCall('connections/dblogin-auth-token', {
code: sentCode,
conid,
redirectUri: location.origin + location.pathname,
}).then(authResp => {
if (authResp.accessToken) {
localStorage.setItem('accessToken', authResp.accessToken);
internalRedirectTo('/');
} else if (authResp.error) {
internalRedirectTo(`/?page=error&error=${encodeURIComponent(authResp.error)}`);
} else {
internalRedirectTo(`/?page=error`);
}
});
return true;
}
return false; return false;
} }
export async function handleAuthOnStartup(config) { export async function handleAuthOnStartup(config, isAdminPage = false) {
if (!config.isLicenseValid) {
internalRedirectTo(`/?page=error`);
return;
}
if (config.isAdminLoginForm && isAdminPage) {
if (localStorage.getItem('adminAccessToken')) {
return;
}
redirectToAdminLogin();
return;
}
if (config.oauth) { if (config.oauth) {
console.log('OAUTH callback URL:', location.origin + location.pathname); console.log('OAUTH callback URL:', location.origin + location.pathname);
} }
@@ -52,6 +134,11 @@ export async function handleAuthOnStartup(config) {
} }
} }
export async function redirectToAdminLogin() {
internalRedirectTo('/?page=admin-login');
return;
}
export async function redirectToLogin(config = null, force = false) { export async function redirectToLogin(config = null, force = false) {
if (!config) { if (!config) {
enableApi(); enableApi();
@@ -61,7 +148,7 @@ export async function redirectToLogin(config = null, force = false) {
if (config.isLoginForm) { if (config.isLoginForm) {
if (!force) { if (!force) {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
if (params.get('page') == 'login' || params.get('page') == 'not-logged') { if (params.get('page') == 'login' || params.get('page') == 'admin-login' || params.get('page') == 'not-logged') {
return; return;
} }
} }
@@ -93,15 +180,18 @@ export async function doLogout() {
enableApi(); enableApi();
const config = await getConfig(); const config = await getConfig();
if (config.oauth) { if (config.oauth) {
localStorage.removeItem('accessToken'); localStorage.removeItem(isAdminPage() ? 'adminAccessToken' : 'accessToken');
if (config.oauthLogout) { if (config.oauthLogout) {
window.location.href = config.oauthLogout; window.location.href = config.oauthLogout;
} else { } else {
internalRedirectTo('/?page=not-logged'); internalRedirectTo('/?page=not-logged');
} }
} else if (config.isLoginForm) { } else if (config.isLoginForm) {
localStorage.removeItem('accessToken'); localStorage.removeItem(isAdminPage() ? 'adminAccessToken' : 'accessToken');
internalRedirectTo('/?page=not-logged'); internalRedirectTo(`/?page=not-logged&is-admin=${isAdminPage() ? 'true' : ''}`);
} else if (config.isAdminLoginForm && isAdminPage()) {
localStorage.removeItem('adminAccessToken');
internalRedirectTo('/?page=admin-login&is-admin=true');
} else { } else {
window.location.href = 'config/logout'; window.location.href = 'config/logout';
} }

View File

@@ -60,7 +60,7 @@
</script> </script>
<script> <script>
import { filterName } from 'dbgate-tools'; import { filterName, getConnectionLabel } from 'dbgate-tools';
import _ from 'lodash'; import _ from 'lodash';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@@ -75,7 +75,6 @@
visibleCommandPalette, visibleCommandPalette,
} from '../stores'; } from '../stores';
import clickOutside from '../utility/clickOutside'; import clickOutside from '../utility/clickOutside';
import getConnectionLabel from '../utility/getConnectionLabel';
import { isElectronAvailable } from '../utility/getElectron'; import { isElectronAvailable } from '../utility/getElectron';
import keycodes from '../utility/keycodes'; import keycodes from '../utility/keycodes';
import { useConnectionList, useDatabaseInfo } from '../utility/metadataLoaders'; import { useConnectionList, useDatabaseInfo } from '../utility/metadataLoaders';

View File

@@ -1,7 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import { recentDatabases, currentDatabase, getRecentDatabases } from '../stores'; import { recentDatabases, currentDatabase, getRecentDatabases } from '../stores';
import getConnectionLabel from '../utility/getConnectionLabel';
import registerCommand from './registerCommand'; import registerCommand from './registerCommand';
import { getConnectionLabel } from 'dbgate-tools';
currentDatabase.subscribe(value => { currentDatabase.subscribe(value => {
if (!value) return; if (!value) return;

View File

@@ -104,7 +104,7 @@ registerCommand({
category: 'New', category: 'New',
toolbarOrder: 1, toolbarOrder: 1,
name: 'Connection', name: 'Connection',
testEnabled: () => !getCurrentConfig()?.runAsPortal, testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase,
onClick: () => { onClick: () => {
openNewTab({ openNewTab({
title: 'New Connection', title: 'New Connection',
@@ -121,7 +121,7 @@ registerCommand({
toolbarName: 'Add connection folder', toolbarName: 'Add connection folder',
category: 'New', category: 'New',
toolbarOrder: 1, toolbarOrder: 1,
name: 'Connection', name: 'Connection folder',
testEnabled: () => !getCurrentConfig()?.runAsPortal, testEnabled: () => !getCurrentConfig()?.runAsPortal,
onClick: () => { onClick: () => {
showModal(InputTextModal, { showModal(InputTextModal, {
@@ -551,7 +551,7 @@ registerCommand({
id: 'app.logout', id: 'app.logout',
category: 'App', category: 'App',
name: 'Logout', name: 'Logout',
testEnabled: () => getCurrentConfig()?.login != null, testEnabled: () => getCurrentConfig()?.isUserLoggedIn,
onClick: doLogout, onClick: doLogout,
}); });
@@ -559,7 +559,7 @@ registerCommand({
id: 'app.disconnect', id: 'app.disconnect',
category: 'App', category: 'App',
name: 'Disconnect', name: 'Disconnect',
testEnabled: () => getCurrentConfig()?.singleConnection != null, testEnabled: () => getCurrentConfig()?.singleConnection != null && !getCurrentConfig()?.isUserLoggedIn,
onClick: () => disconnectServerConnection(getCurrentConfig()?.singleConnection?._id), onClick: () => disconnectServerConnection(getCurrentConfig()?.singleConnection?._id),
}); });
@@ -873,7 +873,6 @@ registerCommand({
onClick: () => showModal(UploadErrorModal), onClick: () => showModal(UploadErrorModal),
}); });
const electron = getElectron(); const electron = getElectron();
if (electron) { if (electron) {
electron.addEventListener('run-command', (e, commandId) => runCommand(commandId)); electron.addEventListener('run-command', (e, commandId) => runCommand(commandId));

View File

@@ -135,12 +135,13 @@
export let macroPreview; export let macroPreview;
export let macroValues; export let macroValues;
export let selectedCellsPublished;
export let setLoadedRows = null; export let setLoadedRows = null;
export let onPublishedCellsChanged;
// export let onChangeGrider = undefined; // export let onChangeGrider = undefined;
let loadedRows = []; let loadedRows = [];
let publishedCells = [];
export const activator = createActivator('CollectionDataGridCore', false); export const activator = createActivator('CollectionDataGridCore', false);
@@ -152,7 +153,7 @@
display, display,
macroPreview, macroPreview,
macroValues, macroValues,
selectedCellsPublished() publishedCells
); );
// $: console.log('GRIDER', grider); // $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider); // $: if (onChangeGrider) onChangeGrider(grider);
@@ -239,7 +240,12 @@
{dataPageAvailable} {dataPageAvailable}
{loadRowCount} {loadRowCount}
setLoadedRows={handleSetLoadedRows} setLoadedRows={handleSetLoadedRows}
bind:selectedCellsPublished onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview} frameSelection={!!macroPreview}
onOpenQuery={openQuery} onOpenQuery={openQuery}
{grider} {grider}

View File

@@ -89,12 +89,15 @@
export let onRunMacro; export let onRunMacro;
export let hasMultiColumnFilter = false; export let hasMultiColumnFilter = false;
export let setLoadedRows = null; export let setLoadedRows = null;
export let hideGridLeftColumn = false;
export let onPublishedCellsChanged;
let loadedRows; let loadedRows;
let publishedCells = [];
export const activator = createActivator('DataGrid', false); export const activator = createActivator('DataGrid', false);
let selectedCellsPublished = () => [];
let domColumnManager; let domColumnManager;
const selectedMacro = writable(null); const selectedMacro = writable(null);
@@ -110,7 +113,7 @@
$: isJsonView = !!config?.isJsonView; $: isJsonView = !!config?.isJsonView;
const handleExecuteMacro = () => { const handleExecuteMacro = () => {
onRunMacro($selectedMacro, extractMacroValuesForMacro($macroValues, $selectedMacro), selectedCellsPublished()); onRunMacro($selectedMacro, extractMacroValuesForMacro($macroValues, $selectedMacro), publishedCells);
$selectedMacro = null; $selectedMacro = null;
}; };
@@ -122,7 +125,7 @@
export function switchToView(view) { export function switchToView(view) {
if (view == 'form') { if (view == 'form') {
display.switchToFormView(selectedCellsPublished()[0]?.row); display.switchToFormView(publishedCells[0]?.row);
} }
if (view == 'table') { if (view == 'table') {
setConfig(cfg => ({ setConfig(cfg => ({
@@ -162,7 +165,7 @@
<HorizontalSplitter <HorizontalSplitter
initialValue={getInitialManagerSize()} initialValue={getInitialManagerSize()}
bind:size={managerSize} bind:size={managerSize}
hideFirst={$collapsedLeftColumnStore} hideFirst={hideGridLeftColumn || $collapsedLeftColumnStore}
> >
<div class="left" slot="1"> <div class="left" slot="1">
<WidgetColumnBar> <WidgetColumnBar>
@@ -219,7 +222,12 @@
macroValues={extractMacroValuesForMacro($macroValues, $selectedMacro)} macroValues={extractMacroValuesForMacro($macroValues, $selectedMacro)}
macroPreview={$selectedMacro} macroPreview={$selectedMacro}
{setLoadedRows} {setLoadedRows}
bind:selectedCellsPublished onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
onChangeSelectedColumns={cols => { onChangeSelectedColumns={cols => {
if (domColumnManager) domColumnManager.setSelectedColumns(cols); if (domColumnManager) domColumnManager.setSelectedColumns(cols);
}} }}

View File

@@ -412,6 +412,7 @@
export let isLoading = false; export let isLoading = false;
export let allRowCount = undefined; export let allRowCount = undefined;
export let onReferenceSourceChanged = undefined; export let onReferenceSourceChanged = undefined;
export let onPublishedCellsChanged = undefined;
export let onReferenceClick = undefined; export let onReferenceClick = undefined;
export let onChangeSelectedColumns = undefined; export let onChangeSelectedColumns = undefined;
// export let onSelectedCellsPublishedChanged = undefined; // export let onSelectedCellsPublishedChanged = undefined;
@@ -422,12 +423,13 @@
export let schemaName = undefined; export let schemaName = undefined;
export let allowDefineVirtualReferences = false; export let allowDefineVirtualReferences = false;
export let formatterFunction; export let formatterFunction;
export let hideGridLeftColumn;
export let isLoadedAll; export let isLoadedAll;
export let loadedTime; export let loadedTime;
export let changeSetStore; export let changeSetStore;
export let isDynamicStructure = false; export let isDynamicStructure = false;
export let selectedCellsPublished = () => []; // export let selectedCellsPublished = () => [];
export let collapsedLeftColumnStore; export let collapsedLeftColumnStore;
export let multipleGridsOnTab = false; export let multipleGridsOnTab = false;
export let tabControlHiddenTab = false; export let tabControlHiddenTab = false;
@@ -1077,16 +1079,29 @@
} }
const lastPublishledSelectedCellsRef = createRef(''); const lastPublishledSelectedCellsRef = createRef('');
const changeSetValueRef = createRef(null);
$: { $: {
const stringified = stableStringify(selectedCells); const stringified = stableStringify(selectedCells);
if (lastPublishledSelectedCellsRef.get() != stringified) { if (
lastPublishledSelectedCellsRef.set(stringified); (lastPublishledSelectedCellsRef.get() != stringified || changeSetValueRef.get() != $changeSetStore?.value) &&
const cellsValue = () => getCellsPublished(selectedCells); realColumnUniqueNames?.length > 0
selectedCellsPublished = cellsValue; ) {
$selectedCellsCallback = cellsValue; tick().then(() => {
const rowIndexes = _.uniq(selectedCells.map(x => x[0]));
if (rowIndexes.every(x => grider.getRowData(x))) {
lastPublishledSelectedCellsRef.set(stringified);
changeSetValueRef.set($changeSetStore?.value);
$selectedCellsCallback = () => getCellsPublished(selectedCells);
if (onChangeSelectedColumns) onChangeSelectedColumns(getSelectedColumns().map(x => x.columnName)); if (onChangeSelectedColumns) {
// if (onSelectedCellsPublishedChanged) onSelectedCellsPublishedChanged(getCellsPublished(selectedCells)); onChangeSelectedColumns(getSelectedColumns().map(x => x.columnName));
}
if (onPublishedCellsChanged) {
onPublishedCellsChanged(getCellsPublished(selectedCells));
}
}
});
} }
} }
@@ -1120,6 +1135,8 @@
column, column,
value: rowData && rowData[column], value: rowData && rowData[column],
engine: display?.driver, engine: display?.driver,
condition: display?.getChangeSetCondition(rowData),
insertedRowIndex: grider?.getInsertedRowIndex(row),
}; };
}) })
.filter(x => x.column); .filter(x => x.column);
@@ -1816,10 +1833,12 @@
data-col="header" data-col="header"
style={`width:${headerColWidth}px; min-width:${headerColWidth}px; max-width:${headerColWidth}px`} style={`width:${headerColWidth}px; min-width:${headerColWidth}px; max-width:${headerColWidth}px`}
> >
<CollapseButton {#if !hideGridLeftColumn}
collapsed={$collapsedLeftColumnStore} <CollapseButton
on:click={() => collapsedLeftColumnStore.update(x => !x)} collapsed={$collapsedLeftColumnStore}
/> on:click={() => collapsedLeftColumnStore.update(x => !x)}
/>
{/if}
</td> </td>
{#each visibleRealColumns as col (col.uniqueName)} {#each visibleRealColumns as col (col.uniqueName)}
<td <td

View File

@@ -69,11 +69,13 @@
export let macroPreview; export let macroPreview;
export let macroValues; export let macroValues;
export let selectedCellsPublished = () => []; export let onPublishedCellsChanged
export const activator = createActivator('JslDataGridCore', false); export const activator = createActivator('JslDataGridCore', false);
export let setLoadedRows; export let setLoadedRows;
let publishedCells = [];
let loadedRows = []; let loadedRows = [];
let domGrid; let domGrid;
@@ -113,7 +115,7 @@
display, display,
macroPreview, macroPreview,
macroValues, macroValues,
selectedCellsPublished(), publishedCells,
true true
); );
} }
@@ -199,7 +201,12 @@
bind:this={domGrid} bind:this={domGrid}
{...$$props} {...$$props}
setLoadedRows={handleSetLoadedRows} setLoadedRows={handleSetLoadedRows}
bind:selectedCellsPublished onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
{loadDataPage} {loadDataPage}
{dataPageAvailable} {dataPageAvailable}
{loadRowCount} {loadRowCount}

View File

@@ -12,7 +12,6 @@
export let grider; export let grider;
export let display; export let display;
export let masterLoadedTime = undefined; export let masterLoadedTime = undefined;
export let selectedCellsPublished;
export let rowCountLoaded = null; export let rowCountLoaded = null;
export let preprocessLoadedRow = null; export let preprocessLoadedRow = null;
@@ -128,7 +127,6 @@
<DataGridCore <DataGridCore
{...$$props} {...$$props}
bind:this={domGrid} bind:this={domGrid}
bind:selectedCellsPublished
onLoadNextData={handleLoadNextData} onLoadNextData={handleLoadNextData}
{errorMessage} {errorMessage}
{isLoading} {isLoading}

View File

@@ -95,7 +95,9 @@
export let macroPreview; export let macroPreview;
export let macroValues; export let macroValues;
export let selectedCellsPublished = () => []; export let onPublishedCellsChanged;
let publishedCells = [];
// export let onChangeGrider = undefined; // export let onChangeGrider = undefined;
@@ -116,7 +118,7 @@
display, display,
macroPreview, macroPreview,
macroValues, macroValues,
selectedCellsPublished() publishedCells
); );
} }
} }
@@ -224,7 +226,12 @@
{dataPageAvailable} {dataPageAvailable}
{loadRowCount} {loadRowCount}
setLoadedRows={handleSetLoadedRows} setLoadedRows={handleSetLoadedRows}
bind:selectedCellsPublished onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview} frameSelection={!!macroPreview}
{grider} {grider}
{display} {display}

View File

@@ -62,18 +62,25 @@
.main { .main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: auto;
} }
.main.flex1 { .main.flex1 {
flex: 1; flex: 1;
max-width: 100%;
} }
.tabs { .tabs {
display: flex; display: flex;
height: var(--dim-tabs-height); height: var(--dim-tabs-height);
min-height: var(--dim-tabs-height);
right: 0; right: 0;
background-color: var(--theme-bg-2); background-color: var(--theme-bg-2);
overflow-x: auto;
max-width: 100%;
}
.tabs::-webkit-scrollbar {
height: 7px;
} }
.tab-item { .tab-item {

View File

@@ -8,6 +8,7 @@
export let name; export let name;
export let disabled = false; export let disabled = false;
export let defaultFileName = '';
const { values, setFieldValue } = getFormContext(); const { values, setFieldValue } = getFormContext();
@@ -25,6 +26,6 @@
</script> </script>
<div class="flex"> <div class="flex">
<TextField {...$$restProps} value={$values[name]} on:click={handleBrowse} readOnly {disabled} /> <TextField {...$$restProps} value={$values[name] || defaultFileName} on:click={handleBrowse} readOnly {disabled} />
<InlineButton on:click={handleBrowse} {disabled}>Browse</InlineButton> <InlineButton on:click={handleBrowse} {disabled}>Browse</InlineButton>
</div> </div>

View File

@@ -10,6 +10,7 @@
export let options; export let options;
export let isClearable = false; export let isClearable = false;
export let selectFieldComponent = SelectField; export let selectFieldComponent = SelectField;
export let defaultSelectValue;
const { values, setFieldValue } = getFormContext(); const { values, setFieldValue } = getFormContext();
</script> </script>
@@ -17,7 +18,7 @@
<svelte:component <svelte:component
this={selectFieldComponent} this={selectFieldComponent}
{...$$restProps} {...$$restProps}
value={$values && $values[name]} value={($values && $values[name]) || defaultSelectValue}
options={_.compact(options)} options={_.compact(options)}
on:change={e => { on:change={e => {
setFieldValue(name, e.detail); setFieldValue(name, e.detail);

View File

@@ -24,6 +24,10 @@
'icon sql-generator': 'mdi mdi-cog-transfer', 'icon sql-generator': 'mdi mdi-cog-transfer',
'icon keyboard': 'mdi mdi-keyboard-settings', 'icon keyboard': 'mdi mdi-keyboard-settings',
'icon settings': 'mdi mdi-cog', 'icon settings': 'mdi mdi-cog',
'icon users': 'mdi mdi-account-multiple',
'icon role': 'mdi mdi-account-group',
'icon admin': 'mdi mdi-security',
'icon auth': 'mdi mdi-account-key',
'icon version': 'mdi mdi-ticket-confirmation', 'icon version': 'mdi mdi-ticket-confirmation',
'icon pin': 'mdi mdi-pin', 'icon pin': 'mdi mdi-pin',
'icon arrange': 'mdi mdi-arrange-send-to-back', 'icon arrange': 'mdi mdi-arrange-send-to-back',
@@ -185,6 +189,10 @@
'img app-query': 'mdi mdi-view-comfy color-icon-magenta', 'img app-query': 'mdi mdi-view-comfy color-icon-magenta',
'img connection': 'mdi mdi-connection color-icon-blue', 'img connection': 'mdi mdi-connection color-icon-blue',
'img profiler': 'mdi mdi-gauge color-icon-blue', 'img profiler': 'mdi mdi-gauge color-icon-blue',
'img users': 'mdi mdi-account-multiple color-icon-blue',
'img role': 'mdi mdi-account-group color-icon-blue',
'img admin': 'mdi mdi-security color-icon-blue',
'img auth': 'mdi mdi-account-key color-icon-blue',
'img add': 'mdi mdi-plus-circle color-icon-green', 'img add': 'mdi mdi-plus-circle color-icon-green',
'img minus': 'mdi mdi-minus-circle color-icon-red', 'img minus': 'mdi mdi-minus-circle color-icon-red',

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import _ from 'lodash'; import _ from 'lodash';
import FormSelectField from '../forms/FormSelectField.svelte'; import FormSelectField from '../forms/FormSelectField.svelte';
import getConnectionLabel from '../utility/getConnectionLabel';
import { useConnectionList } from '../utility/metadataLoaders'; import { useConnectionList } from '../utility/metadataLoaders';
import { getConnectionLabel } from 'dbgate-tools';
export let allowChooseModel = false; export let allowChooseModel = false;
export let direction; export let direction;

View File

@@ -6,14 +6,16 @@ import localStorageGarbageCollector from './utility/localStorageGarbageCollector
import { handleOauthCallback } from './clientAuth'; import { handleOauthCallback } from './clientAuth';
import LoginPage from './LoginPage.svelte'; import LoginPage from './LoginPage.svelte';
import NotLoggedPage from './NotLoggedPage.svelte'; import NotLoggedPage from './NotLoggedPage.svelte';
import ErrorPage from './ErrorPage.svelte';
const isOauthCallback = handleOauthCallback();
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const page = params.get('page'); const page = params.get('page');
const isOauthCallback = handleOauthCallback();
localStorageGarbageCollector(); localStorageGarbageCollector();
function createApp() { function createApp() {
if (isOauthCallback) { if (isOauthCallback) {
return null; return null;
@@ -22,14 +24,35 @@ function createApp() {
switch (page) { switch (page) {
case 'login': case 'login':
return new LoginPage({ return new LoginPage({
target: document.body,
props: {
isAdminPage: false,
},
});
case 'error':
return new ErrorPage({
target: document.body, target: document.body,
props: {}, props: {},
}); });
case 'admin-login':
return new LoginPage({
target: document.body,
props: {
isAdminPage: true,
},
});
case 'not-logged': case 'not-logged':
return new NotLoggedPage({ return new NotLoggedPage({
target: document.body, target: document.body,
props: {}, props: {},
}); });
case 'admin':
return new App({
target: document.body,
props: {
isAdminPage: true,
},
});
} }
return new App({ return new App({

View File

@@ -25,6 +25,7 @@
import ErrorMessageModal from './ErrorMessageModal.svelte'; import ErrorMessageModal from './ErrorMessageModal.svelte';
import ModalBase from './ModalBase.svelte'; import ModalBase from './ModalBase.svelte';
import { closeCurrentModal, showModal } from './modalTools'; import { closeCurrentModal, showModal } from './modalTools';
import { callServerPing } from '../utility/connectionsPinger';
export let conid; export let conid;
export let passwordMode; export let passwordMode;
@@ -83,6 +84,7 @@
isTesting = false; isTesting = false;
if (resp.msgtype == 'connected') { if (resp.msgtype == 'connected') {
setVolatileConnectionRemapping(conid, resp._id); setVolatileConnectionRemapping(conid, resp._id);
await callServerPing();
dispatchCacheChange({ key: `server-status-changed` }); dispatchCacheChange({ key: `server-status-changed` });
batchDispatchCacheTriggers(x => x.conid == conid); batchDispatchCacheTriggers(x => x.conid == conid);
closeCurrentModal(); closeCurrentModal();

View File

@@ -15,7 +15,7 @@
import { closeCurrentModal, showModal } from './modalTools'; import { closeCurrentModal, showModal } from './modalTools';
import InputTextModal from './InputTextModal.svelte'; import InputTextModal from './InputTextModal.svelte';
import { apiCall } from '../utility/api'; import { apiCall } from '../utility/api';
import getConnectionLabel from '../utility/getConnectionLabel'; import { getConnectionLabel } from 'dbgate-tools';
export let connection; export let connection;

View File

@@ -9,12 +9,12 @@
import { currentDropDownMenu } from '../stores'; import { currentDropDownMenu } from '../stores';
import { apiCall } from '../utility/api'; import { apiCall } from '../utility/api';
import { importSqlDump } from '../utility/exportFileTools'; import { importSqlDump } from '../utility/exportFileTools';
import getConnectionLabel from '../utility/getConnectionLabel';
import getElectron from '../utility/getElectron'; import getElectron from '../utility/getElectron';
import { setUploadListener } from '../utility/uploadFiles'; import { setUploadListener } from '../utility/uploadFiles';
import ChangeDownloadUrlModal from './ChangeDownloadUrlModal.svelte'; import ChangeDownloadUrlModal from './ChangeDownloadUrlModal.svelte';
import ModalBase from './ModalBase.svelte'; import ModalBase from './ModalBase.svelte';
import { closeCurrentModal, showModal } from './modalTools'; import { closeCurrentModal, showModal } from './modalTools';
import { getConnectionLabel } from 'dbgate-tools';
export let connection; export let connection;

View File

@@ -5,7 +5,7 @@
import FormSubmit from '../forms/FormSubmit.svelte'; import FormSubmit from '../forms/FormSubmit.svelte';
import ModalBase from '../modals/ModalBase.svelte'; import ModalBase from '../modals/ModalBase.svelte';
import { closeCurrentModal } from '../modals/modalTools'; import { closeCurrentModal } from '../modals/modalTools';
import { fullNameFromString, fullNameToLabel, fullNameToString } from 'dbgate-tools'; import { fullNameFromString, fullNameToLabel, fullNameToString, getConnectionLabel } from 'dbgate-tools';
import SelectField from '../forms/SelectField.svelte'; import SelectField from '../forms/SelectField.svelte';
import _ from 'lodash'; import _ from 'lodash';
import { import {
@@ -18,7 +18,6 @@
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import { createPerspectiveNodeConfig, PerspectiveTreeNode } from 'dbgate-datalib'; import { createPerspectiveNodeConfig, PerspectiveTreeNode } from 'dbgate-datalib';
import type { ChangePerspectiveConfigFunc, PerspectiveConfig, PerspectiveCustomJoinConfig } from 'dbgate-datalib'; import type { ChangePerspectiveConfigFunc, PerspectiveConfig, PerspectiveCustomJoinConfig } from 'dbgate-datalib';
import getConnectionLabel from '../utility/getConnectionLabel';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import TextField from '../forms/TextField.svelte'; import TextField from '../forms/TextField.svelte';

View File

@@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import { getCurrentDatabase } from '../stores'; import { getCurrentDatabase } from '../stores';
import getConnectionLabel from '../utility/getConnectionLabel'; import { getConnectionLabel } from 'dbgate-tools';
import openNewTab from '../utility/openNewTab'; import openNewTab from '../utility/openNewTab';
export default function newQuery({ export default function newQuery({

View File

@@ -12,7 +12,7 @@
import FormTextField from '../forms/FormTextField.svelte'; import FormTextField from '../forms/FormTextField.svelte';
import { extensions, getCurrentConfig, openedConnections, openedSingleDatabaseConnections } from '../stores'; import { extensions, getCurrentConfig, openedConnections, openedSingleDatabaseConnections } from '../stores';
import getElectron from '../utility/getElectron'; import getElectron from '../utility/getElectron';
import { useAuthTypes } from '../utility/metadataLoaders'; import { useAuthTypes, useConfig } from '../utility/metadataLoaders';
import FormColorField from '../forms/FormColorField.svelte'; import FormColorField from '../forms/FormColorField.svelte';
import FontIcon from '../icons/FontIcon.svelte'; import FontIcon from '../icons/FontIcon.svelte';
@@ -27,13 +27,17 @@
$: disabledFields = (currentAuthType ? currentAuthType.disabledFields : null) || []; $: disabledFields = (currentAuthType ? currentAuthType.disabledFields : null) || [];
$: driver = $extensions.drivers.find(x => x.engine == engine); $: driver = $extensions.drivers.find(x => x.engine == engine);
$: defaultDatabase = $values.defaultDatabase; $: defaultDatabase = $values.defaultDatabase;
$: config = useConfig();
$: showUser = driver?.showConnectionField('user', $values) && $values.passwordMode != 'askUser'; $: showConnectionFieldArgs = { config: $config };
$: showUser =
driver?.showConnectionField('user', $values, showConnectionFieldArgs) && $values.passwordMode != 'askUser';
$: showPassword = $: showPassword =
driver?.showConnectionField('password', $values) && driver?.showConnectionField('password', $values, showConnectionFieldArgs) &&
$values.passwordMode != 'askPassword' && $values.passwordMode != 'askPassword' &&
$values.passwordMode != 'askUser'; $values.passwordMode != 'askUser';
$: showPasswordMode = driver?.showConnectionField('password', $values); $: showPasswordMode = driver?.showConnectionField('password', $values, showConnectionFieldArgs);
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id); $: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
</script> </script>
@@ -53,11 +57,11 @@
]} ]}
/> />
{#if driver?.showConnectionField('databaseFile', $values)} {#if driver?.showConnectionField('databaseFile', $values, showConnectionFieldArgs)}
<FormElectronFileSelector label="Database file" name="databaseFile" disabled={isConnected || !electron} /> <FormElectronFileSelector label="Database file" name="databaseFile" disabled={isConnected || !electron} />
{/if} {/if}
{#if driver?.showConnectionField('useDatabaseUrl', $values)} {#if driver?.showConnectionField('useDatabaseUrl', $values, showConnectionFieldArgs)}
<div class="radio"> <div class="radio">
<FormRadioGroupField <FormRadioGroupField
disabled={isConnected} disabled={isConnected}
@@ -70,7 +74,7 @@
</div> </div>
{/if} {/if}
{#if driver?.showConnectionField('databaseUrl', $values)} {#if driver?.showConnectionField('databaseUrl', $values, showConnectionFieldArgs)}
<FormTextField <FormTextField
label="Database URL" label="Database URL"
name="databaseUrl" name="databaseUrl"
@@ -79,21 +83,27 @@
/> />
{/if} {/if}
{#if $authTypes && driver?.showConnectionField('authType', $values)} {#if $authTypes && driver?.showConnectionField('authType', $values, showConnectionFieldArgs)}
<FormSelectField {#key $authTypes}
label={driver?.authTypeLabel ?? 'Authentication'} <FormSelectField
name="authType" label={driver?.authTypeLabel ?? 'Authentication'}
isNative name="authType"
disabled={isConnected} isNative
defaultValue={driver?.defaultAuthTypeName} disabled={isConnected}
options={$authTypes.map(auth => ({ defaultValue={driver?.defaultAuthTypeName}
value: auth.name, options={$authTypes.map(auth => ({
label: auth.title, value: auth.name,
}))} label: auth.title,
/> }))}
/>
{/key}
{/if} {/if}
{#if driver?.showConnectionField('server', $values)} {#if driver?.showConnectionField('clientLibraryPath', $values, showConnectionFieldArgs)}
<FormTextField label="Client library path" name="clientLibraryPath" disabled={isConnected} />
{/if}
{#if driver?.showConnectionField('server', $values, showConnectionFieldArgs)}
<div class="row"> <div class="row">
<div class="col-9 mr-1"> <div class="col-9 mr-1">
<FormTextField <FormTextField
@@ -103,7 +113,7 @@
templateProps={{ noMargin: true }} templateProps={{ noMargin: true }}
/> />
</div> </div>
{#if driver?.showConnectionField('port', $values)} {#if driver?.showConnectionField('port', $values, showConnectionFieldArgs)}
<div class="col-3 mr-1"> <div class="col-3 mr-1">
<FormTextField <FormTextField
label="Port" label="Port"
@@ -123,11 +133,11 @@
{/if} {/if}
{/if} {/if}
{#if driver?.showConnectionField('serviceName', $values)} {#if driver?.showConnectionField('serviceName', $values, showConnectionFieldArgs)}
<FormTextField label="Service name" name="serviceName" disabled={isConnected} /> <FormTextField label="Service name" name="serviceName" disabled={isConnected} />
{/if} {/if}
{#if driver?.showConnectionField('socketPath', $values)} {#if driver?.showConnectionField('socketPath', $values, showConnectionFieldArgs)}
<FormTextField <FormTextField
label="Socket path" label="Socket path"
name="socketPath" name="socketPath"
@@ -183,27 +193,27 @@
/> />
{/if} {/if}
{#if driver?.showConnectionField('treeKeySeparator', $values)} {#if driver?.showConnectionField('treeKeySeparator', $values, showConnectionFieldArgs)}
<FormTextField label="Key separator" name="treeKeySeparator" disabled={isConnected} placeholder=":" /> <FormTextField label="Key separator" name="treeKeySeparator" disabled={isConnected} placeholder=":" />
{/if} {/if}
{#if driver?.showConnectionField('windowsDomain', $values)} {#if driver?.showConnectionField('windowsDomain', $values, showConnectionFieldArgs)}
<FormTextField label="Domain (specify to use NTLM authentication)" name="windowsDomain" disabled={isConnected} /> <FormTextField label="Domain (specify to use NTLM authentication)" name="windowsDomain" disabled={isConnected} />
{/if} {/if}
{#if driver?.showConnectionField('isReadOnly', $values)} {#if driver?.showConnectionField('isReadOnly', $values, showConnectionFieldArgs)}
<FormCheckboxField label="Is read only" name="isReadOnly" disabled={isConnected} /> <FormCheckboxField label="Is read only" name="isReadOnly" disabled={isConnected} />
{/if} {/if}
{#if driver?.showConnectionField('trustServerCertificate', $values)} {#if driver?.showConnectionField('trustServerCertificate', $values, showConnectionFieldArgs)}
<FormCheckboxField label="Trust server certificate" name="trustServerCertificate" disabled={isConnected} /> <FormCheckboxField label="Trust server certificate" name="trustServerCertificate" disabled={isConnected} />
{/if} {/if}
{#if driver?.showConnectionField('defaultDatabase', $values)} {#if driver?.showConnectionField('defaultDatabase', $values, showConnectionFieldArgs)}
<FormTextField label="Default database" name="defaultDatabase" disabled={isConnected} /> <FormTextField label="Default database" name="defaultDatabase" disabled={isConnected} />
{/if} {/if}
{#if defaultDatabase && driver?.showConnectionField('singleDatabase', $values)} {#if defaultDatabase && driver?.showConnectionField('singleDatabase', $values, showConnectionFieldArgs)}
<FormCheckboxField label={`Use only database ${defaultDatabase}`} name="singleDatabase" disabled={isConnected} /> <FormCheckboxField label={`Use only database ${defaultDatabase}`} name="singleDatabase" disabled={isConnected} />
{/if} {/if}

View File

@@ -19,11 +19,11 @@
$: useSshTunnel = $values.useSshTunnel; $: useSshTunnel = $values.useSshTunnel;
$: platformInfo = usePlatformInfo(); $: platformInfo = usePlatformInfo();
$: { // $: {
if (!$values.sshMode) setFieldValue('sshMode', 'userPassword'); // if (!$values.sshMode) setFieldValue('sshMode', 'userPassword');
// if (!$values.sshPort) setFieldValue('sshPort', '22'); // // if (!$values.sshPort) setFieldValue('sshPort', '22');
if (!$values.sshKeyfile && $platformInfo) setFieldValue('sshKeyfile', $platformInfo.defaultKeyfile); // if (!$values.sshKeyfile && $platformInfo) setFieldValue('sshKeyfile', $platformInfo.defaultKeyfile);
} // }
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id); $: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
</script> </script>
@@ -55,6 +55,7 @@
label="SSH Authentication" label="SSH Authentication"
name="sshMode" name="sshMode"
isNative isNative
defaultSelectValue="userPassword"
disabled={isConnected || !useSshTunnel} disabled={isConnected || !useSshTunnel}
options={[ options={[
{ value: 'userPassword', label: 'Username & password' }, { value: 'userPassword', label: 'Username & password' },
@@ -63,11 +64,11 @@
]} ]}
/> />
{#if $values.sshMode != 'userPassword'} {#if ($values.sshMode || 'userPassword') != 'userPassword'}
<FormTextField label="Login" name="sshLogin" disabled={isConnected || !useSshTunnel} /> <FormTextField label="Login" name="sshLogin" disabled={isConnected || !useSshTunnel} />
{/if} {/if}
{#if $values.sshMode == 'userPassword'} {#if ($values.sshMode || 'userPassword') == 'userPassword'}
<div class="row"> <div class="row">
<div class="col-6 mr-1"> <div class="col-6 mr-1">
<FormTextField <FormTextField
@@ -96,6 +97,7 @@
name="sshKeyfile" name="sshKeyfile"
disabled={isConnected || !useSshTunnel} disabled={isConnected || !useSshTunnel}
templateProps={{ noMargin: true }} templateProps={{ noMargin: true }}
defaultFileName={$platformInfo?.defaultKeyfile}
/> />
</div> </div>
<div class="col-6"> <div class="col-6">

View File

@@ -7,6 +7,7 @@ import { getSettings, useConfig, useSettings } from './utility/metadataLoaders';
import _ from 'lodash'; import _ from 'lodash';
import { safeJsonParse } from 'dbgate-tools'; import { safeJsonParse } from 'dbgate-tools';
import { apiCall } from './utility/api'; import { apiCall } from './utility/api';
import { getOpenedTabsStorageName, isAdminPage } from './utility/pageDefs';
export interface TabDefinition { export interface TabDefinition {
title: string; title: string;
@@ -72,7 +73,10 @@ function subscribeCssVariable(store, transform, cssVariable) {
store.subscribe(value => document.documentElement.style.setProperty(cssVariable, transform(value))); store.subscribe(value => document.documentElement.style.setProperty(cssVariable, transform(value)));
} }
export const selectedWidget = writableWithStorage('database', 'selectedWidget'); export const selectedWidget = writableWithStorage(
isAdminPage() ? 'admin' : 'database',
isAdminPage() ? 'selectedAdminWidget' : 'selectedWidget'
);
export const lockedDatabaseMode = writableWithStorage<boolean>(false, 'lockedDatabaseMode'); export const lockedDatabaseMode = writableWithStorage<boolean>(false, 'lockedDatabaseMode');
export const visibleWidgetSideBar = writableWithStorage(true, 'visibleWidgetSideBar'); export const visibleWidgetSideBar = writableWithStorage(true, 'visibleWidgetSideBar');
export const visibleSelectedWidget = derived( export const visibleSelectedWidget = derived(
@@ -86,7 +90,7 @@ export const temporaryOpenedConnections = writable([]);
export const openedSingleDatabaseConnections = writable([]); export const openedSingleDatabaseConnections = writable([]);
export const expandedConnections = writable([]); export const expandedConnections = writable([]);
export const currentDatabase = writable(null); export const currentDatabase = writable(null);
export const openedTabs = writableWithForage<TabDefinition[]>([], 'openedTabs', x => [...(x || [])]); export const openedTabs = writableWithForage<TabDefinition[]>([], getOpenedTabsStorageName(), x => [...(x || [])]);
export const copyRowsFormat = writableWithStorage('textWithoutHeaders', 'copyRowsFormat'); export const copyRowsFormat = writableWithStorage('textWithoutHeaders', 'copyRowsFormat');
export const extensions = writable<ExtensionsDirectory>(null); export const extensions = writable<ExtensionsDirectory>(null);
export const visibleCommandPalette = writable(null); export const visibleCommandPalette = writable(null);

View File

@@ -158,6 +158,7 @@
function getTabDbName(tab, connectionList) { function getTabDbName(tab, connectionList) {
if (tab.tabComponent == 'ConnectionTab') return 'Connections'; if (tab.tabComponent == 'ConnectionTab') return 'Connections';
if (tab.tabComponent?.startsWith('Admin')) return 'Administration';
if (tab.props && tab.props.conid && tab.props.database) return tab.props.database; if (tab.props && tab.props.conid && tab.props.database) return tab.props.database;
if (tab.props && tab.props.conid) { if (tab.props && tab.props.conid) {
const connection = connectionList?.find(x => x._id == tab.props.conid); const connection = connectionList?.find(x => x._id == tab.props.conid);
@@ -174,6 +175,7 @@
if (key.startsWith('archive://')) return 'icon archive'; if (key.startsWith('archive://')) return 'icon archive';
if (key.startsWith('server://')) return 'icon server'; if (key.startsWith('server://')) return 'icon server';
if (key.startsWith('connections.')) return 'icon connection'; if (key.startsWith('connections.')) return 'icon connection';
if (key.startsWith('admin.')) return 'icon admin';
} }
return 'icon file'; return 'icon file';
} }
@@ -285,7 +287,6 @@
import tabs from '../tabs'; import tabs from '../tabs';
import { setSelectedTab } from '../utility/common'; import { setSelectedTab } from '../utility/common';
import contextMenu from '../utility/contextMenu'; import contextMenu from '../utility/contextMenu';
import getConnectionLabel from '../utility/getConnectionLabel';
import { isElectronAvailable } from '../utility/getElectron'; import { isElectronAvailable } from '../utility/getElectron';
import { getConnectionInfo, useConnectionList } from '../utility/metadataLoaders'; import { getConnectionInfo, useConnectionList } from '../utility/metadataLoaders';
import { duplicateTab, getTabDbKey, sortTabs, groupTabs } from '../utility/openNewTab'; import { duplicateTab, getTabDbKey, sortTabs, groupTabs } from '../utility/openNewTab';
@@ -293,6 +294,7 @@
import TabCloseButton from '../elements/TabCloseButton.svelte'; import TabCloseButton from '../elements/TabCloseButton.svelte';
import CloseTabModal from '../modals/CloseTabModal.svelte'; import CloseTabModal from '../modals/CloseTabModal.svelte';
import SwitchDatabaseModal from '../modals/SwitchDatabaseModal.svelte'; import SwitchDatabaseModal from '../modals/SwitchDatabaseModal.svelte';
import { getConnectionLabel } from 'dbgate-tools';
export let multiTabIndex; export let multiTabIndex;
export let shownTab; export let shownTab;

View File

@@ -28,30 +28,37 @@
import { apiCall } from '../utility/api'; import { apiCall } from '../utility/api';
import { showSnackbarError, showSnackbarSuccess } from '../utility/snackbar'; import { showSnackbarError, showSnackbarSuccess } from '../utility/snackbar';
import { changeTab } from '../utility/common'; import { changeTab } from '../utility/common';
import getConnectionLabel from '../utility/getConnectionLabel'; import { getConnectionLabel } from 'dbgate-tools';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { disconnectServerConnection, openConnection } from '../appobj/ConnectionAppObject.svelte'; import { disconnectServerConnection, openConnection } from '../appobj/ConnectionAppObject.svelte';
import { disconnectDatabaseConnection } from '../appobj/DatabaseAppObject.svelte'; import { disconnectDatabaseConnection } from '../appobj/DatabaseAppObject.svelte';
import { useConfig } from '../utility/metadataLoaders';
export let connection; export let connection;
export let tabid; export let tabid;
export let conid; export let conid;
export let connectionStore = undefined;
export let onlyTestButton;
let isTesting; let isTesting;
let sqlConnectResult; let sqlConnectResult;
const values = writable( const values =
connection || { connectionStore ||
server: getCurrentConfig().isDocker ? 'dockerhost' : 'localhost', writable(
engine: '', connection || {
} server: getCurrentConfig().isDocker ? 'dockerhost' : 'localhost',
); engine: '',
}
);
// $: console.log('ConnectionTab.$values', $values); // $: console.log('ConnectionTab.$values', $values);
// $: console.log('ConnectionTab.driver', driver); // $: console.log('ConnectionTab.driver', driver);
$: engine = $values.engine; $: engine = $values.engine;
$: driver = $extensions.drivers.find(x => x.engine == engine); $: driver = $extensions.drivers.find(x => x.engine == engine);
$: config = useConfig();
const testIdRef = createRef(0); const testIdRef = createRef(0);
@@ -86,7 +93,7 @@
'socketPath', 'socketPath',
'serviceName', 'serviceName',
]; ];
const visibleProps = allProps.filter(x => driver?.showConnectionField(x, $values)); const visibleProps = allProps.filter(x => driver?.showConnectionField(x, $values, { config: $config }));
const omitProps = _.difference(allProps, visibleProps); const omitProps = _.difference(allProps, visibleProps);
if (!$values.defaultDatabase) omitProps.push('singleDatabase'); if (!$values.defaultDatabase) omitProps.push('singleDatabase');
@@ -174,6 +181,11 @@
} }
}); });
export function changeConnectionBeforeSave(connection) {
if (driver?.beforeConnectionSave) return driver.beforeConnectionSave(connection);
return connection;
}
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id); $: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
// $: console.log('CONN VALUES', $values); // $: console.log('CONN VALUES', $values);
@@ -204,7 +216,13 @@
{#if driver} {#if driver}
<div class="flex"> <div class="flex">
<div class="buttons"> <div class="buttons">
{#if isConnected} {#if onlyTestButton}
{#if isTesting}
<FormButton value="Cancel test" on:click={handleCancelTest} />
{:else}
<FormButton value="Test connection" on:click={handleTest} />
{/if}
{:else if isConnected}
<FormButton value="Disconnect" on:click={handleDisconnect} /> <FormButton value="Disconnect" on:click={handleDisconnect} />
{:else} {:else}
<FormButton value="Connect" on:click={handleConnect} /> <FormButton value="Connect" on:click={handleConnect} />

View File

@@ -0,0 +1 @@
export default {};

View File

@@ -30,6 +30,8 @@ import * as ProfilerTab from './ProfilerTab.svelte';
import * as DataDuplicatorTab from './DataDuplicatorTab.svelte'; import * as DataDuplicatorTab from './DataDuplicatorTab.svelte';
import * as ImportExportTab from './ImportExportTab.svelte'; import * as ImportExportTab from './ImportExportTab.svelte';
import protabs from './index-pro';
export default { export default {
TableDataTab, TableDataTab,
CollectionDataTab, CollectionDataTab,
@@ -62,4 +64,5 @@ export default {
ProfilerTab, ProfilerTab,
DataDuplicatorTab, DataDuplicatorTab,
ImportExportTab, ImportExportTab,
...protabs,
}; };

View File

@@ -3,6 +3,7 @@
import _ from 'lodash'; import _ from 'lodash';
import { TabDefinition } from '../stores'; import { TabDefinition } from '../stores';
import getElectron from './getElectron'; import getElectron from './getElectron';
import { getOpenedTabsStorageName } from './pageDefs';
let counter = 0; let counter = 0;
$: counterCopy = counter; $: counterCopy = counter;
@@ -26,15 +27,15 @@
) )
) { ) {
try { try {
let openedTabs = (await localforage.getItem<TabDefinition[]>('openedTabs')) || []; let openedTabs = (await localforage.getItem<TabDefinition[]>(getOpenedTabsStorageName())) || [];
if (!_.isArray(openedTabs)) openedTabs = []; if (!_.isArray(openedTabs)) openedTabs = [];
openedTabs = openedTabs openedTabs = openedTabs
.map(tab => (tab.closedTime ? tab : { ...tab, closedTime: new Date().getTime() })) .map(tab => (tab.closedTime ? tab : { ...tab, closedTime: new Date().getTime() }))
.map(tab => ({ ...tab, selected: false })); .map(tab => ({ ...tab, selected: false }));
await localforage.setItem('openedTabs', openedTabs); await localforage.setItem(getOpenedTabsStorageName(), openedTabs);
await localStorage.setItem('selectedWidget', 'history'); await localStorage.setItem('selectedWidget', 'history');
} catch (err) { } catch (err) {
localforage.removeItem('openedTabs'); localforage.removeItem(getOpenedTabsStorageName());
} }
// try { // try {
// await localforage.clear(); // await localforage.clear();

View File

@@ -0,0 +1,8 @@
<script lang="ts">
import createActivator from './createActivator';
export let name;
export let activateOnTabVisible = false;
export const activator = createActivator(name, activateOnTabVisible);
</script>

View File

@@ -4,7 +4,7 @@
import runCommand from '../commands/runCommand'; import runCommand from '../commands/runCommand';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte'; import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import { showModal } from '../modals/modalTools'; import { showModal } from '../modals/modalTools';
import { openedTabs } from '../stores'; import { commandsCustomized, openedTabs } from '../stores';
import { getConfig, getConnectionList, useFavorites } from './metadataLoaders'; import { getConfig, getConnectionList, useFavorites } from './metadataLoaders';
import openNewTab from './openNewTab'; import openNewTab from './openNewTab';
@@ -49,7 +49,11 @@
} }
} }
if (!$openedTabs.find(x => x.closedTime == null) && !(await getConnectionList()).find(x => !x.unsaved)) { if (
!$openedTabs.find(x => x.closedTime == null) &&
!(await getConnectionList()).find(x => !x.unsaved) &&
$commandsCustomized['new.connection']?.enabled
) {
openNewTab({ openNewTab({
title: 'New Connection', title: 'New Connection',
icon: 'img connection', icon: 'img connection',

View File

@@ -4,22 +4,37 @@ import { writable } from 'svelte/store';
import getElectron from './getElectron'; import getElectron from './getElectron';
// import socket from './socket'; // import socket from './socket';
import { showSnackbarError } from '../utility/snackbar'; import { showSnackbarError } from '../utility/snackbar';
import { isOauthCallback, redirectToLogin } from '../clientAuth'; import { isOauthCallback, redirectToAdminLogin, redirectToLogin } from '../clientAuth';
import { showModal } from '../modals/modalTools'; import { showModal } from '../modals/modalTools';
import DatabaseLoginModal, { isDatabaseLoginVisible } from '../modals/DatabaseLoginModal.svelte'; import DatabaseLoginModal, { isDatabaseLoginVisible } from '../modals/DatabaseLoginModal.svelte';
import _ from 'lodash'; import _ from 'lodash';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import { openWebLink } from './exportFileTools';
import { callServerPing } from './connectionsPinger';
import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache';
export const strmid = uuidv1(); export const strmid = uuidv1();
let eventSource; let eventSource;
let apiLogging = false; let apiLogging = true;
// let cacheCleanerRegistered; // let cacheCleanerRegistered;
let apiDisabled = false; let apiDisabled = false;
const disabledOnOauth = isOauthCallback(); const disabledOnOauth = isOauthCallback();
const volatileConnectionMap = {}; export const volatileConnectionMapStore = writable({});
const volatileConnectionMapInv = {}; export const volatileConnectionMapInvStore = writable({});
let volatileConnectionMapValue = {};
volatileConnectionMapStore.subscribe(value => {
volatileConnectionMapValue = value;
});
export const getVolatileConnectionMap = () => volatileConnectionMapValue;
let volatileConnectionMapInvValue = {};
volatileConnectionMapInvStore.subscribe(value => {
volatileConnectionMapInvValue = value;
});
export const getVolatileConnectionInvMap = () => volatileConnectionMapInvValue;
export function disableApi() { export function disableApi() {
apiDisabled = true; apiDisabled = true;
@@ -30,23 +45,29 @@ export function enableApi() {
} }
export function setVolatileConnectionRemapping(existingConnectionId, volatileConnectionId) { export function setVolatileConnectionRemapping(existingConnectionId, volatileConnectionId) {
volatileConnectionMap[existingConnectionId] = volatileConnectionId; volatileConnectionMapStore.update(x => ({
volatileConnectionMapInv[volatileConnectionId] = existingConnectionId; ...x,
[existingConnectionId]: volatileConnectionId,
}));
volatileConnectionMapInvStore.update(x => ({
...x,
[volatileConnectionId]: existingConnectionId,
}));
} }
export function getVolatileRemapping(conid) { export function getVolatileRemapping(conid) {
return volatileConnectionMap[conid] || conid; return volatileConnectionMapValue[conid] || conid;
} }
export function getVolatileRemappingInv(conid) { export function getVolatileRemappingInv(conid) {
return volatileConnectionMapInv[conid] || conid; return volatileConnectionMapInvValue[conid] || conid;
} }
export function removeVolatileMapping(conid) { export function removeVolatileMapping(conid) {
const mapped = volatileConnectionMap[conid]; const mapped = volatileConnectionMapValue[conid];
if (mapped) { if (mapped) {
delete volatileConnectionMap[conid]; volatileConnectionMapStore.update(x => _.omit(x, conid));
delete volatileConnectionMapInv[mapped]; volatileConnectionMapInvStore.update(x => _.omit(x, mapped));
} }
} }
@@ -63,7 +84,15 @@ function processApiResponse(route, args, resp) {
// } // }
if (resp?.missingCredentials) { if (resp?.missingCredentials) {
if (!isDatabaseLoginVisible()) { if (resp.detail.redirectToDbLogin) {
const state = `dbg-dblogin:${strmid}:${resp.detail.conid}`;
localStorage.setItem('dbloginState', state);
openWebLink(
`connections/dblogin?conid=${resp.detail.conid}&state=${encodeURIComponent(state)}&redirectUri=${
location.origin + location.pathname
}`
);
} else if (!isDatabaseLoginVisible()) {
showModal(DatabaseLoginModal, resp.detail); showModal(DatabaseLoginModal, resp.detail);
} }
return null; return null;
@@ -83,16 +112,16 @@ function processApiResponse(route, args, resp) {
export function transformApiArgs(args) { export function transformApiArgs(args) {
return _.mapValues(args, (v, k) => { return _.mapValues(args, (v, k) => {
if (k == 'conid' && v && volatileConnectionMap[v]) return volatileConnectionMap[v]; if (k == 'conid' && v && volatileConnectionMapValue[v]) return volatileConnectionMapValue[v];
if (k == 'conidArray' && _.isArray(v)) return v.map(x => volatileConnectionMap[x] || x); if (k == 'conidArray' && _.isArray(v)) return v.map(x => volatileConnectionMapValue[x] || x);
return v; return v;
}); });
} }
export function transformApiArgsInv(args) { export function transformApiArgsInv(args) {
return _.mapValues(args, (v, k) => { return _.mapValues(args, (v, k) => {
if (k == 'conid' && v && volatileConnectionMapInv[v]) return volatileConnectionMapInv[v]; if (k == 'conid' && v && volatileConnectionMapInvValue[v]) return volatileConnectionMapInvValue[v];
if (k == 'conidArray' && _.isArray(v)) return v.map(x => volatileConnectionMapInv[x] || x); if (k == 'conidArray' && _.isArray(v)) return v.map(x => volatileConnectionMapInvValue[x] || x);
return v; return v;
}); });
} }
@@ -132,9 +161,13 @@ export async function apiCall(route: string, args: {} = undefined) {
disableApi(); disableApi();
console.log('Disabling API', route); console.log('Disabling API', route);
if (params.get('page') != 'login' && params.get('page') != 'not-logged') { if (params.get('page') != 'login' && params.get('page') != 'admin-login' && params.get('page') != 'not-logged') {
// unauthorized // unauthorized
redirectToLogin(); if (params.get('page') == 'admin') {
redirectToAdminLogin();
} else {
redirectToLogin();
}
} }
return; return;
} }
@@ -205,6 +238,19 @@ export function useApiCall(route, args, defaultValue) {
return result; return result;
} }
export function getVolatileConnections() {
return Object.values(volatileConnectionMapValue);
}
export function installNewVolatileConnectionListener() {
apiOn('got-volatile-token', async ({ savedConId, volatileConId }) => {
setVolatileConnectionRemapping(savedConId, volatileConId);
await callServerPing();
dispatchCacheChange({ key: `server-status-changed` });
batchDispatchCacheTriggers(x => x.conid == savedConId);
});
}
function enableApiLog() { function enableApiLog() {
apiLogging = true; apiLogging = true;
console.log('API loggin enabled'); console.log('API loggin enabled');

View File

@@ -1,6 +1,5 @@
import { getOpenedTabs, openedTabs } from '../stores'; import { getOpenedTabs, openedTabs } from '../stores';
import _ from 'lodash'; import _ from 'lodash';
import getElectron from './getElectron';
export class LoadingToken { export class LoadingToken {
isCanceled = false; isCanceled = false;

View File

@@ -1,7 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import { openedConnections, currentDatabase, openedConnectionsWithTemporary } from '../stores'; import { currentDatabase, openedConnectionsWithTemporary, getCurrentConfig, getOpenedConnections } from '../stores';
import { apiCall, strmid } from './api'; import { apiCall, getVolatileConnections, strmid } from './api';
import { getConnectionList } from './metadataLoaders'; import hasPermission from '../utility/hasPermission';
// const doServerPing = async value => { // const doServerPing = async value => {
// const connectionList = getConnectionList(); // const connectionList = getConnectionList();
@@ -10,7 +10,21 @@ import { getConnectionList } from './metadataLoaders';
// }; // };
const doServerPing = value => { const doServerPing = value => {
apiCall('server-connections/ping', { conidArray: value, strmid }); const config = getCurrentConfig();
const conidArray = [...value];
if (config.storageDatabase && hasPermission('internal-storage')) {
conidArray.push('__storage');
}
conidArray.push(...getVolatileConnections());
if (config.singleConnection) {
conidArray.push(config.singleConnection._id);
}
apiCall('server-connections/ping', {
conidArray,
strmid,
});
}; };
const doDatabasePing = value => { const doDatabasePing = value => {
@@ -38,3 +52,8 @@ export function subscribeConnectionPingers() {
currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 20 * 1000); currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 20 * 1000);
}); });
} }
export function callServerPing() {
const connections = getOpenedConnections();
doServerPing(connections);
}

View File

@@ -4,13 +4,17 @@ import { useConfig } from './metadataLoaders';
let compiled = null; let compiled = null;
export default function hasPermission(tested) { export default function hasPermission(tested) {
// console.log('TESTING PERM', tested, compiled, testPermission(tested, compiled));
return testPermission(tested, compiled); return testPermission(tested, compiled);
} }
export function subscribePermissionCompiler() { export function subscribePermissionCompiler() {
// console.log('subscribePermissionCompiler', compiled);
useConfig().subscribe(value => { useConfig().subscribe(value => {
if (!value) return; if (!value) return;
const { permissions } = value; const { permissions } = value;
compiled = compilePermissions(permissions); compiled = compilePermissions(permissions);
// console.log('COMPILED PERMS', compiled);
}); });
} }

View File

@@ -1,14 +1,15 @@
import moment from 'moment'; import moment from 'moment';
import localforage from 'localforage'; import localforage from 'localforage';
import { getOpenedTabsStorageName } from './pageDefs';
export default async function localStorageGarbageCollector() { export default async function localStorageGarbageCollector() {
const openedTabsJson = await localforage.getItem('openedTabs'); const openedTabsJson = await localforage.getItem(getOpenedTabsStorageName());
let openedTabs = openedTabsJson ?? []; let openedTabs = openedTabsJson ?? [];
const closeLimit = moment().add(-7, 'day').valueOf(); const closeLimit = moment().add(-7, 'day').valueOf();
openedTabs = openedTabs.filter(x => !x.closedTime || x.closedTime > closeLimit); openedTabs = openedTabs.filter(x => !x.closedTime || x.closedTime > closeLimit);
await localforage.setItem('openedTabs', openedTabs); await localforage.setItem(getOpenedTabsStorageName(), openedTabs);
const toRemove = []; const toRemove = [];
for (const key in localStorage) { for (const key in localStorage) {

View File

@@ -5,7 +5,7 @@ import ImportExportModal from '../modals/ImportExportModal.svelte';
import getElectron from './getElectron'; import getElectron from './getElectron';
import { currentDatabase, extensions, getCurrentDatabase } from '../stores'; import { currentDatabase, extensions, getCurrentDatabase } from '../stores';
import { getUploadListener } from './uploadFiles'; import { getUploadListener } from './uploadFiles';
import getConnectionLabel, { getDatabaseFileLabel } from './getConnectionLabel'; import {getConnectionLabel, getDatabaseFileLabel } from 'dbgate-tools';
import { apiCall } from './api'; import { apiCall } from './api';
import openNewTab from './openNewTab'; import openNewTab from './openNewTab';
import { openJsonDocument } from '../tabs/JsonTab.svelte'; import { openJsonDocument } from '../tabs/JsonTab.svelte';

View File

@@ -152,6 +152,9 @@ export function getTabDbKey(tab) {
if (tab.tabComponent == 'ConnectionTab') { if (tab.tabComponent == 'ConnectionTab') {
return 'connections.'; return 'connections.';
} }
if (tab.tabComponent?.startsWith('Admin')) {
return 'admin.';
}
if (tab.props && tab.props.conid && tab.props.database) { if (tab.props && tab.props.conid && tab.props.database) {
return `database://${tab.props.database}-${tab.props.conid}`; return `database://${tab.props.database}-${tab.props.conid}`;
} }

View File

@@ -0,0 +1,15 @@
let isAdminPageCache;
export function isAdminPage() {
if (isAdminPageCache == null) {
const params = new URLSearchParams(location.search);
const urlPage = params.get('page');
isAdminPageCache = urlPage == 'admin';
}
return isAdminPageCache;
}
export function getOpenedTabsStorageName() {
return isAdminPage() ? 'adminOpenedTabs' : 'openedTabs';
}

View File

@@ -1,4 +1,5 @@
import getElectron from './getElectron'; import getElectron from './getElectron';
import { isAdminPage } from './pageDefs';
let apiUrl = null; let apiUrl = null;
try { try {
@@ -16,9 +17,12 @@ export function resolveApiHeaders() {
const electron = getElectron(); const electron = getElectron();
const res = {}; const res = {};
const accessToken = localStorage.getItem('accessToken'); const accessToken = localStorage.getItem(isAdminPage() ? 'adminAccessToken' : 'accessToken');
if (accessToken) { if (accessToken) {
res['Authorization'] = `Bearer ${accessToken}`; res['Authorization'] = `Bearer ${accessToken}`;
} }
if (isAdminPage()) {
res['x-is-admin-page'] = 'true';
}
return res; return res;
} }

View File

@@ -0,0 +1 @@
Sorry, administration is not available

View File

@@ -18,11 +18,11 @@
collapsedConnectionGroupNames, collapsedConnectionGroupNames,
} from '../stores'; } from '../stores';
import runCommand from '../commands/runCommand'; import runCommand from '../commands/runCommand';
import getConnectionLabel from '../utility/getConnectionLabel'; import { getConnectionLabel } from 'dbgate-tools';
import { useConnectionColorFactory } from '../utility/useConnectionColor'; import { useConnectionColorFactory } from '../utility/useConnectionColor';
import FontIcon from '../icons/FontIcon.svelte'; import FontIcon from '../icons/FontIcon.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte'; import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { apiCall, getVolatileRemapping } from '../utility/api'; import { apiCall, volatileConnectionMapStore } from '../utility/api';
import LargeButton from '../buttons/LargeButton.svelte'; import LargeButton from '../buttons/LargeButton.svelte';
import { plusExpandIcon, chevronExpandIcon } from '../icons/expandIcons'; import { plusExpandIcon, chevronExpandIcon } from '../icons/expandIcons';
import { safeJsonParse } from 'dbgate-tools'; import { safeJsonParse } from 'dbgate-tools';
@@ -37,7 +37,10 @@
$: connectionsWithStatus = $: connectionsWithStatus =
$connections && $serverStatus $connections && $serverStatus
? $connections.map(conn => ({ ...conn, status: $serverStatus[getVolatileRemapping(conn._id)] })) ? $connections.map(conn => ({
...conn,
status: $serverStatus[$volatileConnectionMapStore[conn._id] || conn._id],
}))
: $connections; : $connections;
$: connectionsWithStatusFiltered = connectionsWithStatus?.filter( $: connectionsWithStatusFiltered = connectionsWithStatus?.filter(

View File

@@ -14,7 +14,7 @@
selectedWidget, selectedWidget,
visibleCommandPalette, visibleCommandPalette,
} from '../stores'; } from '../stores';
import getConnectionLabel from '../utility/getConnectionLabel'; import { getConnectionLabel } from 'dbgate-tools';
import { useConnectionList, useDatabaseServerVersion, useDatabaseStatus } from '../utility/metadataLoaders'; import { useConnectionList, useDatabaseServerVersion, useDatabaseStatus } from '../utility/metadataLoaders';
import { findCommand } from '../commands/runCommand'; import { findCommand } from '../commands/runCommand';
import { useConnectionColor } from '../utility/useConnectionColor'; import { useConnectionColor } from '../utility/useConnectionColor';

View File

@@ -7,6 +7,7 @@
import CellDataWidget from './CellDataWidget.svelte'; import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte'; import HistoryWidget from './HistoryWidget.svelte';
import AppWidget from './AppWidget.svelte'; import AppWidget from './AppWidget.svelte';
import AdminMenuWidget from './AdminMenuWidget.svelte';
</script> </script>
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} /> <DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
@@ -29,3 +30,6 @@
{#if $visibleSelectedWidget == 'app'} {#if $visibleSelectedWidget == 'app'}
<AppWidget /> <AppWidget />
{/if} {/if}
{#if $visibleSelectedWidget == 'admin'}
<AdminMenuWidget />
{/if}

View File

@@ -8,6 +8,7 @@
visibleWidgetSideBar, visibleWidgetSideBar,
visibleHamburgerMenuWidget, visibleHamburgerMenuWidget,
lockedDatabaseMode, lockedDatabaseMode,
getCurrentConfig,
} from '../stores'; } from '../stores';
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition'; import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
import hasPermission from '../utility/hasPermission'; import hasPermission from '../utility/hasPermission';
@@ -16,6 +17,11 @@
let domMainMenu; let domMainMenu;
const widgets = [ const widgets = [
getCurrentConfig().storageDatabase && {
icon: 'icon admin',
name: 'admin',
title: 'Administration',
},
{ {
icon: 'icon database', icon: 'icon database',
name: 'database', name: 'database',
@@ -98,7 +104,7 @@
<FontIcon icon="icon menu" /> <FontIcon icon="icon menu" />
</div> </div>
{/if} {/if}
{#each widgets.filter(x => hasPermission(`widgets/${x.name}`)) as item} {#each widgets.filter(x => x && hasPermission(`widgets/${x.name}`)) as item}
<div <div
class="wrapper" class="wrapper"
class:selected={item.name == $visibleSelectedWidget} class:selected={item.name == $visibleSelectedWidget}
@@ -119,6 +125,7 @@
> >
<FontIcon icon={$lockedDatabaseMode ? 'icon locked-database-mode' : 'icon unlocked-database-mode'} /> <FontIcon icon={$lockedDatabaseMode ? 'icon locked-database-mode' : 'icon unlocked-database-mode'} />
</div> </div>
<div class="wrapper" on:click={handleSettingsMenu} bind:this={domSettings}> <div class="wrapper" on:click={handleSettingsMenu} bind:this={domSettings}>
<FontIcon icon="icon settings" /> <FontIcon icon="icon settings" />
</div> </div>

View File

@@ -33,7 +33,7 @@ class Analyser extends DatabaseAnalyser {
collections: [ collections: [
...collections.map((x, index) => ({ ...collections.map((x, index) => ({
pureName: x.name, pureName: x.name,
tableRowCount: stats[index].count, tableRowCount: stats[index]?.count,
})), })),
...views.map((x, index) => ({ ...views.map((x, index) => ({
pureName: x.name, pureName: x.name,

View File

@@ -31,12 +31,13 @@
"plugout": "dbgate-plugout dbgate-plugin-mssql" "plugout": "dbgate-plugout dbgate-plugin-mssql"
}, },
"devDependencies": { "devDependencies": {
"async-lock": "^1.2.6",
"@azure/msal-node": "^2.12.0",
"dbgate-plugin-tools": "^1.0.7", "dbgate-plugin-tools": "^1.0.7",
"dbgate-query-splitter": "^4.10.1", "dbgate-query-splitter": "^4.10.1",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",
"tedious": "^18.2.0", "tedious": "^18.2.0",
"async-lock": "^1.2.6" "webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
} }
} }

View File

@@ -0,0 +1,22 @@
function getAzureAuthTypes(platformInfo) {
return null;
}
async function azureGetRedirectAuthUrl(connection) {
return null;
}
async function azureGetAuthTokenFromCode(connection, code) {
return null;
}
function getAzureAuthOptions(connection) {
return null;
}
module.exports = {
getAzureAuthTypes,
azureGetRedirectAuthUrl,
azureGetAuthTokenFromCode,
getAzureAuthOptions,
};

View File

@@ -8,8 +8,11 @@ const AsyncLock = require('async-lock');
const nativeDriver = require('./nativeDriver'); const nativeDriver = require('./nativeDriver');
const lock = new AsyncLock(); const lock = new AsyncLock();
const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver'); const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver');
const { getAzureAuthTypes, azureGetRedirectAuthUrl, azureGetAuthTokenFromCode } = require('./azureAuth');
const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = nativeDriver; const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = nativeDriver;
let requireMsnodesqlv8; let requireMsnodesqlv8;
let platformInfo;
const versionQuery = ` const versionQuery = `
SELECT SELECT
@@ -52,7 +55,14 @@ const driver = {
analyserClass: MsSqlAnalyser, analyserClass: MsSqlAnalyser,
getAuthTypes() { getAuthTypes() {
return requireMsnodesqlv8 ? windowsAuthTypes : null; const res = [];
if (requireMsnodesqlv8) res.push(...windowsAuthTypes);
const azureAuthTypes = getAzureAuthTypes(platformInfo);
if (azureAuthTypes) res.push(...azureAuthTypes);
if (res.length > 0) {
return _.uniqBy(res, 'name');
}
return null;
}, },
async connect(conn) { async connect(conn) {
@@ -115,12 +125,19 @@ const driver = {
const { rows } = await this.query(pool, 'SELECT name FROM sys.databases order by name'); const { rows } = await this.query(pool, 'SELECT name FROM sys.databases order by name');
return rows; return rows;
}, },
getRedirectAuthUrl(connection, options) {
return azureGetRedirectAuthUrl(connection, options);
},
getAuthTokenFromCode(connection, options) {
return azureGetAuthTokenFromCode(connection, options);
},
}; };
driver.initialize = dbgateEnv => { driver.initialize = dbgateEnv => {
if (dbgateEnv.nativeModules && dbgateEnv.nativeModules.msnodesqlv8) { if (dbgateEnv.nativeModules && dbgateEnv.nativeModules.msnodesqlv8) {
requireMsnodesqlv8 = dbgateEnv.nativeModules.msnodesqlv8; requireMsnodesqlv8 = dbgateEnv.nativeModules.msnodesqlv8;
} }
platformInfo = dbgateEnv.platformInfo;
nativeDriver.initialize(dbgateEnv); nativeDriver.initialize(dbgateEnv);
}; };

Some files were not shown because too many files have changed in this diff Show More