diff --git a/LICENSE b/LICENSE index 3b73f734..15c6f98a 100644 --- a/LICENSE +++ b/LICENSE @@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - QueryM, Database Client + Querym, Database Client Copyright (C) 2023 Visal .In This program is free software: you can redistribute it and/or modify @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - QueryM Copyright (C) 2023 Visal .In + m Copyright (C) 2023 Visal .In This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff --git a/package.json b/package.json index baafe006..79aab58d 100644 --- a/package.json +++ b/package.json @@ -195,12 +195,12 @@ }, "build": { "protocols": { - "name": "QueryM", + "name": "Querym", "schemes": [ "querymaster" ] }, - "productName": "QueryM", + "productName": "Querym", "appId": "com.invisal.querymaster", "asar": true, "asarUnpack": "**\\*.{node,dll}", diff --git a/src/libs/ConnectionListStorage.ts b/src/libs/ConnectionListStorage/ConnectionListLocalStorage.ts similarity index 96% rename from src/libs/ConnectionListStorage.ts rename to src/libs/ConnectionListStorage/ConnectionListLocalStorage.ts index fb9364e4..57d4c488 100644 --- a/src/libs/ConnectionListStorage.ts +++ b/src/libs/ConnectionListStorage/ConnectionListLocalStorage.ts @@ -2,7 +2,7 @@ import { ConnectionStoreItem } from 'drivers/base/SQLLikeConnection'; import { v1 as uuidv1 } from 'uuid'; import { db } from 'renderer/db'; -export default class ConnectionListStorage { +export default class ConnectionListLocalStorage { protected connections: ConnectionStoreItem[] = []; protected dict: Record = {}; diff --git a/src/libs/ConnectionListStorage/ConnectionListRemoteStorage.ts b/src/libs/ConnectionListStorage/ConnectionListRemoteStorage.ts new file mode 100644 index 00000000..de15a323 --- /dev/null +++ b/src/libs/ConnectionListStorage/ConnectionListRemoteStorage.ts @@ -0,0 +1,98 @@ +import { + ConnectionStoreConfig, + ConnectionStoreItem, +} from 'drivers/base/SQLLikeConnection'; +import { QueryDialetType } from 'libs/QueryBuilder'; +import RemoteAPI from 'renderer/utils/RemoteAPI'; + +export default class ConnectionListRemoteStorage { + protected connections: ConnectionStoreItem[] = []; + protected dict: Record = {}; + protected masterPassword: string; + protected salt: string; + protected api: RemoteAPI; + + constructor(api: RemoteAPI, masterPassword: string, salt: string) { + this.api = api; + this.masterPassword = masterPassword; + this.salt = salt; + } + + async loadAll() { + const conns = await this.api.getAll(); + this.connections = []; + + for (const conn of conns.nodes) { + try { + const config: ConnectionStoreConfig = JSON.parse( + await window.electron.decrypt( + conn.content, + this.masterPassword, + this.salt, + ), + ); + + if (config) { + this.connections.push({ + config, + id: conn.id, + createdAt: conn.created_at, + lastUsedAt: conn.last_used_at, + name: conn.name, + type: conn.connection_type as QueryDialetType, + }); + } + } catch (e) { + console.error(e); + } + } + + this.dict = this.connections.reduce( + (acc, cur) => { + acc[cur.id] = cur; + return acc; + }, + {} as Record, + ); + } + + get(id: string): ConnectionStoreItem | undefined { + return this.dict[id]; + } + + getAll(): ConnectionStoreItem[] { + return this.connections; + } + + async save( + data: Omit & { id?: string }, + ): Promise { + const r = await this.api.saveConnection(data.id, { + connection_type: data.type, + content: await window.electron.encrypt( + JSON.stringify(data.config), + this.masterPassword, + this.salt, + ), + name: data.name, + }); + + const newData = { ...data, id: r.id }; + this.dict[newData.id] = newData; + + return newData; + } + + async remove(id: string) { + delete this.dict[id]; + this.connections = this.connections.filter((conn) => conn.id !== id); + this.api.removeConnection(id); + } + + async updateLastUsed(id: string) { + if (this.dict[id]) { + this.dict[id].lastUsedAt = Math.ceil(Date.now() / 1000); + this.api.updateConnectionLastUsed(id); + } + } +} diff --git a/src/libs/ConnectionListStorage/IConnectionListStorage.ts b/src/libs/ConnectionListStorage/IConnectionListStorage.ts new file mode 100644 index 00000000..d05a4df7 --- /dev/null +++ b/src/libs/ConnectionListStorage/IConnectionListStorage.ts @@ -0,0 +1,12 @@ +import { ConnectionStoreItem } from 'drivers/base/SQLLikeConnection'; + +export default abstract class IConnectionListStorage { + abstract loadAll(): Promise; + abstract get(id: string): ConnectionStoreItem | undefined; + abstract getAll(): ConnectionStoreItem[]; + abstract save( + data: Omit & { id?: string }, + ): Promise; + abstract remove(id: string): Promise; + abstract updateLastUsed(id: string): Promise; +} diff --git a/src/libs/SqlRunnerManager.ts b/src/libs/SqlRunnerManager.ts index 4c7885a6..48f88b74 100644 --- a/src/libs/SqlRunnerManager.ts +++ b/src/libs/SqlRunnerManager.ts @@ -9,12 +9,12 @@ export interface SqlStatementWithAnalyze extends SqlStatement { export type BeforeAllEventCallback = ( statements: SqlStatementWithAnalyze[], - skipProtection?: boolean + skipProtection?: boolean, ) => Promise; export type BeforeEachEventCallback = ( statements: SqlStatementWithAnalyze, - skipProtection?: boolean + skipProtection?: boolean, ) => Promise; export interface SqlStatementResult { @@ -41,13 +41,11 @@ export class SqlRunnerManager { async execute( statements: SqlStatement[], - options?: SqlExecuteOption + options?: SqlExecuteOption, ): Promise { const result: SqlStatementResult[] = []; const parser = new Parser(); - console.log(statements); - // We only wrap transaction if it is multiple statement and // insideTransactin is specified. Single statement, by itself, is // transactional already. @@ -89,7 +87,7 @@ export class SqlRunnerManager { const startTime = Date.now(); const returnedResult = await this.executor( statement.sql, - statement.params + statement.params, ); if (!returnedResult?.error) { @@ -117,7 +115,7 @@ export class SqlRunnerManager { unregisterBeforeAll(cb: BeforeAllEventCallback) { this.beforeAllCallbacks = this.beforeAllCallbacks.filter( - (prevCb) => prevCb !== cb + (prevCb) => prevCb !== cb, ); } @@ -127,7 +125,7 @@ export class SqlRunnerManager { unregisterBeforeEach(cb: BeforeEachEventCallback) { this.beforeEachCallbacks = this.beforeEachCallbacks.filter( - (prevCb) => prevCb !== cb + (prevCb) => prevCb !== cb, ); } } diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 1550f073..6e44a010 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -3,3 +3,4 @@ import './ipc_native_menu'; import './ipc_other'; import './ipc_rdms'; import './ipc_auto_update'; +import './ipc_cipher'; diff --git a/src/main/ipc/ipc_cipher.ts b/src/main/ipc/ipc_cipher.ts new file mode 100644 index 00000000..8955c331 --- /dev/null +++ b/src/main/ipc/ipc_cipher.ts @@ -0,0 +1,64 @@ +import crypto from 'crypto'; +import CommunicateHandler from './../CommunicateHandler'; + +export class Encryption { + protected key: Buffer; + + constructor(masterkey: string, salt: string) { + this.key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512'); + } + + decrypt(encdata: string) { + const buffer = Buffer.from(encdata, 'base64'); + const iv = buffer.subarray(0, 16); + const data = buffer.subarray(16); + const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv); + const text = + decipher.update(data).toString('utf8') + decipher.final('utf8'); + return text; + } + + encrypt(plain: string) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv); + return Buffer.concat([ + iv, + cipher.update(plain, 'utf8'), + cipher.final(), + ]).toString('base64'); + } +} + +const EncryptionDict: Record = {}; + +CommunicateHandler.handle( + 'encrypt', + ([text, masterkey, salt]: [string, string, string]) => { + try { + const key = masterkey + '_' + salt; + if (!EncryptionDict[key]) { + EncryptionDict[key] = new Encryption(masterkey, salt); + } + + return EncryptionDict[key].encrypt(text); + } catch { + return null; + } + }, +); + +CommunicateHandler.handle( + 'decrypt', + ([encrypted, masterkey, salt]: [string, string, string]) => { + try { + const key = masterkey + '_' + salt; + if (!EncryptionDict[key]) { + EncryptionDict[key] = new Encryption(masterkey, salt); + } + + return EncryptionDict[key].decrypt(encrypted); + } catch { + return null; + } + }, +); diff --git a/src/main/preload.ts b/src/main/preload.ts index c82b6423..f5a8c46b 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -57,7 +57,7 @@ const electronHandler = { // Related to File O/I // ---------------------------------- showSaveDialog: ( - options: SaveDialogSyncOptions + options: SaveDialogSyncOptions, ): Promise => ipcRenderer.invoke('show-save-dialog', [options]), @@ -78,7 +78,7 @@ const electronHandler = { checkForUpdates: () => ipcRenderer.invoke('check-for-updates'), handleMenuClick: ( - callback: (event: IpcRendererEvent, id: string) => void + callback: (event: IpcRendererEvent, id: string) => void, ) => { if (cacheHandleMenuClickCb) { ipcRenderer.off('native-menu-click', cacheHandleMenuClickCb); @@ -88,7 +88,7 @@ const electronHandler = { }, listenDeeplink: ( - callback: (event: IpcRendererEvent, url: string) => void + callback: (event: IpcRendererEvent, url: string) => void, ) => { ipcRenderer.removeAllListeners('deeplink'); return ipcRenderer.on('deeplink', callback); @@ -100,28 +100,28 @@ const electronHandler = { }, listenUpdateAvailable: ( - callback: (event: IpcRendererEvent, e: UpdateInfo) => void + callback: (event: IpcRendererEvent, e: UpdateInfo) => void, ) => { ipcRenderer.removeAllListeners('update-available'); return ipcRenderer.on('update-available', callback); }, listenUpdateNotAvailable: ( - callback: (event: IpcRendererEvent, e: UpdateInfo) => void + callback: (event: IpcRendererEvent, e: UpdateInfo) => void, ) => { ipcRenderer.removeAllListeners('update-not-available'); return ipcRenderer.on('update-not-available', callback); }, listenUpdateDownloadProgress: ( - callback: (event: IpcRendererEvent, e: ProgressInfo) => void + callback: (event: IpcRendererEvent, e: ProgressInfo) => void, ) => { ipcRenderer.removeAllListeners('update-download-progress'); return ipcRenderer.on('update-download-progress', callback); }, listenUpdateDownloaded: ( - callback: (event: IpcRendererEvent, e: UpdateDownloadedEvent) => void + callback: (event: IpcRendererEvent, e: UpdateDownloadedEvent) => void, ) => { ipcRenderer.removeAllListeners('update-downloaded'); return ipcRenderer.on('update-downloaded', callback); @@ -129,12 +129,18 @@ const electronHandler = { listen: function listen( name: string, - callback: (event: IpcRendererEvent, ...args: T[]) => void + callback: (event: IpcRendererEvent, ...args: T[]) => void, ) { return ipcRenderer.on(name, callback); }, openExternal: (url: string) => ipcRenderer.invoke('open-external', [url]), + + // Encryption + encrypt: (text: string, masterKey: string, salt: string) => + ipcRenderer.invoke('encrypt', [text, masterKey, salt]), + decrypt: (encrypted: string, masterKey: string, salt: string) => + ipcRenderer.invoke('decrypt', [encrypted, masterKey, salt]), }; contextBridge.exposeInMainWorld('electron', electronHandler); diff --git a/src/renderer/components/ConnectionListTree/index.tsx b/src/renderer/components/ConnectionListTree/index.tsx index eb7fe557..2390c4cb 100644 --- a/src/renderer/components/ConnectionListTree/index.tsx +++ b/src/renderer/components/ConnectionListTree/index.tsx @@ -2,7 +2,6 @@ import { ConnectionStoreItem, ConnectionStoreItemWithoutId, } from 'drivers/base/SQLLikeConnection'; -import ConnectionListStorage from 'libs/ConnectionListStorage'; import { useState, useMemo, @@ -20,9 +19,11 @@ import ConnectionToolbar from './ConnectionToolbar'; import useConnectionContextMenu from './useConnectionContextMenu'; import { useConnection } from 'renderer/App'; import ConnectionIcon from '../ConnectionIcon'; +import IConnectionListStorage from 'libs/ConnectionListStorage/IConnectionListStorage'; +import ConnectionListLocalStorage from 'libs/ConnectionListStorage/ConnectionListLocalStorage'; const ConnectionListContext = createContext<{ - storage: ConnectionListStorage; + storage: IConnectionListStorage; refresh: () => void; finishEditing: () => void; showEditConnection: (initialValue: ConnectionStoreItemWithoutId) => void; @@ -30,7 +31,7 @@ const ConnectionListContext = createContext<{ React.SetStateAction | undefined> >; }>({ - storage: new ConnectionListStorage(), + storage: new ConnectionListLocalStorage(), refresh: NotImplementCallback, finishEditing: NotImplementCallback, showEditConnection: NotImplementCallback, @@ -130,8 +131,11 @@ function ConnectionListTreeBody({ ); } -export default function ConnectionListTree() { - const storage = useMemo(() => new ConnectionListStorage(), []); +export default function ConnectionListTree({ + storage, +}: { + storage: IConnectionListStorage; +}) { const [selectedItem, setSelectedItem] = useState>(); const [editingItem, setEditingItem] = diff --git a/src/renderer/components/ConnectionListTree/useConnectionContextMenu.tsx b/src/renderer/components/ConnectionListTree/useConnectionContextMenu.tsx index ab3b3fe5..e6a02c5d 100644 --- a/src/renderer/components/ConnectionListTree/useConnectionContextMenu.tsx +++ b/src/renderer/components/ConnectionListTree/useConnectionContextMenu.tsx @@ -36,13 +36,18 @@ export default function useConnectionContextMenu({ { text: 'Connect', disabled: !selectedItem, - separator: true, + onClick: () => { if (selectedItem) { connectWithRecordUpdate(selectedItem); } }, }, + { + text: 'Refresh', + separator: true, + onClick: refresh, + }, { text: 'New Connection', children: newConnectionMenu, @@ -70,6 +75,7 @@ export default function useConnectionContextMenu({ onRemoveClick, showEditConnection, connectWithRecordUpdate, + refresh, ]); return { handleContextMenu }; diff --git a/src/renderer/components/PasswordField/index.tsx b/src/renderer/components/PasswordField/index.tsx index 8b8255cf..2c24cb2f 100644 --- a/src/renderer/components/PasswordField/index.tsx +++ b/src/renderer/components/PasswordField/index.tsx @@ -1,38 +1,39 @@ import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useState } from 'react'; +import { useState, forwardRef } from 'react'; import TextField, { TextFieldCommonProps } from 'renderer/components/TextField'; -export default function PasswordField({ - label, - value, - autoFocus, - onChange, - placeholder, - readOnly, -}: TextFieldCommonProps) { - const props = { - label, - value, - autoFocus, - onChange, - placeholder, - readOnly, - }; - const [showPassword, setShowPassword] = useState(false); +const PasswordField = forwardRef( + function PasswordField( + { label, value, autoFocus, onChange, placeholder, readOnly }, + ref, + ) { + const props = { + label, + value, + autoFocus, + onChange, + placeholder, + readOnly, + }; + const [showPassword, setShowPassword] = useState(false); - return ( - setShowPassword(!showPassword)} - actionIcon={ - showPassword ? ( - - ) : ( - - ) - } - {...props} - /> - ); -} + return ( + setShowPassword(!showPassword)} + actionIcon={ + showPassword ? ( + + ) : ( + + ) + } + {...props} + /> + ); + }, +); + +export default PasswordField; diff --git a/src/renderer/components/StatusBar/index.tsx b/src/renderer/components/StatusBar/index.tsx index 59e5a054..9e820a4b 100644 --- a/src/renderer/components/StatusBar/index.tsx +++ b/src/renderer/components/StatusBar/index.tsx @@ -98,7 +98,7 @@ export default function StatusBar() { return (
    -
  • QueryM v{pkg.version}
  • +
  • Querym v{pkg.version}
  • {!!connectionStatus?.version &&
  • {connectionStatus?.version}
  • } {!!connectionStatus?.status && (
  • diff --git a/src/renderer/components/TextField/TextAreaField.tsx b/src/renderer/components/TextField/TextAreaField.tsx new file mode 100644 index 00000000..e6baabef --- /dev/null +++ b/src/renderer/components/TextField/TextAreaField.tsx @@ -0,0 +1,34 @@ +import { forwardRef } from 'react'; +import styles from './styles.module.scss'; +import { TextFieldCommonProps } from '.'; + +const TextAreaField = forwardRef( + function TextField( + { label, value, autoFocus, onChange, placeholder, readOnly, onKeyDown }, + ref, + ) { + return ( +
    + {label && } + +
    +