Skip to content

Commit 371b016

Browse files
committed
feat(desktop): add secure credential storage with keytar
1 parent 1dec57d commit 371b016

15 files changed

Lines changed: 427 additions & 108 deletions

File tree

.github/FUNDING.yml

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,74 @@
1-
---
21
name: Bug Report
3-
about: Report a bug or unexpected behavior
4-
labels: ["bug"]
2+
description: Report a bug or unexpected behavior
3+
labels: ['bug']
54
assignees: []
6-
---
75

8-
## Bug Description
6+
body:
7+
- type: textarea
8+
attributes:
9+
label: Bug Description
10+
description: |
11+
Please describe the bug you encountered in as much detail as possible.
912
10-
Please describe the bug you encountered in as much detail as possible.
13+
- What happened?
14+
- What did you expect to happen?
15+
- Are there any error messages shown?
16+
placeholder: |
17+
Describe the bug clearly and concisely.
18+
validations:
19+
required: true
1120

12-
- What happened?
13-
- What did you expect to happen?
14-
- Are there any error messages shown?
21+
- type: textarea
22+
attributes:
23+
label: Steps to Reproduce
24+
description: |
25+
Please include exact steps so we can reproduce the issue.
26+
placeholder: |
27+
1.
28+
2.
29+
3.
30+
4.
31+
validations:
32+
required: true
1533

16-
## Steps to Reproduce
34+
- type: textarea
35+
attributes:
36+
label: Screenshots / Logs
37+
description: |
38+
If applicable, add screenshots or logs to help explain your problem.
39+
placeholder: |
40+
Paste logs here or attach screenshots.
1741
18-
1.
19-
2.
20-
3.
21-
4.
42+
- type: markdown
43+
attributes:
44+
value: |
45+
## Environment
2246
23-
_(Please include exact steps so we can reproduce the issue.)_
47+
- type: input
48+
attributes:
49+
label: OS
50+
placeholder: macOS / Windows / Linux
51+
validations:
52+
required: true
2453

25-
## Screenshots / Logs
54+
- type: input
55+
attributes:
56+
label: Datary Version
57+
placeholder: 0.0.1
58+
validations:
59+
required: true
2660

27-
If applicable, add screenshots or logs to help explain your problem.
61+
- type: input
62+
attributes:
63+
label: Database
64+
placeholder: PostgreSQL / MySQL / MariaDB / Microsoft SQL Server
65+
validations:
66+
required: true
2867

29-
## Environment
30-
31-
| Feature | Details |
32-
|---------|---------|
33-
| OS | |
34-
| Datary Version | |
35-
| Database | |
36-
37-
## Additional Context
38-
39-
Add any other context about the problem here.
68+
- type: textarea
69+
attributes:
70+
label: Additional Context
71+
description: |
72+
Add any other context about the problem here.
73+
placeholder: |
74+
Any extra information that may help.

.github/ISSUE_TEMPLATE/feature-request.yml

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Feature Request
2+
description: Suggest a new idea or improvement for Datary
3+
labels: ['enhancement']
4+
assignees: []
5+
6+
body:
7+
- type: markdown
8+
attributes:
9+
value: |
10+
Thanks for taking the time to suggest an improvement!
11+
Datary is a database management tool, so features related to databases, UX, performance, or integrations are especially welcome.
12+
13+
- type: textarea
14+
attributes:
15+
label: Feature description
16+
description: |
17+
What feature are you requesting?
18+
Explain what it does and why it would be valuable.
19+
placeholder: |
20+
Example:
21+
Add support for exporting query results to CSV/JSON directly from the results table.
22+
validations:
23+
required: true
24+
25+
- type: textarea
26+
attributes:
27+
label: Use case / motivation
28+
description: |
29+
Describe the problem this feature solves and who would benefit from it.
30+
placeholder: |
31+
Example:
32+
When working with large datasets, I often need to share query results with teammates.
33+
Currently, I have to copy data manually or use external tools.
34+
validations:
35+
required: true
36+
37+
- type: textarea
38+
attributes:
39+
label: Suggested implementation
40+
description: |
41+
If you have ideas on how this could work (UI, behavior, API), describe them here.
42+
This is optional but very helpful.
43+
placeholder: |
44+
Example:
45+
- Add an "Export" button near the results table
46+
- Allow selecting format (CSV / JSON)
47+
- Save file via native OS dialog
48+
49+
- type: textarea
50+
attributes:
51+
label: Examples / references
52+
description: |
53+
If similar functionality exists in other tools, add links, screenshots, or descriptions.
54+
placeholder: |
55+
Example:
56+
DBeaver has a similar export dialog with format selection and preview.

