mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
536dc35258
* Feature request network graph
* Fixing PR442:
- Fixed:
- UI design elemets
- UI and button colors
- JSON export
- recent activity is default again
- Removed:
- Online/Offline UI labels
- left-click menu on hosts
- Added:
- small pulsing dot inside the hosts to indicate online status like in the left bar
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
* fix: build error on docker
* Handle enter button (#481)
* Update Crowdin configuration file
* Update Crowdin configuration file
* Update Linux Portable section with AUR link (#474)
* fix: file manager incorrectly decoding/encoding when editing files (#476)
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
---------
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: build error on docker (#477)
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
* fix: build error on docker
---------
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* Increase max old space size for npm builds
* Increase Node.js memory limit in Dockerfile
* Remove NODE_OPTIONS from build commands in Dockerfile
* Change runner to blacksmith-4vcpu-ubuntu-2404
* fix: build error on docker
* Add handle on enter button;
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
* fix: remove top tech
* fix: update readme
* fix: prevent long container names from overflowing card (#496)
Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.
Fixes #411
* fix: use SFTP readdir for file listing to support non-Linux systems (#495)
The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.
Fixes #317
* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)
The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.
Fixes #404
* feat: add Docker container healthcheck (#493)
* fix: owner should not be marked as shared when host is shared to their role (#492)
* fix: use correct MIME types for image preview (#491)
* fix: prevent session reset when updating host properties (#490)
* fix: add shell creation timeout and improve error handling (#489)
* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)
* fix: delete all related data when removing user (#487)
* fix: nginx permission denied on restricted kernels (#486)
* fix: skip existing hosts and credentials during JSON import (#485)
Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.
This matches the existing behavior of importDismissedAlerts.
Fixes #389
* feat: add firewall status widget for server stats (#484)
* Feature: PWA (#479)
* feat: add PWA support with offline capabilities
- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching
* Update package-lock.json
* New Crowdin updates (#472)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Swedish)
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (English)
* New translations en.json (Vietnamese)
* New translations en.json (German)
* feat: add listening ports widget for server stats (#483)
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* feat: fix network stats merge and add openapi jsdocs comments
* feat: add workflow/config to auto generate openapi json
* feat: remove locales
* feat: support URL routes to open terminal directly (#156) (#503)
* fix: resolve merge conflict artifacts in dev-1.10.1
- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts
* feat: support URL routes to open terminal directly (#156)
- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)
Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.
- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)
Fixes Termix-SSH/Support#407
* feat: remove locales
* New Crowdin updates (#504)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Swedish)
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (English)
* New translations en.json (Vietnamese)
* New translations en.json (German)
* New translations en.json (Norwegian)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Arabic)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Swedish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Finnish)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* feat: add option to disable update checker (#502)
* feat: add option to disable update checker
Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.
- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client
Fixes Termix-SSH/Support#410
* feat: remove locales
---------
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
* New Crowdin updates (#505)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Swedish)
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (English)
* New translations en.json (Vietnamese)
* New translations en.json (German)
* New translations en.json (Norwegian)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Arabic)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Swedish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Finnish)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Bulgarian)
* New translations en.json (Indonesian)
* New translations en.json (Hindi)
* feat: add crowdin i18n
* feat: remove locales
* New Crowdin updates (#506)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Swedish)
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (English)
* New translations en.json (Vietnamese)
* New translations en.json (German)
* New translations en.json (Norwegian)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Arabic)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Swedish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Finnish)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Bulgarian)
* New translations en.json (Indonesian)
* New translations en.json (Hindi)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Vietnamese)
* New translations en.json (Portuguese, Brazilian)
* New translations en.json (Bulgarian)
* New translations en.json (Indonesian)
* New translations en.json (Hindi)
* New translations en.json (Bengali)
* New translations en.json (Thai)
* feat: update readme
* feat: update readme
* feat: update credential editor to use submitting system and add health monitor
* feat: added toggle for command pallete
* feat: added close button on tab dropdown
* feat: added sidebar management and improved some host manager UI/UX
* feat: re-added missing users.ts route from merge
* feat: add toggle for password reset feature in admin settings (#508)
* feat: add sudo support for file manager operations (#509)
* fix: add sudo support for listFiles and improve permission error handling (#512)
* feat: add sudo support for file manager operations
* fix: add sudo support for listFiles and improve permission error handling
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated
* feat: add copy password button and fixed new line carriage issues and backend crash for auth key
* feat: added quick connection system (ad-hoc)
* Enter Key for Quick Login (#513)
* feat: added -r and -l support for tunnels
* feat: begin dashboard overhaul by splitting into cards and adding customization
* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.
* feat: add auth.tsx suppot for fullscreen
* feat: greatly improve network graph ui/ux and migrated to use translations and theme system
* feat: update to use blacksmith
* feat: improve ui for customized tabs and hide add/edit host/credential when submiting
* feat: add warpgate support with a dialog (terminal only)
* feat: expand warpgate to docker/file manager
* fix: docker not working wtih warpgate and none auth failing for terminal
* fix: prevent owner permission loss when sharing host to own role (#514)
* Update Crowdin configuration file
* Update Crowdin configuration file
* Update Linux Portable section with AUR link (#474)
* fix: file manager incorrectly decoding/encoding when editing files (#476)
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
---------
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: build error on docker (#477)
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
* fix: build error on docker
---------
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* Increase max old space size for npm builds
* Increase Node.js memory limit in Dockerfile
* Remove NODE_OPTIONS from build commands in Dockerfile
* Change runner to blacksmith-4vcpu-ubuntu-2404
* fix: build error on docker
* fix: prevent owner permission loss when sharing host to own role
Fixes #391
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
* fix: SSH key passphrase not passed for Docker and Tunnel (#521)
* perf: optimize Host Manager for large host lists
- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)
Fixes performance issues when managing ~1000 hosts with status monitoring enabled.
* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel
Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.
Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* perf: optimize Host Manager for large host lists (#520)
- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)
Fixes performance issues when managing ~1000 hosts with status monitoring enabled.
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* fix: add missing mimeTypes definition for image preview (#518)
Fixes Termix-SSH/Support#408
* fix: prevent session reset when updating host properties (#517)
Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.
Closes Termix-SSH/Support#401
* fix: backend type error
* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)
* feat: improved conneciton log ui/logic
* feat: improved conneciton log ui/logic
* feat: expanded connection log to work across all components (readying for release)
* feat: update readme
* feat: update readme
* fix: build error
* fix: build error
* fix: changed ver
* chore: clean up
* chore: continue clean up
* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies
* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field
* fix: update readme and run cleaner
* fix: update readme
* fix: update readme
* fix: update readme
* feat: update chinese readme
---------
Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>
1224 lines
38 KiB
TypeScript
1224 lines
38 KiB
TypeScript
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
import Database from "better-sqlite3";
|
|
import * as schema from "./schema.js";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { databaseLogger } from "../../utils/logger.js";
|
|
import { DatabaseFileEncryption } from "../../utils/database-file-encryption.js";
|
|
import { SystemCrypto } from "../../utils/system-crypto.js";
|
|
import { DatabaseMigration } from "../../utils/database-migration.js";
|
|
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
|
|
|
|
const dataDir = process.env.DATA_DIR || "./db/data";
|
|
const dbDir = path.resolve(dataDir);
|
|
if (!fs.existsSync(dbDir)) {
|
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
}
|
|
|
|
const enableFileEncryption = process.env.DB_FILE_ENCRYPTION !== "false";
|
|
const dbPath = path.join(dataDir, "db.sqlite");
|
|
const encryptedDbPath = `${dbPath}.encrypted`;
|
|
|
|
const actualDbPath = ":memory:";
|
|
let memoryDatabase: Database.Database;
|
|
let isNewDatabase = false;
|
|
let sqlite: Database.Database;
|
|
|
|
async function initializeDatabaseAsync(): Promise<void> {
|
|
const systemCrypto = SystemCrypto.getInstance();
|
|
|
|
await systemCrypto.getDatabaseKey();
|
|
if (enableFileEncryption) {
|
|
try {
|
|
if (DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)) {
|
|
const decryptedBuffer =
|
|
await DatabaseFileEncryption.decryptDatabaseToBuffer(encryptedDbPath);
|
|
|
|
memoryDatabase = new Database(decryptedBuffer);
|
|
|
|
try {
|
|
const sessionCount = memoryDatabase
|
|
.prepare("SELECT COUNT(*) as count FROM sessions")
|
|
.get() as { count: number };
|
|
} catch (countError) {
|
|
}
|
|
} else {
|
|
const migration = new DatabaseMigration(dataDir);
|
|
const migrationStatus = migration.checkMigrationStatus();
|
|
|
|
if (migrationStatus.needsMigration) {
|
|
const migrationResult = await migration.migrateDatabase();
|
|
|
|
if (migrationResult.success) {
|
|
migration.cleanupOldBackups();
|
|
|
|
if (
|
|
DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)
|
|
) {
|
|
const decryptedBuffer =
|
|
await DatabaseFileEncryption.decryptDatabaseToBuffer(
|
|
encryptedDbPath,
|
|
);
|
|
memoryDatabase = new Database(decryptedBuffer);
|
|
isNewDatabase = false;
|
|
} else {
|
|
throw new Error(
|
|
"Migration completed but encrypted database file not found",
|
|
);
|
|
}
|
|
} else {
|
|
databaseLogger.error("Automatic database migration failed", null, {
|
|
operation: "auto_migration_failed",
|
|
error: migrationResult.error,
|
|
migratedTables: migrationResult.migratedTables,
|
|
migratedRows: migrationResult.migratedRows,
|
|
duration: migrationResult.duration,
|
|
backupPath: migrationResult.backupPath,
|
|
});
|
|
throw new Error(
|
|
`Database migration failed: ${migrationResult.error}. Backup available at: ${migrationResult.backupPath}`,
|
|
);
|
|
}
|
|
} else {
|
|
memoryDatabase = new Database(":memory:");
|
|
isNewDatabase = true;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
databaseLogger.error("Failed to initialize memory database", error, {
|
|
operation: "db_memory_init_failed",
|
|
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
|
errorStack: error instanceof Error ? error.stack : undefined,
|
|
encryptedDbExists:
|
|
DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath),
|
|
databaseKeyAvailable: !!process.env.DATABASE_KEY,
|
|
databaseKeyLength: process.env.DATABASE_KEY?.length || 0,
|
|
});
|
|
|
|
try {
|
|
const diagnosticInfo =
|
|
DatabaseFileEncryption.getDiagnosticInfo(encryptedDbPath);
|
|
databaseLogger.error(
|
|
"Database encryption diagnostic completed - check logs above for details",
|
|
null,
|
|
{
|
|
operation: "db_encryption_diagnostic_completed",
|
|
filesConsistent: diagnosticInfo.validation.filesConsistent,
|
|
sizeMismatch: diagnosticInfo.validation.sizeMismatch,
|
|
},
|
|
);
|
|
} catch (diagError) {
|
|
databaseLogger.warn("Failed to generate diagnostic information", {
|
|
operation: "db_diagnostic_failed",
|
|
error:
|
|
diagError instanceof Error ? diagError.message : "Unknown error",
|
|
});
|
|
}
|
|
|
|
throw new Error(
|
|
`Database decryption failed: ${error instanceof Error ? error.message : "Unknown error"}. This prevents data loss.`,
|
|
);
|
|
}
|
|
} else {
|
|
memoryDatabase = new Database(":memory:");
|
|
isNewDatabase = true;
|
|
}
|
|
}
|
|
|
|
async function initializeCompleteDatabase(): Promise<void> {
|
|
await initializeDatabaseAsync();
|
|
|
|
databaseLogger.info(`Initializing SQLite database`, {
|
|
operation: "db_init",
|
|
path: actualDbPath,
|
|
encrypted:
|
|
enableFileEncryption &&
|
|
DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath),
|
|
inMemory: true,
|
|
isNewDatabase,
|
|
});
|
|
|
|
sqlite = memoryDatabase;
|
|
|
|
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
|
|
db = drizzle(sqlite, { schema });
|
|
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
|
is_oidc INTEGER NOT NULL DEFAULT 0,
|
|
oidc_identifier TEXT,
|
|
client_id TEXT,
|
|
client_secret TEXT,
|
|
issuer_url TEXT,
|
|
authorization_url TEXT,
|
|
token_url TEXT,
|
|
identifier_path TEXT,
|
|
name_path TEXT,
|
|
scopes TEXT DEFAULT 'openid email profile',
|
|
totp_secret TEXT,
|
|
totp_enabled INTEGER NOT NULL DEFAULT 0,
|
|
totp_backup_codes TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
jwt_token TEXT NOT NULL,
|
|
device_type TEXT NOT NULL,
|
|
device_info TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TEXT NOT NULL,
|
|
last_active_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ssh_data (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
name TEXT,
|
|
ip TEXT NOT NULL,
|
|
port INTEGER NOT NULL,
|
|
username TEXT NOT NULL,
|
|
folder TEXT,
|
|
tags TEXT,
|
|
pin INTEGER NOT NULL DEFAULT 0,
|
|
auth_type TEXT NOT NULL,
|
|
password TEXT,
|
|
key TEXT,
|
|
key_password TEXT,
|
|
key_type TEXT,
|
|
enable_terminal INTEGER NOT NULL DEFAULT 1,
|
|
enable_tunnel INTEGER NOT NULL DEFAULT 1,
|
|
tunnel_connections TEXT,
|
|
enable_file_manager INTEGER NOT NULL DEFAULT 1,
|
|
enable_docker INTEGER NOT NULL DEFAULT 0,
|
|
default_path TEXT,
|
|
autostart_password TEXT,
|
|
autostart_key TEXT,
|
|
autostart_key_password TEXT,
|
|
force_keyboard_interactive TEXT,
|
|
stats_config TEXT,
|
|
docker_config TEXT,
|
|
terminal_config TEXT,
|
|
notes TEXT,
|
|
use_socks5 INTEGER,
|
|
socks5_host TEXT,
|
|
socks5_port INTEGER,
|
|
socks5_username TEXT,
|
|
socks5_password TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS file_manager_recent (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
last_opened TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS file_manager_pinned (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
pinned_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS file_manager_shortcuts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dismissed_alerts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
alert_id TEXT NOT NULL,
|
|
dismissed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ssh_credentials (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
folder TEXT,
|
|
tags TEXT,
|
|
auth_type TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
password TEXT,
|
|
key TEXT,
|
|
key_password TEXT,
|
|
key_type TEXT,
|
|
usage_count INTEGER NOT NULL DEFAULT 0,
|
|
last_used TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ssh_credential_usage (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
credential_id INTEGER NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (credential_id) REFERENCES ssh_credentials (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS snippets (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
description TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ssh_folders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
color TEXT,
|
|
icon TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS recent_activity (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
host_name TEXT,
|
|
timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS command_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
command TEXT NOT NULL,
|
|
executed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS host_access (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
host_id INTEGER NOT NULL,
|
|
user_id TEXT,
|
|
role_id INTEGER,
|
|
granted_by TEXT NOT NULL,
|
|
permission_level TEXT NOT NULL DEFAULT 'use',
|
|
expires_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
last_accessed_at TEXT,
|
|
access_count INTEGER NOT NULL DEFAULT 0,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS roles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
display_name TEXT NOT NULL,
|
|
description TEXT,
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
permissions TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_roles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
role_id INTEGER NOT NULL,
|
|
granted_by TEXT,
|
|
granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, role_id),
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
resource_type TEXT NOT NULL,
|
|
resource_id TEXT,
|
|
resource_name TEXT,
|
|
details TEXT,
|
|
ip_address TEXT,
|
|
user_agent TEXT,
|
|
success INTEGER NOT NULL,
|
|
error_message TEXT,
|
|
timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS session_recordings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
host_id INTEGER NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
access_id INTEGER,
|
|
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
ended_at TEXT,
|
|
duration INTEGER,
|
|
commands TEXT,
|
|
dangerous_actions TEXT,
|
|
recording_path TEXT,
|
|
terminated_by_owner INTEGER DEFAULT 0,
|
|
termination_reason TEXT,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL
|
|
);
|
|
|
|
`);
|
|
|
|
try {
|
|
sqlite.prepare("DELETE FROM sessions").run();
|
|
} catch (e) {
|
|
databaseLogger.warn("Could not clear sessions on startup", {
|
|
operation: "db_init_session_cleanup_failed",
|
|
error: e,
|
|
});
|
|
}
|
|
|
|
migrateSchema();
|
|
|
|
try {
|
|
const row = sqlite
|
|
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
|
|
.get();
|
|
if (!row) {
|
|
sqlite
|
|
.prepare(
|
|
"INSERT INTO settings (key, value) VALUES ('allow_registration', 'true')",
|
|
)
|
|
.run();
|
|
}
|
|
} catch (e) {
|
|
databaseLogger.warn("Could not initialize default settings", {
|
|
operation: "db_init",
|
|
error: e,
|
|
});
|
|
}
|
|
|
|
try {
|
|
const row = sqlite
|
|
.prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
|
|
.get();
|
|
if (!row) {
|
|
sqlite
|
|
.prepare(
|
|
"INSERT INTO settings (key, value) VALUES ('allow_password_login', 'true')",
|
|
)
|
|
.run();
|
|
}
|
|
} catch (e) {
|
|
databaseLogger.warn("Could not initialize allow_password_login setting", {
|
|
operation: "db_init",
|
|
error: e,
|
|
});
|
|
}
|
|
}
|
|
|
|
const addColumnIfNotExists = (
|
|
table: string,
|
|
column: string,
|
|
definition: string,
|
|
) => {
|
|
try {
|
|
sqlite
|
|
.prepare(
|
|
`SELECT "${column}"
|
|
FROM ${table} LIMIT 1`,
|
|
)
|
|
.get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`ALTER TABLE ${table}
|
|
ADD COLUMN "${column}" ${definition};`);
|
|
} catch (alterError) {
|
|
databaseLogger.warn(`Failed to add column ${column} to ${table}`, {
|
|
operation: "schema_migration",
|
|
table,
|
|
column,
|
|
error: alterError,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const migrateSchema = () => {
|
|
addColumnIfNotExists("users", "is_admin", "INTEGER NOT NULL DEFAULT 0");
|
|
|
|
addColumnIfNotExists("users", "is_oidc", "INTEGER NOT NULL DEFAULT 0");
|
|
addColumnIfNotExists("users", "oidc_identifier", "TEXT");
|
|
addColumnIfNotExists("users", "client_id", "TEXT");
|
|
addColumnIfNotExists("users", "client_secret", "TEXT");
|
|
addColumnIfNotExists("users", "issuer_url", "TEXT");
|
|
addColumnIfNotExists("users", "authorization_url", "TEXT");
|
|
addColumnIfNotExists("users", "token_url", "TEXT");
|
|
|
|
addColumnIfNotExists("users", "identifier_path", "TEXT");
|
|
addColumnIfNotExists("users", "name_path", "TEXT");
|
|
addColumnIfNotExists("users", "scopes", "TEXT");
|
|
|
|
addColumnIfNotExists("users", "totp_secret", "TEXT");
|
|
addColumnIfNotExists("users", "totp_enabled", "INTEGER NOT NULL DEFAULT 0");
|
|
addColumnIfNotExists("users", "totp_backup_codes", "TEXT");
|
|
|
|
addColumnIfNotExists("ssh_data", "name", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "folder", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "tags", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "pin", "INTEGER NOT NULL DEFAULT 0");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"auth_type",
|
|
'TEXT NOT NULL DEFAULT "password"',
|
|
);
|
|
addColumnIfNotExists("ssh_data", "password", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "key", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "key_password", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "key_type", "TEXT");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"enable_terminal",
|
|
"INTEGER NOT NULL DEFAULT 1",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"enable_tunnel",
|
|
"INTEGER NOT NULL DEFAULT 1",
|
|
);
|
|
addColumnIfNotExists("ssh_data", "tunnel_connections", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "jump_hosts", "TEXT");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"enable_file_manager",
|
|
"INTEGER NOT NULL DEFAULT 1",
|
|
);
|
|
addColumnIfNotExists("ssh_data", "default_path", "TEXT");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"created_at",
|
|
"TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"updated_at",
|
|
"TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
);
|
|
addColumnIfNotExists("ssh_data", "force_keyboard_interactive", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"credential_id",
|
|
"INTEGER REFERENCES ssh_credentials(id) ON DELETE SET NULL",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"override_credential_username",
|
|
"INTEGER",
|
|
);
|
|
|
|
addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "stats_config", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "terminal_config", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "quick_actions", "TEXT");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"enable_docker",
|
|
"INTEGER NOT NULL DEFAULT 0",
|
|
);
|
|
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
|
|
|
|
addColumnIfNotExists("ssh_data", "notes", "TEXT");
|
|
|
|
addColumnIfNotExists("ssh_data", "use_socks5", "INTEGER");
|
|
addColumnIfNotExists("ssh_data", "socks5_host", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "socks5_port", "INTEGER");
|
|
addColumnIfNotExists("ssh_data", "socks5_username", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "socks5_password", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "socks5_proxy_chain", "TEXT");
|
|
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"show_terminal_in_sidebar",
|
|
"INTEGER NOT NULL DEFAULT 1",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"show_file_manager_in_sidebar",
|
|
"INTEGER NOT NULL DEFAULT 0",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"show_tunnel_in_sidebar",
|
|
"INTEGER NOT NULL DEFAULT 0",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"show_docker_in_sidebar",
|
|
"INTEGER NOT NULL DEFAULT 0",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"show_server_stats_in_sidebar",
|
|
"INTEGER NOT NULL DEFAULT 0",
|
|
);
|
|
|
|
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
|
|
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
|
|
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
|
|
|
|
addColumnIfNotExists("ssh_credentials", "system_password", "TEXT");
|
|
addColumnIfNotExists("ssh_credentials", "system_key", "TEXT");
|
|
addColumnIfNotExists("ssh_credentials", "system_key_password", "TEXT");
|
|
|
|
addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL");
|
|
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
|
|
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
|
|
|
|
addColumnIfNotExists("snippets", "folder", "TEXT");
|
|
addColumnIfNotExists("snippets", "order", "INTEGER NOT NULL DEFAULT 0");
|
|
|
|
try {
|
|
sqlite
|
|
.prepare("SELECT id FROM snippet_folders LIMIT 1")
|
|
.get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS snippet_folders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
color TEXT,
|
|
icon TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create snippet_folders table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite
|
|
.prepare("SELECT id FROM sessions LIMIT 1")
|
|
.get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
jwt_token TEXT NOT NULL,
|
|
device_type TEXT NOT NULL,
|
|
device_info TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TEXT NOT NULL,
|
|
last_active_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create sessions table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite
|
|
.prepare("SELECT id FROM network_topology LIMIT 1")
|
|
.get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS network_topology (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
topology TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create network_topology table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite
|
|
.prepare("SELECT id FROM dashboard_preferences LIMIT 1")
|
|
.get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS dashboard_preferences (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL UNIQUE,
|
|
layout TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create dashboard_preferences table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite.prepare("SELECT id FROM host_access LIMIT 1").get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS host_access (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
host_id INTEGER NOT NULL,
|
|
user_id TEXT,
|
|
role_id INTEGER,
|
|
granted_by TEXT NOT NULL,
|
|
permission_level TEXT NOT NULL DEFAULT 'use',
|
|
expires_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
last_accessed_at TEXT,
|
|
access_count INTEGER NOT NULL DEFAULT 0,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create host_access table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite.prepare("SELECT role_id FROM host_access LIMIT 1").get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec("ALTER TABLE host_access ADD COLUMN role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE");
|
|
} catch (alterError) {
|
|
databaseLogger.warn("Failed to add role_id column", {
|
|
operation: "schema_migration",
|
|
error: alterError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite.prepare("SELECT sudo_password FROM ssh_data LIMIT 1").get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec("ALTER TABLE ssh_data ADD COLUMN sudo_password TEXT");
|
|
} catch (alterError) {
|
|
databaseLogger.warn("Failed to add sudo_password column", {
|
|
operation: "schema_migration",
|
|
error: alterError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite.prepare("SELECT id FROM roles LIMIT 1").get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS roles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
display_name TEXT NOT NULL,
|
|
description TEXT,
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
permissions TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create roles table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite.prepare("SELECT id FROM user_roles LIMIT 1").get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS user_roles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
role_id INTEGER NOT NULL,
|
|
granted_by TEXT,
|
|
granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, role_id),
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE SET NULL
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create user_roles table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite.prepare("SELECT id FROM audit_logs LIMIT 1").get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
resource_type TEXT NOT NULL,
|
|
resource_id TEXT,
|
|
resource_name TEXT,
|
|
details TEXT,
|
|
ip_address TEXT,
|
|
user_agent TEXT,
|
|
success INTEGER NOT NULL,
|
|
error_message TEXT,
|
|
timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create audit_logs table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite.prepare("SELECT id FROM session_recordings LIMIT 1").get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS session_recordings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
host_id INTEGER NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
access_id INTEGER,
|
|
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
ended_at TEXT,
|
|
duration INTEGER,
|
|
commands TEXT,
|
|
dangerous_actions TEXT,
|
|
recording_path TEXT,
|
|
terminated_by_owner INTEGER DEFAULT 0,
|
|
termination_reason TEXT,
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create session_recordings table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
sqlite.prepare("SELECT id FROM shared_credentials LIMIT 1").get();
|
|
} catch {
|
|
try {
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS shared_credentials (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
host_access_id INTEGER NOT NULL,
|
|
original_credential_id INTEGER NOT NULL,
|
|
target_user_id TEXT NOT NULL,
|
|
encrypted_username TEXT NOT NULL,
|
|
encrypted_auth_type TEXT NOT NULL,
|
|
encrypted_password TEXT,
|
|
encrypted_key TEXT,
|
|
encrypted_key_password TEXT,
|
|
encrypted_key_type TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
needs_re_encryption INTEGER NOT NULL DEFAULT 0,
|
|
FOREIGN KEY (host_access_id) REFERENCES host_access (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (original_credential_id) REFERENCES ssh_credentials (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
} catch (createError) {
|
|
databaseLogger.warn("Failed to create shared_credentials table", {
|
|
operation: "schema_migration",
|
|
error: createError,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>;
|
|
|
|
try {
|
|
const validSystemRoles = ['admin', 'user'];
|
|
const unwantedRoleNames = ['superAdmin', 'powerUser', 'readonly', 'member'];
|
|
let deletedCount = 0;
|
|
|
|
const deleteByName = sqlite.prepare("DELETE FROM roles WHERE name = ?");
|
|
for (const roleName of unwantedRoleNames) {
|
|
const result = deleteByName.run(roleName);
|
|
if (result.changes > 0) {
|
|
deletedCount += result.changes;
|
|
}
|
|
}
|
|
|
|
const deleteOldSystemRole = sqlite.prepare("DELETE FROM roles WHERE name = ? AND is_system = 1");
|
|
for (const role of existingRoles) {
|
|
if (role.is_system === 1 && !validSystemRoles.includes(role.name) && !unwantedRoleNames.includes(role.name)) {
|
|
const result = deleteOldSystemRole.run(role.name);
|
|
if (result.changes > 0) {
|
|
deletedCount += result.changes;
|
|
}
|
|
}
|
|
}
|
|
} catch (cleanupError) {
|
|
databaseLogger.warn("Failed to clean up old system roles", {
|
|
operation: "schema_migration",
|
|
error: cleanupError,
|
|
});
|
|
}
|
|
|
|
const systemRoles = [
|
|
{
|
|
name: "admin",
|
|
displayName: "rbac.roles.admin",
|
|
description: "Administrator with full access",
|
|
permissions: null,
|
|
},
|
|
{
|
|
name: "user",
|
|
displayName: "rbac.roles.user",
|
|
description: "Regular user",
|
|
permissions: null,
|
|
},
|
|
];
|
|
|
|
for (const role of systemRoles) {
|
|
const existingRole = sqlite.prepare("SELECT id FROM roles WHERE name = ?").get(role.name);
|
|
if (!existingRole) {
|
|
try {
|
|
sqlite.prepare(`
|
|
INSERT INTO roles (name, display_name, description, is_system, permissions)
|
|
VALUES (?, ?, ?, 1, ?)
|
|
`).run(role.name, role.displayName, role.description, role.permissions);
|
|
} catch (insertError) {
|
|
databaseLogger.warn(`Failed to create system role: ${role.name}`, {
|
|
operation: "schema_migration",
|
|
error: insertError,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
const adminUsers = sqlite.prepare("SELECT id FROM users WHERE is_admin = 1").all() as { id: string }[];
|
|
const normalUsers = sqlite.prepare("SELECT id FROM users WHERE is_admin = 0").all() as { id: string }[];
|
|
|
|
const adminRole = sqlite.prepare("SELECT id FROM roles WHERE name = 'admin'").get() as { id: number } | undefined;
|
|
const userRole = sqlite.prepare("SELECT id FROM roles WHERE name = 'user'").get() as { id: number } | undefined;
|
|
|
|
if (adminRole) {
|
|
const insertUserRole = sqlite.prepare(`
|
|
INSERT OR IGNORE INTO user_roles (user_id, role_id, granted_at)
|
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
`);
|
|
|
|
for (const admin of adminUsers) {
|
|
try {
|
|
insertUserRole.run(admin.id, adminRole.id);
|
|
} catch (error) {
|
|
// Ignore duplicate errors
|
|
}
|
|
}
|
|
}
|
|
|
|
if (userRole) {
|
|
const insertUserRole = sqlite.prepare(`
|
|
INSERT OR IGNORE INTO user_roles (user_id, role_id, granted_at)
|
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
`);
|
|
|
|
for (const user of normalUsers) {
|
|
try {
|
|
insertUserRole.run(user.id, userRole.id);
|
|
} catch (error) {
|
|
// Ignore duplicate errors
|
|
}
|
|
}
|
|
}
|
|
} catch (migrationError) {
|
|
databaseLogger.warn("Failed to migrate existing users to roles", {
|
|
operation: "schema_migration",
|
|
error: migrationError,
|
|
});
|
|
}
|
|
} catch (seedError) {
|
|
databaseLogger.warn("Failed to seed system roles", {
|
|
operation: "schema_migration",
|
|
error: seedError,
|
|
});
|
|
}
|
|
|
|
databaseLogger.success("Schema migration completed", {
|
|
operation: "schema_migration",
|
|
});
|
|
};
|
|
|
|
async function saveMemoryDatabaseToFile() {
|
|
if (!memoryDatabase) return;
|
|
|
|
try {
|
|
const buffer = memoryDatabase.serialize();
|
|
|
|
if (!fs.existsSync(dataDir)) {
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
}
|
|
|
|
try {
|
|
const sessionCount = memoryDatabase
|
|
.prepare("SELECT COUNT(*) as count FROM sessions")
|
|
.get() as { count: number };
|
|
} catch (countError) {
|
|
}
|
|
|
|
if (enableFileEncryption) {
|
|
await DatabaseFileEncryption.encryptDatabaseFromBuffer(
|
|
buffer,
|
|
encryptedDbPath,
|
|
);
|
|
} else {
|
|
fs.writeFileSync(dbPath, buffer);
|
|
}
|
|
} catch (error) {
|
|
databaseLogger.error("Failed to save in-memory database", error, {
|
|
operation: "memory_db_save_failed",
|
|
enableFileEncryption,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function handlePostInitFileEncryption() {
|
|
if (!enableFileEncryption) return;
|
|
|
|
try {
|
|
if (memoryDatabase) {
|
|
await saveMemoryDatabaseToFile();
|
|
|
|
setInterval(saveMemoryDatabaseToFile, 15 * 1000);
|
|
|
|
DatabaseSaveTrigger.initialize(saveMemoryDatabaseToFile);
|
|
}
|
|
|
|
try {
|
|
const migration = new DatabaseMigration(dataDir);
|
|
migration.cleanupOldBackups();
|
|
} catch (cleanupError) {
|
|
databaseLogger.warn("Failed to cleanup old migration files", {
|
|
operation: "migration_cleanup_startup_failed",
|
|
error:
|
|
cleanupError instanceof Error
|
|
? cleanupError.message
|
|
: "Unknown error",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
databaseLogger.error(
|
|
"Failed to handle database file encryption setup",
|
|
error,
|
|
{
|
|
operation: "db_encrypt_setup_failed",
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
async function initializeDatabase(): Promise<void> {
|
|
await initializeCompleteDatabase();
|
|
await handlePostInitFileEncryption();
|
|
}
|
|
|
|
export { initializeDatabase };
|
|
|
|
async function cleanupDatabase() {
|
|
if (memoryDatabase) {
|
|
try {
|
|
await saveMemoryDatabaseToFile();
|
|
} catch (error) {
|
|
databaseLogger.error(
|
|
"Failed to save in-memory database before shutdown",
|
|
error,
|
|
{
|
|
operation: "shutdown_save_failed",
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (sqlite) {
|
|
sqlite.close();
|
|
}
|
|
} catch (error) {
|
|
databaseLogger.warn("Error closing database connection", {
|
|
operation: "db_close_error",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
|
|
try {
|
|
const tempDir = path.join(dataDir, ".temp");
|
|
if (fs.existsSync(tempDir)) {
|
|
const files = fs.readdirSync(tempDir);
|
|
for (const file of files) {
|
|
try {
|
|
fs.unlinkSync(path.join(tempDir, file));
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
try {
|
|
fs.rmdirSync(tempDir);
|
|
} catch {
|
|
}
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
process.on("exit", () => {
|
|
if (sqlite) {
|
|
try {
|
|
sqlite.close();
|
|
} catch {
|
|
}
|
|
}
|
|
});
|
|
|
|
process.on("SIGINT", async () => {
|
|
databaseLogger.info("Received SIGINT, cleaning up...", {
|
|
operation: "shutdown",
|
|
});
|
|
await cleanupDatabase();
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on("SIGTERM", async () => {
|
|
databaseLogger.info("Received SIGTERM, cleaning up...", {
|
|
operation: "shutdown",
|
|
});
|
|
await cleanupDatabase();
|
|
process.exit(0);
|
|
});
|
|
|
|
let db: ReturnType<typeof drizzle<typeof schema>>;
|
|
|
|
export function getDb(): ReturnType<typeof drizzle<typeof schema>> {
|
|
if (!db) {
|
|
throw new Error(
|
|
"Database not initialized. Ensure initializeDatabase() is called before accessing db.",
|
|
);
|
|
}
|
|
return db;
|
|
}
|
|
|
|
export function getSqlite(): Database.Database {
|
|
if (!sqlite) {
|
|
throw new Error(
|
|
"SQLite not initialized. Ensure initializeDatabase() is called before accessing sqlite.",
|
|
);
|
|
}
|
|
return sqlite;
|
|
}
|
|
|
|
export { db };
|
|
export { DatabaseFileEncryption };
|
|
export const databasePaths = {
|
|
main: actualDbPath,
|
|
encrypted: encryptedDbPath,
|
|
directory: dbDir,
|
|
inMemory: true,
|
|
};
|
|
|
|
export { saveMemoryDatabaseToFile };
|
|
|
|
export { DatabaseSaveTrigger };
|