apps/desktop/main/app/window.manager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import { mainWindow } from './app.state'
99
export function createMainWindow() {
1010
const preloadPath = getPreloadPath(__dirname)
1111

12-
console.log('preloadPath: ', preloadPath)
13-
1412
const win = new BrowserWindow({
1513
width: 1200,
1614
height: 800,
@@ -28,6 +26,9 @@ export function createMainWindow() {
2826
mainWindow.set(win)
2927

3028
if (isDev && DEV_SERVER_URL) {
29+
win.webContents.closeDevTools()
30+
win.webContents.setVisualZoomLevelLimits(1, 1)
31+
3132
win.loadURL(DEV_SERVER_URL)
3233
} else {
3334
win.loadFile(getRendererIndex(__dirname))

apps/desktop/main/ipc/handlers/connection.handler.ts

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,83 @@ import { IPC_CHANNELS } from '@datary/ipc'
22
import { ipcMain } from 'electron'
33
import Store from 'electron-store'
44

5+
import { buildCredentialId } from '../../services/credential/credential.utils'
6+
import { CredentialVault } from '../../services/credential/credential.vault'
57
import { logger } from '../../utils/logger'
68

79
export interface ConnectionHistoryItem {
8-
id?: string
10+
id: string
911
name?: string
1012
host: string
1113
port: number
1214
user: string
13-
password?: string
1415
database: string
1516
type: 'postgresql' | 'mysql' | 'mariadb' | 'mssql'
1617
ssl?: boolean
1718
lastUsed?: number
1819
}
1920

20-
const ENCRYPTION_KEY = 'datary-secret-key-123'
2121
const store = new Store<{ history: ConnectionHistoryItem[] }>({
2222
name: 'connections',
23-
defaults: { history: [] },
24-
encryptionKey: ENCRYPTION_KEY
23+
defaults: { history: [] }
2524
})
2625

26+
const vault = new CredentialVault()
27+
2728
export function registerConnectionHandlers() {
2829
ipcMain.handle(IPC_CHANNELS.CONNECTIONS.GET, () => {
2930
return store.get('history') ?? []
3031
})
3132

32-
ipcMain.handle(IPC_CHANNELS.CONNECTIONS.ADD, (_, connection: ConnectionHistoryItem) => {
33-
const history = store.get('history') ?? []
34-
const index = history.findIndex(
35-
c =>
36-
c.host === connection.host &&
37-
c.port === connection.port &&
38-
c.user === connection.user &&
39-
c.database === connection.database &&
40-
c.type === connection.type
41-
)
42-
43-
const newConnection = { ...connection, lastUsed: Date.now() }
44-
45-
if (index > -1) {
46-
history[index] = newConnection
47-
} else {
48-
history.unshift(newConnection)
49-
}
33+
ipcMain.handle(
34+
IPC_CHANNELS.CONNECTIONS.ADD,
35+
async (_, connection: ConnectionHistoryItem & { password?: string }) => {
36+
const history = store.get('history') ?? []
5037

51-
store.set('history', history)
52-
return history
53-
})
38+
const credentialId = buildCredentialId(connection)
39+
40+
if (connection.password) await vault.save(credentialId, connection.password)
5441

55-
ipcMain.handle(IPC_CHANNELS.CONNECTIONS.DELETE, (_, connection: ConnectionHistoryItem) => {
56-
const history = store.get('history') ?? []
57-
const filtered = history.filter(
58-
c =>
59-
!(
42+
const index = history.findIndex(
43+
c =>
6044
c.host === connection.host &&
6145
c.port === connection.port &&
6246
c.user === connection.user &&
6347
c.database === connection.database &&
6448
c.type === connection.type
65-
)
66-
)
67-
store.set('history', filtered)
68-
return filtered
69-
})
49+
)
50+
51+
const newConnection = { ...connection, lastUsed: Date.now() }
52+
53+
if (index > -1) history[index] = newConnection
54+
else history.unshift(newConnection)
55+
56+
store.set('history', history)
57+
return history
58+
}
59+
)
60+
61+
ipcMain.handle(
62+
IPC_CHANNELS.CONNECTIONS.DELETE,
63+
async (_, connection: ConnectionHistoryItem) => {
64+
if (connection.id) await vault.delete(connection.id)
65+
66+
const history = store.get('history') ?? []
67+
const filtered = history.filter(
68+
c =>
69+
!(
70+
c.host === connection.host &&
71+
c.port === connection.port &&
72+
c.user === connection.user &&
73+
c.database === connection.database &&
74+
c.type === connection.type
75+
)
76+
)
77+
78+
store.set('history', filtered)
79+
return filtered
80+
}
81+
)
7082

7183
ipcMain.handle(IPC_CHANNELS.CONNECTIONS.CLEAR, () => {
7284
store.set('history', [])
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { ConnectionType } from '@datary/core'
2+
3+
export interface DbCredential {
4+
id: string
5+
username: string
6+
password: string
7+
type: ConnectionType
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { ConnectionHistoryItem } from '../../ipc/handlers/connection.handler'
2+
3+
export function buildCredentialId(connection: ConnectionHistoryItem) {
4+
return `${connection.type}::/${connection.user}@${connection.host}:${connection.port}/${connection.database}`
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import keytar from 'keytar'
2+
3+
const SERVICE_NAME = 'datary'
4+
5+
export class CredentialVault {
6+
public async save(id: string, value: string): Promise<void> {
7+
await keytar.setPassword(SERVICE_NAME, id, value)
8+
}
9+
10+
public async get(id: string): Promise<string | null> {
11+
return keytar.getPassword(SERVICE_NAME, id)
12+
}
13+
14+
public async delete(id: string): Promise<void> {
15+
await keytar.deletePassword(SERVICE_NAME, id)
16+
}
17+
}

apps/desktop/package.json

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,8 @@
22
"name": "@datary/desktop",
33
"version": "0.0.1",
44
"main": "dist/main/index.js",
5-
"author": {
6-
"name": "Vadim (TeaCoder)",
7-
"url": "https://github.com/TeaCoder52"
8-
},
9-
"repository": {
10-
"type": "git",
11-
"url": "https://github.com/TeaCoder52/datary.git"
12-
},
135
"scripts": {
6+
"postinstall": "node node_modules/electron/install.js && pnpm exec electron-rebuild -f -w keytar",
147
"dev": "concurrently \"pnpm dev:preload\" \"pnpm dev:renderer\" \"pnpm dev:main\" \"pnpm dev:electron\"",
158
"dev:renderer": "pnpm --filter @datary/renderer dev",
169
"dev:preload": "tsc -p preload/tsconfig.json --watch",
@@ -26,7 +19,9 @@
2619
"@datary/core": "workspace:*",
2720
"@datary/db": "workspace:*",
2821
"@datary/ipc": "workspace:*",
29-
"electron-store": "^11.0.2"
22+
"add": "^2.0.6",
23+
"electron-store": "^11.0.2",
24+
"keytar": "^7.9.0"
3025
},
3126
"devDependencies": {
3227
"concurrently": "^9.2.1",
@@ -42,7 +37,11 @@
4237
"dist/**/*",
4338
"preload/**/*",
4439
"renderer/dist/**/*",
45-
"assets/icons/**/*"
40+
"assets/icons/**/*",
41+
"!**/*.map",
42+
"!**/tests/**",
43+
"!**/__mocks__/**",
44+
"!**/*.spec.ts"
4645
],
4746
"directories": {
4847
"output": "build"

0 commit comments

Comments
 (0)