v1.11.1 (#566)
* Temporary merge for 1.11.1 syncing (#543) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * 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 * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * 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 * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * 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 confli… * feat: add configurable base path for reverse proxy subpath deployment (#542) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * 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 * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * 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 * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * 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) … * fix: sort file manager entries alphabetically with directories first (#541) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * 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 * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * 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 * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * 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) (… * fix: add timeout for SFTP readlink to prevent request hang (#540) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * 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 * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * 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 * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * 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) * f… * feat: add copy deploy command for SSH keys without password auth (#538) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * 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 * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * 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 * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * 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: preserve all ssh_data fields in database export/import (#537) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * 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 * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * 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 * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs <s.josefs@gmx.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.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> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Gaylord Julien <g.j@mailbox.org> Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * 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: change minmax lower bound to 540px from 600px for gridTemplateColumns (#532) * feat: update/standardize the icon * feat: update mobile terminal ui to support all new features * fix: increment ver * feat: begin opkssh implementation by adding auth type * feat: finalize opkssh based auth * fix: update docker/file manager to reflect need for Terminal * fix: opkssh issues * fix: ssh.ts build error * feat: improve logging verbosity (is that a word?) * fix: remove debug logging * feat: add host key verification * feat: consolidate host manager tabs * feat: allow no password in a credential and fix opkssh issues * Remove macOS-specific keyboard handling (#544) * fix: translation with region (#535) * fix: translation with region * fix: update `LanguageSwitcher.tsx` and `i18n.ts` * chore: add temp locales to fix build error * feat: add opkssh docs button adn update default config * feat: improve opkssh reliability and docker support * feat: add remote redirect uri support * feat: continued opkssh docker support * feat: continued opkssh docker support * feat: continued opkssh docker support * feat: continued opkssh docker support * feat: continued opkssh docker support * feat: continued opkssh docker support * fix: disable browser caching for file manager API responses (#549) The file manager API endpoint (/ssh/file_manager/ssh/) did not set any Cache-Control headers. Browsers applied heuristic caching to GET requests (e.g. listFiles), causing stale file listings to be returned after creating, deleting, or renaming files. Users had to clear cookies and re-login to see updated file lists. Add Cache-Control: no-store, no-cache, must-revalidate to both nginx.conf and nginx-https.conf for the file manager API location to ensure browsers always fetch fresh data from the server. Fixes: Termix-SSH/Support#455 * fix: prevent data loss from encrypted database format mismatch (#548) * fix: isEncryptedDatabaseFile() fallback to single-file detection when .meta is invalid Previously, if a stale or corrupted .meta file existed alongside a valid single-file format encrypted database, isEncryptedDatabaseFile() would return false without attempting single-file format detection. This could cause the initialization flow to treat a valid encrypted database as non-existent, leading to creation of an empty database that would then overwrite the original encrypted data on the next periodic save cycle — resulting in permanent data loss. Now the method falls through to single-file format detection when .meta validation fails, matching the behavior of decryptDatabaseToBuffer(). Refs: Termix-SSH/Support#452 * fix: decryptDatabaseFile() support single-file encrypted format decryptDatabaseFile() previously only supported the two-file format (.encrypted + .meta), but encryptDatabaseFromBuffer() (used by the periodic 15-second save) writes single-file format and deletes .meta. This meant /database/restore via restoreFromEncryptedBackup() would fail on any database that had been saved by the periodic save cycle. Refactored to delegate to decryptDatabaseToBuffer() which already handles both formats correctly, eliminating the duplicated decryption logic and the format mismatch. Refs: Termix-SSH/Support#452 * fix: force save after database import to prevent data loss The /database/import endpoint wrote imported data to the in-memory SQLite database but did not trigger an immediate persist to the encrypted file on disk. Data persistence relied solely on the 15-second periodic save interval. If the container crashed or was restarted within that window, all imported data would be lost. This was the likely cause of users reporting that imported data "flashed briefly then disappeared." Now calls DatabaseSaveTrigger.forceSave() immediately after a successful import to ensure data is written to disk before returning the response to the client. Refs: Termix-SSH/Support#452 * fix: getEncryptedFileInfo() support single-file encrypted format getEncryptedFileInfo() only read metadata from the separate .meta file, but the periodic save (encryptDatabaseFromBuffer) writes single-file format and deletes .meta files. This caused the method to always return null for databases saved by the periodic cycle, breaking diagnostic APIs and status checks. Now tries .meta first, then falls back to reading embedded metadata from the single-file format header, consistent with the detection logic in isEncryptedDatabaseFile(). Refs: Termix-SSH/Support#452 * feat: file manager disconnecting and added toggle for snippet confirmation * feat: puid/pgid support * fix: opkssh timesouts and dockerfile error * fix: build error * fix: opkssh auth not loading properly * fix: show opkssh auth failed logs if failed * chore: clean up files * fix: accidental line paste * fix: remove translatiosn for merge * New Crowdin updates (#551) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * 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 (Bulgarian) * 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 (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Bengali) * New translations en.json (Thai) * New translations en.json (Hindi) * Delete src/locales/translated directory * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * 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 (Bulgarian) * 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 (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Bengali) * New translations en.json (Thai) * New translations en.json (Hindi) * fix: updated i18n config to support new file naming scheme for crowdin * fix: host/credential enter key not working and saving overriden username * feat: update readme * fix: credential in server stats error and browser refresh and client terminal size issue * feat: add HTTP proxy support for GitHub API requests (#554) * feat: add HTTP proxy support for GitHub API requests * Remove comments from getProxyAgent function Removed documentation comments for the getProxyAgent function. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: desktop app random logouts * fix: constant logouts * fix: constant logouts * fix: unlinked credentials after ujpdating --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> 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: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> Co-authored-by: Deepansh Khurana <deepanshkhurana@outlook.com> Co-authored-by: Dylan Ysmal <Xenthys@users.noreply.github.com> Co-authored-by: TomyJan <TomyJan6@gmail.com> Co-authored-by: ZacharyZcR <PayasoNorahC@protonmail.com>
@@ -58,7 +58,7 @@ free and self-hosted alternative to Termius available for all platforms.
|
|||||||
- **Command History** - Auto-complete and view previously ran SSH commands
|
- **Command History** - Auto-complete and view previously ran SSH commands
|
||||||
- **Quick Connect** - Connect to a server without having to save the connection data
|
- **Quick Connect** - Connect to a server without having to save the connection data
|
||||||
- **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard
|
- **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard
|
||||||
- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, password autofill, etc.
|
- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
|
||||||
- **Network Graph** - Customize your Dashboard to visualize your homelab based off your SSH connections with status support
|
- **Network Graph** - Customize your Dashboard to visualize your homelab based off your SSH connections with status support
|
||||||
|
|
||||||
# Planned Features
|
# Planned Features
|
||||||
@@ -136,7 +136,7 @@ channel, however, response times may be longer.
|
|||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=j1_I1mkhUkE)
|
[](https://youtu.be/8OYCPwS7ciA)
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
|
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
|
||||||
|
|||||||
@@ -51,11 +51,12 @@ ENV DATA_DIR=/app/data \
|
|||||||
PORT=8080 \
|
PORT=8080 \
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y nginx gettext-base openssl && \
|
RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu && \
|
||||||
|
update-ca-certificates && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
mkdir -p /app/data /app/uploads /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \
|
mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \
|
||||||
chown -R node:node /app && \
|
chown -R node:node /app && \
|
||||||
chmod 755 /app/data /app/uploads /app/nginx && \
|
chmod 755 /app/data /app/uploads /app/data/.opk /app/nginx && \
|
||||||
touch /app/nginx/nginx.conf && \
|
touch /app/nginx/nginx.conf && \
|
||||||
chown node:node /app/nginx/nginx.conf
|
chown node:node /app/nginx/nginx.conf
|
||||||
|
|
||||||
@@ -80,6 +81,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
USER node
|
|
||||||
|
|
||||||
CMD ["/entrypoint.sh"]
|
CMD ["/entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
PUID=${PUID:-1000}
|
||||||
|
PGID=${PGID:-1000}
|
||||||
|
|
||||||
|
if [ "$(id -u)" = "0" ]; then
|
||||||
|
echo "Setting up user permissions (PUID: $PUID, PGID: $PGID)..."
|
||||||
|
|
||||||
|
groupmod -o -g "$PGID" node 2>/dev/null || true
|
||||||
|
usermod -o -u "$PUID" node 2>/dev/null || true
|
||||||
|
|
||||||
|
chown -R node:node /app/data /app/uploads /app/nginx 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "User node is now UID: $PUID, GID: $PGID"
|
||||||
|
|
||||||
|
exec gosu node:node "$0" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
export PORT=${PORT:-8080}
|
export PORT=${PORT:-8080}
|
||||||
export ENABLE_SSL=${ENABLE_SSL:-false}
|
export ENABLE_SSL=${ENABLE_SSL:-false}
|
||||||
export SSL_PORT=${SSL_PORT:-8443}
|
export SSL_PORT=${SSL_PORT:-8443}
|
||||||
@@ -19,8 +35,26 @@ fi
|
|||||||
|
|
||||||
envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /app/nginx/nginx.conf
|
envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /app/nginx/nginx.conf
|
||||||
|
|
||||||
mkdir -p /app/data /app/uploads
|
mkdir -p /app/data /app/uploads /app/data/.opk
|
||||||
chmod 755 /app/data /app/uploads 2>/dev/null || true
|
chmod 755 /app/data /app/uploads /app/data/.opk 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ -w /app/data ]; then
|
||||||
|
echo "Data directory is writable"
|
||||||
|
else
|
||||||
|
echo "WARNING: Data directory is not writable. OPKSSH may fail."
|
||||||
|
ls -ld /app/data
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -w /app/data/.opk ]; then
|
||||||
|
echo "OPKSSH directory is writable"
|
||||||
|
else
|
||||||
|
echo "WARNING: OPKSSH directory is not writable. OPKSSH authentication will fail."
|
||||||
|
ls -ld /app/data/.opk
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "/app/opkssh" ]; then
|
||||||
|
echo "WARNING: OPKSSH binary directory not found at /app/opkssh"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$ENABLE_SSL" = "true" ]; then
|
if [ "$ENABLE_SSL" = "true" ]; then
|
||||||
echo "Checking SSL certificate configuration..."
|
echo "Checking SSL certificate configuration..."
|
||||||
|
|||||||
@@ -213,6 +213,28 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/ssh/opkssh-chooser(/.*)?$ {
|
||||||
|
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/ssh/opkssh-callback(/.*)?$ {
|
||||||
|
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location /ssh/ {
|
location /ssh/ {
|
||||||
proxy_pass http://127.0.0.1:30001;
|
proxy_pass http://127.0.0.1:30001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -228,6 +250,10 @@ http {
|
|||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
@@ -294,6 +320,8 @@ http {
|
|||||||
client_max_body_size 5G;
|
client_max_body_size 5G;
|
||||||
client_body_timeout 300s;
|
client_body_timeout 300s;
|
||||||
|
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:30004;
|
proxy_pass http://127.0.0.1:30004;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -202,6 +202,28 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/ssh/opkssh-chooser(/.*)?$ {
|
||||||
|
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/ssh/opkssh-callback(/.*)?$ {
|
||||||
|
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location /ssh/ {
|
location /ssh/ {
|
||||||
proxy_pass http://127.0.0.1:30001;
|
proxy_pass http://127.0.0.1:30001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -217,7 +239,10 @@ http {
|
|||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -283,6 +308,8 @@ http {
|
|||||||
client_max_body_size 5G;
|
client_max_body_size 5G;
|
||||||
client_body_timeout 300s;
|
client_body_timeout 300s;
|
||||||
|
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:30004;
|
proxy_pass http://127.0.0.1:30004;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<!-- PWA Meta Tags -->
|
|
||||||
<meta name="theme-color" content="#09090b" />
|
<meta name="theme-color" content="#09090b" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta
|
<meta
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "1.11.0",
|
"version": "1.11.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "1.11.0",
|
"version": "1.11.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.18.7",
|
"@codemirror/autocomplete": "^6.18.7",
|
||||||
"@codemirror/commands": "^6.3.3",
|
"@codemirror/commands": "^6.3.3",
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"drizzle-orm": "^0.44.3",
|
"drizzle-orm": "^0.44.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
"i18n-auto-translation": "^2.2.3",
|
"i18n-auto-translation": "^2.2.3",
|
||||||
"i18next": "^25.4.2",
|
"i18next": "^25.4.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
@@ -7885,6 +7886,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.11.0",
|
"version": "1.11.1",
|
||||||
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
||||||
"author": "Karmaa",
|
"author": "Karmaa",
|
||||||
"main": "electron/main.cjs",
|
"main": "electron/main.cjs",
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
"build": "vite build && tsc -p tsconfig.node.json",
|
"build": "vite build && tsc -p tsconfig.node.json",
|
||||||
"build:backend": "tsc -p tsconfig.node.json",
|
"build:backend": "tsc -p tsconfig.node.json",
|
||||||
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
|
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
|
||||||
|
"dev:docker": "docker stop termix-dev 2>nul & docker rm termix-dev 2>nul & docker build -f docker/Dockerfile -t termix:dev --no-cache . && docker run -d --name termix-dev -p 3000:3000 -p 8080:8080 -p 30001-30006:30001-30006 -v \"%cd%\\db\\data:/app/data\" termix:dev",
|
||||||
|
"dev:docker:restart": "docker stop termix-dev 2>nul & docker rm termix-dev 2>nul & docker run -d --name termix-dev -p 8080:8080 -p 30001-30006:30001-30006 -v \"%cd%\\db\\data:/app/data\" termix:dev",
|
||||||
"generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js",
|
"generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"",
|
"electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"",
|
||||||
@@ -82,6 +84,7 @@
|
|||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"drizzle-orm": "^0.44.3",
|
"drizzle-orm": "^0.44.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
"i18n-auto-translation": "^2.2.3",
|
"i18n-auto-translation": "^2.2.3",
|
||||||
"i18next": "^25.4.2",
|
"i18next": "^25.4.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 447 B |
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 671 B |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
@@ -6,31 +6,31 @@
|
|||||||
"background_color": "#09090b",
|
"background_color": "#09090b",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"scope": "/",
|
"scope": "./",
|
||||||
"start_url": "/",
|
"start_url": "./",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icons/48x48.png",
|
"src": "./icons/48x48.png",
|
||||||
"sizes": "48x48",
|
"sizes": "48x48",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/64x64.png",
|
"src": "./icons/64x64.png",
|
||||||
"sizes": "64x64",
|
"sizes": "64x64",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/128x128.png",
|
"src": "./icons/128x128.png",
|
||||||
"sizes": "128x128",
|
"sizes": "128x128",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/256x256.png",
|
"src": "./icons/256x256.png",
|
||||||
"sizes": "256x256",
|
"sizes": "256x256",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/512x512.png",
|
"src": "./icons/512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 149 KiB |
@@ -27,12 +27,7 @@ app.use(
|
|||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
if (!origin) return callback(null, true);
|
if (!origin) return callback(null, true);
|
||||||
|
|
||||||
const allowedOrigins = [
|
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
||||||
"http://localhost:5173",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:5173",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (allowedOrigins.includes(origin)) {
|
if (allowedOrigins.includes(origin)) {
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
@@ -90,6 +85,9 @@ app.use(authManager.createAuthMiddleware());
|
|||||||
*/
|
*/
|
||||||
app.get("/uptime", async (req, res) => {
|
app.get("/uptime", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
const uptimeMs = Date.now() - serverStartTime;
|
const uptimeMs = Date.now() - serverStartTime;
|
||||||
const uptimeSeconds = Math.floor(uptimeMs / 1000);
|
const uptimeSeconds = Math.floor(uptimeMs / 1000);
|
||||||
const days = Math.floor(uptimeSeconds / 86400);
|
const days = Math.floor(uptimeSeconds / 86400);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { UserDataExport } from "../utils/user-data-export.js";
|
|||||||
import { AutoSSLSetup } from "../utils/auto-ssl-setup.js";
|
import { AutoSSLSetup } from "../utils/auto-ssl-setup.js";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { parseUserAgent } from "../utils/user-agent-parser.js";
|
import { parseUserAgent } from "../utils/user-agent-parser.js";
|
||||||
|
import { getProxyAgent } from "../utils/proxy-agent.js";
|
||||||
import {
|
import {
|
||||||
users,
|
users,
|
||||||
sshData,
|
sshData,
|
||||||
@@ -42,7 +43,7 @@ import type {
|
|||||||
GitHubAPIResponse,
|
GitHubAPIResponse,
|
||||||
AuthenticatedRequest,
|
AuthenticatedRequest,
|
||||||
} from "../../types/index.js";
|
} from "../../types/index.js";
|
||||||
import { getDb } from "./db/index.js";
|
import { getDb, DatabaseSaveTrigger } from "./db/index.js";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -57,12 +58,7 @@ app.use(
|
|||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
if (!origin) return callback(null, true);
|
if (!origin) return callback(null, true);
|
||||||
|
|
||||||
const allowedOrigins = [
|
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
||||||
"http://localhost:5173",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:5173",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (allowedOrigins.includes(origin)) {
|
if (allowedOrigins.includes(origin)) {
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
@@ -166,12 +162,14 @@ async function fetchGitHubAPI<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
|
const url = `${GITHUB_API_BASE}${endpoint}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
"User-Agent": "TermixUpdateChecker/1.0",
|
"User-Agent": "TermixUpdateChecker/1.0",
|
||||||
"X-GitHub-Api-Version": "2022-11-28",
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
},
|
},
|
||||||
|
agent: getProxyAgent(url),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -696,19 +694,39 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
|||||||
tags TEXT,
|
tags TEXT,
|
||||||
pin INTEGER NOT NULL DEFAULT 0,
|
pin INTEGER NOT NULL DEFAULT 0,
|
||||||
auth_type TEXT NOT NULL,
|
auth_type TEXT NOT NULL,
|
||||||
|
force_keyboard_interactive TEXT,
|
||||||
password TEXT,
|
password TEXT,
|
||||||
key TEXT,
|
key TEXT,
|
||||||
key_password TEXT,
|
key_password TEXT,
|
||||||
key_type TEXT,
|
key_type TEXT,
|
||||||
|
sudo_password TEXT,
|
||||||
autostart_password TEXT,
|
autostart_password TEXT,
|
||||||
autostart_key TEXT,
|
autostart_key TEXT,
|
||||||
autostart_key_password TEXT,
|
autostart_key_password TEXT,
|
||||||
credential_id INTEGER,
|
credential_id INTEGER,
|
||||||
|
override_credential_username INTEGER,
|
||||||
enable_terminal INTEGER NOT NULL DEFAULT 1,
|
enable_terminal INTEGER NOT NULL DEFAULT 1,
|
||||||
enable_tunnel INTEGER NOT NULL DEFAULT 1,
|
enable_tunnel INTEGER NOT NULL DEFAULT 1,
|
||||||
tunnel_connections TEXT,
|
tunnel_connections TEXT,
|
||||||
|
jump_hosts TEXT,
|
||||||
enable_file_manager INTEGER NOT NULL DEFAULT 1,
|
enable_file_manager INTEGER NOT NULL DEFAULT 1,
|
||||||
|
enable_docker INTEGER NOT NULL DEFAULT 0,
|
||||||
|
show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1,
|
||||||
|
show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0,
|
||||||
|
show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0,
|
||||||
|
show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0,
|
||||||
|
show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0,
|
||||||
default_path TEXT,
|
default_path TEXT,
|
||||||
|
stats_config TEXT,
|
||||||
|
terminal_config TEXT,
|
||||||
|
quick_actions TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
use_socks5 INTEGER,
|
||||||
|
socks5_host TEXT,
|
||||||
|
socks5_port INTEGER,
|
||||||
|
socks5_username TEXT,
|
||||||
|
socks5_password TEXT,
|
||||||
|
socks5_proxy_chain TEXT,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -808,8 +826,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
|||||||
.from(sshData)
|
.from(sshData)
|
||||||
.where(eq(sshData.userId, userId));
|
.where(eq(sshData.userId, userId));
|
||||||
const insertHost = exportDb.prepare(`
|
const insertHost = exportDb.prepare(`
|
||||||
INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, password, key, key_password, key_type, autostart_password, autostart_key, autostart_key_password, credential_id, enable_terminal, enable_tunnel, tunnel_connections, enable_file_manager, default_path, created_at, updated_at)
|
INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
for (const host of sshHosts) {
|
for (const host of sshHosts) {
|
||||||
@@ -830,19 +848,39 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
|||||||
decrypted.tags || null,
|
decrypted.tags || null,
|
||||||
decrypted.pin ? 1 : 0,
|
decrypted.pin ? 1 : 0,
|
||||||
decrypted.authType,
|
decrypted.authType,
|
||||||
|
decrypted.forceKeyboardInteractive || null,
|
||||||
decrypted.password || null,
|
decrypted.password || null,
|
||||||
decrypted.key || null,
|
decrypted.key || null,
|
||||||
decrypted.key_password || null,
|
decrypted.key_password || null,
|
||||||
decrypted.keyType || null,
|
decrypted.keyType || null,
|
||||||
|
decrypted.sudoPassword || null,
|
||||||
decrypted.autostartPassword || null,
|
decrypted.autostartPassword || null,
|
||||||
decrypted.autostartKey || null,
|
decrypted.autostartKey || null,
|
||||||
decrypted.autostartKeyPassword || null,
|
decrypted.autostartKeyPassword || null,
|
||||||
decrypted.credentialId || null,
|
decrypted.credentialId || null,
|
||||||
decrypted.enableTerminal ? 1 : 0,
|
Boolean(decrypted.overrideCredentialUsername) ? 1 : 0,
|
||||||
decrypted.enableTunnel ? 1 : 0,
|
Boolean(decrypted.enableTerminal) ? 1 : 0,
|
||||||
|
Boolean(decrypted.enableTunnel) ? 1 : 0,
|
||||||
decrypted.tunnelConnections || null,
|
decrypted.tunnelConnections || null,
|
||||||
decrypted.enableFileManager ? 1 : 0,
|
decrypted.jumpHosts || null,
|
||||||
|
Boolean(decrypted.enableFileManager) ? 1 : 0,
|
||||||
|
Boolean(decrypted.enableDocker) ? 1 : 0,
|
||||||
|
Boolean(decrypted.showTerminalInSidebar) ? 1 : 0,
|
||||||
|
Boolean(decrypted.showFileManagerInSidebar) ? 1 : 0,
|
||||||
|
Boolean(decrypted.showTunnelInSidebar) ? 1 : 0,
|
||||||
|
Boolean(decrypted.showDockerInSidebar) ? 1 : 0,
|
||||||
|
Boolean(decrypted.showServerStatsInSidebar) ? 1 : 0,
|
||||||
decrypted.defaultPath || null,
|
decrypted.defaultPath || null,
|
||||||
|
decrypted.statsConfig || null,
|
||||||
|
decrypted.terminalConfig || null,
|
||||||
|
decrypted.quickActions || null,
|
||||||
|
decrypted.notes || null,
|
||||||
|
Boolean(decrypted.useSocks5) ? 1 : 0,
|
||||||
|
decrypted.socks5Host || null,
|
||||||
|
decrypted.socks5Port || null,
|
||||||
|
decrypted.socks5Username || null,
|
||||||
|
decrypted.socks5Password || null,
|
||||||
|
decrypted.socks5ProxyChain || null,
|
||||||
decrypted.createdAt,
|
decrypted.createdAt,
|
||||||
decrypted.updatedAt,
|
decrypted.updatedAt,
|
||||||
);
|
);
|
||||||
@@ -1231,19 +1269,45 @@ app.post(
|
|||||||
tags: host.tags,
|
tags: host.tags,
|
||||||
pin: Boolean(host.pin),
|
pin: Boolean(host.pin),
|
||||||
authType: host.auth_type,
|
authType: host.auth_type,
|
||||||
|
forceKeyboardInteractive: host.force_keyboard_interactive,
|
||||||
password: host.password,
|
password: host.password,
|
||||||
key: host.key,
|
key: host.key,
|
||||||
keyPassword: host.key_password,
|
keyPassword: host.key_password,
|
||||||
keyType: host.key_type,
|
keyType: host.key_type,
|
||||||
|
sudoPassword: host.sudo_password,
|
||||||
autostartPassword: host.autostart_password,
|
autostartPassword: host.autostart_password,
|
||||||
autostartKey: host.autostart_key,
|
autostartKey: host.autostart_key,
|
||||||
autostartKeyPassword: host.autostart_key_password,
|
autostartKeyPassword: host.autostart_key_password,
|
||||||
credentialId: null,
|
credentialId: host.credential_id || null,
|
||||||
|
overrideCredentialUsername: Boolean(
|
||||||
|
host.override_credential_username,
|
||||||
|
),
|
||||||
enableTerminal: Boolean(host.enable_terminal),
|
enableTerminal: Boolean(host.enable_terminal),
|
||||||
enableTunnel: Boolean(host.enable_tunnel),
|
enableTunnel: Boolean(host.enable_tunnel),
|
||||||
tunnelConnections: host.tunnel_connections,
|
tunnelConnections: host.tunnel_connections,
|
||||||
|
jumpHosts: host.jump_hosts,
|
||||||
enableFileManager: Boolean(host.enable_file_manager),
|
enableFileManager: Boolean(host.enable_file_manager),
|
||||||
|
enableDocker: Boolean(host.enable_docker),
|
||||||
|
showTerminalInSidebar: Boolean(host.show_terminal_in_sidebar),
|
||||||
|
showFileManagerInSidebar: Boolean(
|
||||||
|
host.show_file_manager_in_sidebar,
|
||||||
|
),
|
||||||
|
showTunnelInSidebar: Boolean(host.show_tunnel_in_sidebar),
|
||||||
|
showDockerInSidebar: Boolean(host.show_docker_in_sidebar),
|
||||||
|
showServerStatsInSidebar: Boolean(
|
||||||
|
host.show_server_stats_in_sidebar,
|
||||||
|
),
|
||||||
defaultPath: host.default_path,
|
defaultPath: host.default_path,
|
||||||
|
statsConfig: host.stats_config,
|
||||||
|
terminalConfig: host.terminal_config,
|
||||||
|
quickActions: host.quick_actions,
|
||||||
|
notes: host.notes,
|
||||||
|
useSocks5: Boolean(host.use_socks5),
|
||||||
|
socks5Host: host.socks5_host,
|
||||||
|
socks5Port: host.socks5_port,
|
||||||
|
socks5Username: host.socks5_username,
|
||||||
|
socks5Password: host.socks5_password,
|
||||||
|
socks5ProxyChain: host.socks5_proxy_chain,
|
||||||
createdAt: host.created_at || new Date().toISOString(),
|
createdAt: host.created_at || new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -1483,6 +1547,19 @@ app.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.success = true;
|
result.success = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DatabaseSaveTrigger.forceSave("database_import");
|
||||||
|
} catch (saveError) {
|
||||||
|
apiLogger.error(
|
||||||
|
"Failed to persist imported data to disk",
|
||||||
|
saveError,
|
||||||
|
{
|
||||||
|
operation: "import_force_save_failed",
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (importDb) {
|
if (importDb) {
|
||||||
importDb.close();
|
importDb.close();
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ async function initializeCompleteDatabase(): Promise<void> {
|
|||||||
folder TEXT,
|
folder TEXT,
|
||||||
tags TEXT,
|
tags TEXT,
|
||||||
auth_type TEXT NOT NULL,
|
auth_type TEXT NOT NULL,
|
||||||
username TEXT NOT NULL,
|
username TEXT,
|
||||||
password TEXT,
|
password TEXT,
|
||||||
key TEXT,
|
key TEXT,
|
||||||
key_password TEXT,
|
key_password TEXT,
|
||||||
@@ -414,9 +414,11 @@ async function initializeCompleteDatabase(): Promise<void> {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sqlite.prepare("DELETE FROM sessions").run();
|
sqlite
|
||||||
|
.prepare("DELETE FROM sessions WHERE expires_at < datetime('now')")
|
||||||
|
.run();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
databaseLogger.warn("Could not clear sessions on startup", {
|
databaseLogger.warn("Could not clear expired sessions on startup", {
|
||||||
operation: "db_init_session_cleanup_failed",
|
operation: "db_init_session_cleanup_failed",
|
||||||
error: e,
|
error: e,
|
||||||
});
|
});
|
||||||
@@ -585,6 +587,13 @@ const migrateSchema = () => {
|
|||||||
addColumnIfNotExists("ssh_data", "socks5_password", "TEXT");
|
addColumnIfNotExists("ssh_data", "socks5_password", "TEXT");
|
||||||
addColumnIfNotExists("ssh_data", "socks5_proxy_chain", "TEXT");
|
addColumnIfNotExists("ssh_data", "socks5_proxy_chain", "TEXT");
|
||||||
|
|
||||||
|
addColumnIfNotExists("ssh_data", "host_key_fingerprint", "TEXT");
|
||||||
|
addColumnIfNotExists("ssh_data", "host_key_type", "TEXT");
|
||||||
|
addColumnIfNotExists("ssh_data", "host_key_algorithm", "TEXT DEFAULT 'sha256'");
|
||||||
|
addColumnIfNotExists("ssh_data", "host_key_first_seen", "TEXT");
|
||||||
|
addColumnIfNotExists("ssh_data", "host_key_last_verified", "TEXT");
|
||||||
|
addColumnIfNotExists("ssh_data", "host_key_changed_count", "INTEGER DEFAULT 0");
|
||||||
|
|
||||||
addColumnIfNotExists(
|
addColumnIfNotExists(
|
||||||
"ssh_data",
|
"ssh_data",
|
||||||
"show_terminal_in_sidebar",
|
"show_terminal_in_sidebar",
|
||||||
@@ -619,6 +628,68 @@ const migrateSchema = () => {
|
|||||||
addColumnIfNotExists("ssh_credentials", "system_key", "TEXT");
|
addColumnIfNotExists("ssh_credentials", "system_key", "TEXT");
|
||||||
addColumnIfNotExists("ssh_credentials", "system_key_password", "TEXT");
|
addColumnIfNotExists("ssh_credentials", "system_key_password", "TEXT");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableInfo = sqlite.prepare("PRAGMA table_info(ssh_credentials)").all() as Array<{
|
||||||
|
cid: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
notnull: number;
|
||||||
|
dflt_value: string | null;
|
||||||
|
pk: number;
|
||||||
|
}>;
|
||||||
|
const usernameCol = tableInfo.find((col) => col.name === "username");
|
||||||
|
|
||||||
|
if (usernameCol && usernameCol.notnull === 1) {
|
||||||
|
const tempTableName = "ssh_credentials_temp_migration";
|
||||||
|
const allColumns = tableInfo.map((col) => col.name).join(", ");
|
||||||
|
|
||||||
|
sqlite.exec(`PRAGMA foreign_keys = OFF`);
|
||||||
|
sqlite.exec(`
|
||||||
|
CREATE TABLE ${tempTableName} (
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
private_key TEXT,
|
||||||
|
public_key TEXT,
|
||||||
|
detected_key_type TEXT,
|
||||||
|
system_password TEXT,
|
||||||
|
system_key TEXT,
|
||||||
|
system_key_password TEXT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO ${tempTableName} SELECT ${allColumns} FROM ssh_credentials;
|
||||||
|
|
||||||
|
DROP TABLE ssh_credentials;
|
||||||
|
|
||||||
|
ALTER TABLE ${tempTableName} RENAME TO ssh_credentials;
|
||||||
|
`);
|
||||||
|
sqlite.exec(`PRAGMA foreign_keys = ON`);
|
||||||
|
|
||||||
|
databaseLogger.info("Successfully migrated ssh_credentials table to remove username NOT NULL constraint", {
|
||||||
|
operation: "schema_migration_username_nullable",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (migrationError) {
|
||||||
|
databaseLogger.warn("Failed to migrate ssh_credentials username column", {
|
||||||
|
operation: "schema_migration",
|
||||||
|
error: migrationError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL");
|
addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL");
|
||||||
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
|
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
|
||||||
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
|
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
|
||||||
@@ -925,6 +996,37 @@ const migrateSchema = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
sqlite.prepare("SELECT id FROM opkssh_tokens LIMIT 1").get();
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
sqlite.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS opkssh_tokens (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
host_id INTEGER NOT NULL,
|
||||||
|
ssh_cert TEXT NOT NULL,
|
||||||
|
private_key TEXT NOT NULL,
|
||||||
|
email TEXT,
|
||||||
|
sub TEXT,
|
||||||
|
issuer TEXT,
|
||||||
|
audience TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at TEXT NOT NULL,
|
||||||
|
last_used TEXT,
|
||||||
|
UNIQUE(user_id, host_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
} catch (createError) {
|
||||||
|
databaseLogger.warn("Failed to create opkssh_tokens table", {
|
||||||
|
operation: "schema_migration",
|
||||||
|
error: createError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>;
|
const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>;
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ export const sshData = sqliteTable("ssh_data", {
|
|||||||
socks5Password: text("socks5_password"),
|
socks5Password: text("socks5_password"),
|
||||||
socks5ProxyChain: text("socks5_proxy_chain"),
|
socks5ProxyChain: text("socks5_proxy_chain"),
|
||||||
|
|
||||||
|
hostKeyFingerprint: text("host_key_fingerprint"),
|
||||||
|
hostKeyType: text("host_key_type"),
|
||||||
|
hostKeyAlgorithm: text("host_key_algorithm").default("sha256"),
|
||||||
|
hostKeyFirstSeen: text("host_key_first_seen"),
|
||||||
|
hostKeyLastVerified: text("host_key_last_verified"),
|
||||||
|
hostKeyChangedCount: integer("host_key_changed_count").default(0),
|
||||||
|
|
||||||
createdAt: text("created_at")
|
createdAt: text("created_at")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`CURRENT_TIMESTAMP`),
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
@@ -192,7 +199,7 @@ export const sshCredentials = sqliteTable("ssh_credentials", {
|
|||||||
folder: text("folder"),
|
folder: text("folder"),
|
||||||
tags: text("tags"),
|
tags: text("tags"),
|
||||||
authType: text("auth_type").notNull(),
|
authType: text("auth_type").notNull(),
|
||||||
username: text("username").notNull(),
|
username: text("username"),
|
||||||
password: text("password"),
|
password: text("password"),
|
||||||
key: text("key", { length: 16384 }),
|
key: text("key", { length: 16384 }),
|
||||||
private_key: text("private_key", { length: 16384 }),
|
private_key: text("private_key", { length: 16384 }),
|
||||||
@@ -491,3 +498,27 @@ export const sessionRecordings = sqliteTable("session_recordings", {
|
|||||||
.default(false),
|
.default(false),
|
||||||
terminationReason: text("termination_reason"),
|
terminationReason: text("termination_reason"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const opksshTokens = sqliteTable("opkssh_tokens", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
hostId: integer("host_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
sshCert: text("ssh_cert", { length: 8192 }).notNull(),
|
||||||
|
privateKey: text("private_key", { length: 8192 }).notNull(),
|
||||||
|
|
||||||
|
email: text("email"),
|
||||||
|
sub: text("sub"),
|
||||||
|
issuer: text("issuer"),
|
||||||
|
audience: text("audience"),
|
||||||
|
|
||||||
|
createdAt: text("created_at")
|
||||||
|
.notNull()
|
||||||
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
expiresAt: text("expires_at").notNull(),
|
||||||
|
lastUsed: text("last_used"),
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { eq, and } from "drizzle-orm";
|
|||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { authLogger } from "../../utils/logger.js";
|
import { authLogger } from "../../utils/logger.js";
|
||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
|
import { getProxyAgent } from "../../utils/proxy-agent.js";
|
||||||
|
|
||||||
class AlertCache {
|
class AlertCache {
|
||||||
private cache: Map<string, CacheEntry> = new Map();
|
private cache: Map<string, CacheEntry> = new Map();
|
||||||
@@ -60,6 +61,7 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
|||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"User-Agent": "TermixAlertChecker/1.0",
|
"User-Agent": "TermixAlertChecker/1.0",
|
||||||
},
|
},
|
||||||
|
agent: getProxyAgent(url),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -149,18 +149,13 @@ router.post(
|
|||||||
keyType,
|
keyType,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (
|
if (!isNonEmptyString(userId) || !isNonEmptyString(name)) {
|
||||||
!isNonEmptyString(userId) ||
|
|
||||||
!isNonEmptyString(name) ||
|
|
||||||
!isNonEmptyString(username)
|
|
||||||
) {
|
|
||||||
authLogger.warn("Invalid credential creation data validation failed", {
|
authLogger.warn("Invalid credential creation data validation failed", {
|
||||||
operation: "credential_create",
|
operation: "credential_create",
|
||||||
userId,
|
userId,
|
||||||
hasName: !!name,
|
hasName: !!name,
|
||||||
hasUsername: !!username,
|
|
||||||
});
|
});
|
||||||
return res.status(400).json({ error: "Name and username are required" });
|
return res.status(400).json({ error: "Name is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!["password", "key"].includes(authType)) {
|
if (!["password", "key"].includes(authType)) {
|
||||||
@@ -227,7 +222,7 @@ router.post(
|
|||||||
folder: folder?.trim() || null,
|
folder: folder?.trim() || null,
|
||||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||||
authType,
|
authType,
|
||||||
username: username.trim(),
|
username: username?.trim() || null,
|
||||||
password: plainPassword,
|
password: plainPassword,
|
||||||
key: plainKey,
|
key: plainKey,
|
||||||
private_key: keyInfo?.privateKey || plainKey,
|
private_key: keyInfo?.privateKey || plainKey,
|
||||||
@@ -508,6 +503,12 @@ router.put(
|
|||||||
authLogger.warn("Invalid request for credential update");
|
authLogger.warn("Invalid request for credential update");
|
||||||
return res.status(400).json({ error: "Invalid request" });
|
return res.status(400).json({ error: "Invalid request" });
|
||||||
}
|
}
|
||||||
|
authLogger.info("Updating SSH credential", {
|
||||||
|
operation: "credential_update",
|
||||||
|
userId,
|
||||||
|
credentialId: parseInt(id),
|
||||||
|
changes: Object.keys(updateData),
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existing = await db
|
const existing = await db
|
||||||
@@ -538,7 +539,7 @@ router.put(
|
|||||||
: updateData.tags || "";
|
: updateData.tags || "";
|
||||||
}
|
}
|
||||||
if (updateData.username !== undefined)
|
if (updateData.username !== undefined)
|
||||||
updateFields.username = updateData.username.trim();
|
updateFields.username = updateData.username?.trim() || null;
|
||||||
if (updateData.authType !== undefined)
|
if (updateData.authType !== undefined)
|
||||||
updateFields.authType = updateData.authType;
|
updateFields.authType = updateData.authType;
|
||||||
if (updateData.keyType !== undefined)
|
if (updateData.keyType !== undefined)
|
||||||
@@ -614,17 +615,11 @@ router.put(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const credential = updated[0];
|
const credential = updated[0];
|
||||||
authLogger.success(
|
authLogger.success("SSH credential updated", {
|
||||||
`SSH credential updated: ${credential.name} (${credential.authType}) by user ${userId}`,
|
|
||||||
{
|
|
||||||
operation: "credential_update_success",
|
operation: "credential_update_success",
|
||||||
userId,
|
userId,
|
||||||
credentialId: parseInt(id),
|
credentialId: parseInt(id),
|
||||||
name: credential.name,
|
});
|
||||||
authType: credential.authType,
|
|
||||||
username: credential.username,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(formatCredentialOutput(updated[0]));
|
res.json(formatCredentialOutput(updated[0]));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -673,6 +668,11 @@ router.delete(
|
|||||||
authLogger.warn("Invalid request for credential deletion");
|
authLogger.warn("Invalid request for credential deletion");
|
||||||
return res.status(400).json({ error: "Invalid request" });
|
return res.status(400).json({ error: "Invalid request" });
|
||||||
}
|
}
|
||||||
|
authLogger.info("Deleting SSH credential", {
|
||||||
|
operation: "credential_delete",
|
||||||
|
userId,
|
||||||
|
credentialId: parseInt(id),
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentialToDelete = await db
|
const credentialToDelete = await db
|
||||||
@@ -752,17 +752,11 @@ router.delete(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const credential = credentialToDelete[0];
|
const credential = credentialToDelete[0];
|
||||||
authLogger.success(
|
authLogger.success("SSH credential deleted", {
|
||||||
`SSH credential deleted: ${credential.name} (${credential.authType}) by user ${userId}`,
|
|
||||||
{
|
|
||||||
operation: "credential_delete_success",
|
operation: "credential_delete_success",
|
||||||
userId,
|
userId,
|
||||||
credentialId: parseInt(id),
|
credentialId: parseInt(id),
|
||||||
name: credential.name,
|
});
|
||||||
authType: credential.authType,
|
|
||||||
username: credential.username,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ message: "Credential deleted successfully" });
|
res.json({ message: "Credential deleted successfully" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -846,7 +840,7 @@ router.post(
|
|||||||
.update(sshData)
|
.update(sshData)
|
||||||
.set({
|
.set({
|
||||||
credentialId: parseInt(credentialId),
|
credentialId: parseInt(credentialId),
|
||||||
username: credential.username as string,
|
username: (credential.username as string) || "",
|
||||||
authType: (credential.auth_type || credential.authType) as string,
|
authType: (credential.auth_type || credential.authType) as string,
|
||||||
password: null,
|
password: null,
|
||||||
key: null,
|
key: null,
|
||||||
@@ -961,7 +955,7 @@ function formatCredentialOutput(
|
|||||||
: []
|
: []
|
||||||
: [],
|
: [],
|
||||||
authType: credential.authType || credential.auth_type,
|
authType: credential.authType || credential.auth_type,
|
||||||
username: credential.username,
|
username: credential.username || null,
|
||||||
publicKey: credential.public_key || credential.publicKey,
|
publicKey: credential.public_key || credential.publicKey,
|
||||||
keyType: credential.key_type || credential.keyType,
|
keyType: credential.key_type || credential.keyType,
|
||||||
detectedKeyType: credential.detected_key_type || credential.detectedKeyType,
|
detectedKeyType: credential.detected_key_type || credential.detectedKeyType,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { getDb } from "../db/index.js";
|
|||||||
import { networkTopology } from "../db/schema.js";
|
import { networkTopology } from "../db/schema.js";
|
||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||||
|
import { databaseLogger } from "../../utils/logger.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
@@ -83,7 +84,10 @@ router.get(
|
|||||||
return res.json(null);
|
return res.json(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching network topology:", error);
|
databaseLogger.error("Failed to fetch network topology", error, {
|
||||||
|
operation: "network_topology_fetch",
|
||||||
|
userId: (req as AuthenticatedRequest).userId,
|
||||||
|
});
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: "Failed to fetch network topology",
|
error: "Failed to fetch network topology",
|
||||||
details: (error as Error).message,
|
details: (error as Error).message,
|
||||||
@@ -176,7 +180,10 @@ router.post(
|
|||||||
|
|
||||||
return res.json({ success: true });
|
return res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving network topology:", error);
|
databaseLogger.error("Failed to save network topology", error, {
|
||||||
|
operation: "network_topology_save",
|
||||||
|
userId: (req as AuthenticatedRequest).userId,
|
||||||
|
});
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: "Failed to save network topology",
|
error: "Failed to save network topology",
|
||||||
details: (error as Error).message,
|
details: (error as Error).message,
|
||||||
|
|||||||
@@ -117,10 +117,12 @@ router.post(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (host.length === 0) {
|
if (host.length === 0) {
|
||||||
databaseLogger.warn("Attempt to share host not owned by user", {
|
databaseLogger.warn("Permission denied", {
|
||||||
operation: "share_host",
|
operation: "rbac_permission_denied",
|
||||||
userId,
|
userId,
|
||||||
hostId,
|
resource: "host",
|
||||||
|
resourceId: hostId,
|
||||||
|
action: "share",
|
||||||
});
|
});
|
||||||
return res.status(403).json({ error: "Not host owner" });
|
return res.status(403).json({ error: "Not host owner" });
|
||||||
}
|
}
|
||||||
@@ -218,6 +220,13 @@ router.post(
|
|||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
databaseLogger.info("Permission granted", {
|
||||||
|
operation: "rbac_permission_grant",
|
||||||
|
adminId: userId,
|
||||||
|
hostId,
|
||||||
|
resource: "host",
|
||||||
|
action: "view",
|
||||||
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -254,6 +263,13 @@ router.post(
|
|||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
databaseLogger.success("Host shared successfully", {
|
||||||
|
operation: "rbac_host_share_success",
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
targetUserId: targetType === "user" ? targetUserId : undefined,
|
||||||
|
permissionLevel,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -328,6 +344,12 @@ router.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.delete(hostAccess).where(eq(hostAccess.id, accessId));
|
await db.delete(hostAccess).where(eq(hostAccess.id, accessId));
|
||||||
|
databaseLogger.info("Permission revoked", {
|
||||||
|
operation: "rbac_permission_revoke",
|
||||||
|
adminId: userId,
|
||||||
|
hostId,
|
||||||
|
accessId,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ success: true, message: "Access revoked" });
|
res.json({ success: true, message: "Access revoked" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -990,6 +1012,13 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
permissionManager.invalidateUserPermissionCache(targetUserId);
|
permissionManager.invalidateUserPermissionCache(targetUserId);
|
||||||
|
databaseLogger.info("Role assigned to user", {
|
||||||
|
operation: "rbac_role_assign",
|
||||||
|
adminId: currentUserId,
|
||||||
|
targetUserId,
|
||||||
|
roleId,
|
||||||
|
roleName: role[0].name,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -1082,6 +1111,12 @@ router.delete(
|
|||||||
);
|
);
|
||||||
|
|
||||||
permissionManager.invalidateUserPermissionCache(targetUserId);
|
permissionManager.invalidateUserPermissionCache(targetUserId);
|
||||||
|
databaseLogger.info("Role removed from user", {
|
||||||
|
operation: "rbac_role_remove",
|
||||||
|
adminId: req.userId!,
|
||||||
|
targetUserId,
|
||||||
|
roleId,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { db } from "../db/index.js";
|
|||||||
import { snippets, snippetFolders } from "../db/schema.js";
|
import { snippets, snippetFolders } from "../db/schema.js";
|
||||||
import { eq, and, desc, asc, sql } from "drizzle-orm";
|
import { eq, and, desc, asc, sql } from "drizzle-orm";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { authLogger } from "../../utils/logger.js";
|
import { authLogger, databaseLogger } from "../../utils/logger.js";
|
||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -1043,9 +1043,8 @@ router.post(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await db.insert(snippets).values(insertData).returning();
|
const result = await db.insert(snippets).values(insertData).returning();
|
||||||
|
databaseLogger.info("Command snippet created", {
|
||||||
authLogger.success(`Snippet created: ${name} by user ${userId}`, {
|
operation: "snippet_create",
|
||||||
operation: "snippet_create_success",
|
|
||||||
userId,
|
userId,
|
||||||
snippetId: result[0].id,
|
snippetId: result[0].id,
|
||||||
name,
|
name,
|
||||||
@@ -1156,16 +1155,11 @@ router.put(
|
|||||||
.select()
|
.select()
|
||||||
.from(snippets)
|
.from(snippets)
|
||||||
.where(eq(snippets.id, parseInt(id)));
|
.where(eq(snippets.id, parseInt(id)));
|
||||||
|
databaseLogger.info("Command snippet updated", {
|
||||||
authLogger.success(
|
operation: "snippet_update",
|
||||||
`Snippet updated: ${updated[0].name} by user ${userId}`,
|
|
||||||
{
|
|
||||||
operation: "snippet_update_success",
|
|
||||||
userId,
|
userId,
|
||||||
snippetId: parseInt(id),
|
snippetId: parseInt(id),
|
||||||
name: updated[0].name,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(updated[0]);
|
res.json(updated[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1227,16 +1221,11 @@ router.delete(
|
|||||||
await db
|
await db
|
||||||
.delete(snippets)
|
.delete(snippets)
|
||||||
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
|
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
|
||||||
|
databaseLogger.info("Command snippet deleted", {
|
||||||
authLogger.success(
|
operation: "snippet_delete",
|
||||||
`Snippet deleted: ${existing[0].name} by user ${userId}`,
|
|
||||||
{
|
|
||||||
operation: "snippet_delete_success",
|
|
||||||
userId,
|
userId,
|
||||||
snippetId: parseInt(id),
|
snippetId: parseInt(id),
|
||||||
name: existing[0].name,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { db } from "../db/index.js";
|
|||||||
import { commandHistory } from "../db/schema.js";
|
import { commandHistory } from "../db/schema.js";
|
||||||
import { eq, and, desc, sql } from "drizzle-orm";
|
import { eq, and, desc, sql } from "drizzle-orm";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { authLogger } from "../../utils/logger.js";
|
import { authLogger, databaseLogger } from "../../utils/logger.js";
|
||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -269,9 +269,8 @@ router.delete(
|
|||||||
eq(commandHistory.hostId, hostIdNum),
|
eq(commandHistory.hostId, hostIdNum),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
databaseLogger.info("Terminal history cleared", {
|
||||||
authLogger.success(`Command history cleared for host ${hostId}`, {
|
operation: "terminal_history_clear",
|
||||||
operation: "command_history_clear_success",
|
|
||||||
userId,
|
userId,
|
||||||
hostId: hostIdNum,
|
hostId: hostIdNum,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,50 @@ import { loginRateLimiter } from "../../utils/login-rate-limiter.js";
|
|||||||
|
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OIDC configuration from environment variables.
|
||||||
|
* Returns null if required env vars are not set.
|
||||||
|
*/
|
||||||
|
function getOIDCConfigFromEnv(): {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
issuer_url: string;
|
||||||
|
authorization_url: string;
|
||||||
|
token_url: string;
|
||||||
|
userinfo_url: string;
|
||||||
|
identifier_path: string;
|
||||||
|
name_path: string;
|
||||||
|
scopes: string;
|
||||||
|
} | null {
|
||||||
|
const client_id = process.env.OIDC_CLIENT_ID;
|
||||||
|
const client_secret = process.env.OIDC_CLIENT_SECRET;
|
||||||
|
const issuer_url = process.env.OIDC_ISSUER_URL;
|
||||||
|
const authorization_url = process.env.OIDC_AUTHORIZATION_URL;
|
||||||
|
const token_url = process.env.OIDC_TOKEN_URL;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!client_id ||
|
||||||
|
!client_secret ||
|
||||||
|
!issuer_url ||
|
||||||
|
!authorization_url ||
|
||||||
|
!token_url
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
issuer_url,
|
||||||
|
authorization_url,
|
||||||
|
token_url,
|
||||||
|
userinfo_url: process.env.OIDC_USERINFO_URL || "",
|
||||||
|
identifier_path: process.env.OIDC_IDENTIFIER_PATH || "sub",
|
||||||
|
name_path: process.env.OIDC_NAME_PATH || "name",
|
||||||
|
scopes: process.env.OIDC_SCOPES || "openid email profile",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function verifyOIDCToken(
|
async function verifyOIDCToken(
|
||||||
idToken: string,
|
idToken: string,
|
||||||
issuerUrl: string,
|
issuerUrl: string,
|
||||||
@@ -256,6 +300,10 @@ router.post("/create", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
authLogger.info("User registration attempt", {
|
||||||
|
operation: "user_register_attempt",
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
|
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
|
||||||
authLogger.warn(
|
authLogger.warn(
|
||||||
@@ -277,9 +325,10 @@ router.post("/create", async (req, res) => {
|
|||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.username, username));
|
.where(eq(users.username, username));
|
||||||
if (existing && existing.length > 0) {
|
if (existing && existing.length > 0) {
|
||||||
authLogger.warn(`Attempt to create duplicate username: ${username}`, {
|
authLogger.warn("Registration failed - username exists", {
|
||||||
operation: "user_create",
|
operation: "user_register_failed",
|
||||||
username,
|
username,
|
||||||
|
reason: "username_exists",
|
||||||
});
|
});
|
||||||
return res.status(409).json({ error: "Username already exists" });
|
return res.status(409).json({ error: "Username already exists" });
|
||||||
}
|
}
|
||||||
@@ -367,15 +416,12 @@ router.post("/create", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
authLogger.success(
|
authLogger.success("User registration successful", {
|
||||||
`Traditional user created: ${username} (is_admin: ${isFirstUser})`,
|
operation: "user_register_success",
|
||||||
{
|
userId: id,
|
||||||
operation: "user_create",
|
|
||||||
username,
|
username,
|
||||||
isAdmin: isFirstUser,
|
isAdmin: isFirstUser,
|
||||||
userId: id,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
res.json({
|
res.json({
|
||||||
message: "User created",
|
message: "User created",
|
||||||
is_admin: isFirstUser,
|
is_admin: isFirstUser,
|
||||||
@@ -589,6 +635,16 @@ router.delete("/oidc-config", authenticateJWT, async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.get("/oidc-config", async (req, res) => {
|
router.get("/oidc-config", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const envConfig = getOIDCConfigFromEnv();
|
||||||
|
if (envConfig) {
|
||||||
|
return res.json({
|
||||||
|
client_id: envConfig.client_id,
|
||||||
|
issuer_url: envConfig.issuer_url,
|
||||||
|
authorization_url: envConfig.authorization_url,
|
||||||
|
scopes: envConfig.scopes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const row = db.$client
|
const row = db.$client
|
||||||
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
||||||
.get();
|
.get();
|
||||||
@@ -699,14 +755,20 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.get("/oidc/authorize", async (req, res) => {
|
router.get("/oidc/authorize", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const envConfig = getOIDCConfigFromEnv();
|
||||||
|
let config;
|
||||||
|
|
||||||
|
if (envConfig) {
|
||||||
|
config = envConfig;
|
||||||
|
} else {
|
||||||
const row = db.$client
|
const row = db.$client
|
||||||
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
||||||
.get();
|
.get();
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return res.status(404).json({ error: "OIDC not configured" });
|
return res.status(404).json({ error: "OIDC not configured" });
|
||||||
}
|
}
|
||||||
|
config = JSON.parse((row as Record<string, unknown>).value as string);
|
||||||
const config = JSON.parse((row as Record<string, unknown>).value as string);
|
}
|
||||||
const state = nanoid();
|
const state = nanoid();
|
||||||
const nonce = nanoid();
|
const nonce = nanoid();
|
||||||
|
|
||||||
@@ -760,6 +822,10 @@ router.get("/oidc/authorize", async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.get("/oidc/callback", async (req, res) => {
|
router.get("/oidc/callback", async (req, res) => {
|
||||||
const { code, state } = req.query;
|
const { code, state } = req.query;
|
||||||
|
authLogger.info("OIDC login callback received", {
|
||||||
|
operation: "oidc_login_request",
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
|
||||||
if (!isNonEmptyString(code) || !isNonEmptyString(state)) {
|
if (!isNonEmptyString(code) || !isNonEmptyString(state)) {
|
||||||
return res.status(400).json({ error: "Code and state are required" });
|
return res.status(400).json({ error: "Code and state are required" });
|
||||||
@@ -791,16 +857,22 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
.prepare("DELETE FROM settings WHERE key = ?")
|
.prepare("DELETE FROM settings WHERE key = ?")
|
||||||
.run(`oidc_redirect_${state}`);
|
.run(`oidc_redirect_${state}`);
|
||||||
|
|
||||||
|
const envConfig = getOIDCConfigFromEnv();
|
||||||
|
let config;
|
||||||
|
|
||||||
|
if (envConfig) {
|
||||||
|
config = envConfig;
|
||||||
|
} else {
|
||||||
const configRow = db.$client
|
const configRow = db.$client
|
||||||
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
||||||
.get();
|
.get();
|
||||||
if (!configRow) {
|
if (!configRow) {
|
||||||
return res.status(500).json({ error: "OIDC not configured" });
|
return res.status(500).json({ error: "OIDC not configured" });
|
||||||
}
|
}
|
||||||
|
config = JSON.parse(
|
||||||
const config = JSON.parse(
|
|
||||||
(configRow as Record<string, unknown>).value as string,
|
(configRow as Record<string, unknown>).value as string,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const tokenResponse = await fetch(config.token_url, {
|
const tokenResponse = await fetch(config.token_url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -1121,11 +1193,10 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
deviceInfo: deviceInfo.deviceInfo,
|
deviceInfo: deviceInfo.deviceInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
authLogger.success("OIDC user authenticated", {
|
authLogger.success("OIDC login successful", {
|
||||||
operation: "oidc_login_success",
|
operation: "oidc_login_complete",
|
||||||
userId: userRecord.id,
|
userId: userRecord.id,
|
||||||
deviceType: deviceInfo.type,
|
username: userRecord.username,
|
||||||
deviceInfo: deviceInfo.deviceInfo,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let frontendUrl = (redirectUri as string).replace(
|
let frontendUrl = (redirectUri as string).replace(
|
||||||
@@ -1205,6 +1276,10 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
router.post("/login", async (req, res) => {
|
router.post("/login", async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
|
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
|
||||||
|
authLogger.info("User login request received", {
|
||||||
|
operation: "user_login_request",
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
|
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
|
||||||
authLogger.warn("Invalid traditional login attempt", {
|
authLogger.warn("Invalid traditional login attempt", {
|
||||||
@@ -1361,14 +1436,12 @@ router.post("/login", async (req, res) => {
|
|||||||
|
|
||||||
loginRateLimiter.resetAttempts(clientIp, username);
|
loginRateLimiter.resetAttempts(clientIp, username);
|
||||||
|
|
||||||
authLogger.success(`User logged in successfully: ${username}`, {
|
const payload = await authManager.verifyJWTToken(token);
|
||||||
operation: "user_login_success",
|
authLogger.success("User login successful", {
|
||||||
username,
|
operation: "user_login_complete",
|
||||||
userId: userRecord.id,
|
userId: userRecord.id,
|
||||||
dataUnlocked: true,
|
username,
|
||||||
deviceType: deviceInfo.type,
|
sessionId: payload?.sessionId,
|
||||||
deviceInfo: deviceInfo.deviceInfo,
|
|
||||||
ip: clientIp,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: Record<string, unknown> = {
|
const response: Record<string, unknown> = {
|
||||||
@@ -2375,6 +2448,10 @@ router.post("/complete-reset", async (req, res) => {
|
|||||||
router.post("/change-password", authenticateJWT, async (req, res) => {
|
router.post("/change-password", authenticateJWT, async (req, res) => {
|
||||||
const userId = (req as AuthenticatedRequest).userId;
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
const { oldPassword, newPassword } = req.body;
|
const { oldPassword, newPassword } = req.body;
|
||||||
|
authLogger.info("Password change request", {
|
||||||
|
operation: "password_change_request",
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(401).json({ error: "User not authenticated" });
|
return res.status(401).json({ error: "User not authenticated" });
|
||||||
@@ -2393,6 +2470,11 @@ router.post("/change-password", authenticateJWT, async (req, res) => {
|
|||||||
|
|
||||||
const isMatch = await bcrypt.compare(oldPassword, user[0].password_hash);
|
const isMatch = await bcrypt.compare(oldPassword, user[0].password_hash);
|
||||||
if (!isMatch) {
|
if (!isMatch) {
|
||||||
|
authLogger.warn("Password change failed - old password incorrect", {
|
||||||
|
operation: "password_change_failed",
|
||||||
|
userId,
|
||||||
|
reason: "old_password_wrong",
|
||||||
|
});
|
||||||
return res.status(401).json({ error: "Incorrect current password" });
|
return res.status(401).json({ error: "Incorrect current password" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2412,6 +2494,10 @@ router.post("/change-password", authenticateJWT, async (req, res) => {
|
|||||||
await db.update(users).set({ password_hash }).where(eq(users.id, userId));
|
await db.update(users).set({ password_hash }).where(eq(users.id, userId));
|
||||||
|
|
||||||
authManager.logoutUser(userId);
|
authManager.logoutUser(userId);
|
||||||
|
authLogger.success("Password changed successfully", {
|
||||||
|
operation: "password_change_complete",
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ message: "Password changed successfully. Please log in again." });
|
res.json({ message: "Password changed successfully. Please log in again." });
|
||||||
});
|
});
|
||||||
@@ -2527,9 +2613,12 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
authLogger.success(
|
authLogger.info("Admin privileges granted", {
|
||||||
`User ${username} made admin by ${adminUser[0].username}`,
|
operation: "admin_grant",
|
||||||
);
|
adminId: userId,
|
||||||
|
targetUserId: targetUser[0].id,
|
||||||
|
targetUsername: username,
|
||||||
|
});
|
||||||
res.json({ message: `User ${username} is now an admin` });
|
res.json({ message: `User ${username} is now an admin` });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authLogger.error("Failed to make user admin", err);
|
authLogger.error("Failed to make user admin", err);
|
||||||
@@ -2613,9 +2702,12 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
authLogger.success(
|
authLogger.info("Admin privileges revoked", {
|
||||||
`Admin status removed from ${username} by ${adminUser[0].username}`,
|
operation: "admin_revoke",
|
||||||
);
|
adminId: userId,
|
||||||
|
targetUserId: targetUser[0].id,
|
||||||
|
targetUsername: username,
|
||||||
|
});
|
||||||
res.json({ message: `Admin status removed from ${username}` });
|
res.json({ message: `Admin status removed from ${username}` });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authLogger.error("Failed to remove admin status", err);
|
authLogger.error("Failed to remove admin status", err);
|
||||||
@@ -2753,6 +2845,10 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => {
|
|||||||
totp_backup_codes: JSON.stringify(backupCodes),
|
totp_backup_codes: JSON.stringify(backupCodes),
|
||||||
})
|
})
|
||||||
.where(eq(users.id, userId));
|
.where(eq(users.id, userId));
|
||||||
|
authLogger.info("Two-factor authentication enabled", {
|
||||||
|
operation: "totp_enable",
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: "TOTP enabled successfully",
|
message: "TOTP enabled successfully",
|
||||||
@@ -2843,6 +2939,10 @@ router.post("/totp/disable", authenticateJWT, async (req, res) => {
|
|||||||
totp_backup_codes: null,
|
totp_backup_codes: null,
|
||||||
})
|
})
|
||||||
.where(eq(users.id, userId));
|
.where(eq(users.id, userId));
|
||||||
|
authLogger.info("Two-factor authentication disabled", {
|
||||||
|
operation: "totp_disable",
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ message: "TOTP disabled successfully" });
|
res.json({ message: "TOTP disabled successfully" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -3205,9 +3305,12 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
|
|||||||
|
|
||||||
await deleteUserAndRelatedData(targetUserId);
|
await deleteUserAndRelatedData(targetUserId);
|
||||||
|
|
||||||
authLogger.success(
|
authLogger.warn("User account deleted by admin", {
|
||||||
`User ${username} deleted by admin ${adminUser[0].username}`,
|
operation: "admin_delete_user",
|
||||||
);
|
adminId: userId,
|
||||||
|
targetUserId,
|
||||||
|
targetUsername: username,
|
||||||
|
});
|
||||||
res.json({ message: `User ${username} deleted successfully` });
|
res.json({ message: `User ${username} deleted successfully` });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authLogger.error("Failed to delete user", err);
|
authLogger.error("Failed to delete user", err);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
import type { WebSocket } from "ws";
|
import type { WebSocket } from "ws";
|
||||||
import { sshLogger } from "../utils/logger.js";
|
import { sshLogger, authLogger } from "../utils/logger.js";
|
||||||
import { getDb } from "../database/db/index.js";
|
import { getDb } from "../database/db/index.js";
|
||||||
import { sshCredentials, sshData } from "../database/db/schema.js";
|
import { sshCredentials, sshData } from "../database/db/schema.js";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
@@ -210,6 +210,11 @@ export class SSHAuthManager {
|
|||||||
operation: "ssh_keyboard_interactive_totp_retry",
|
operation: "ssh_keyboard_interactive_totp_retry",
|
||||||
hostId: this.context.hostId,
|
hostId: this.context.hostId,
|
||||||
});
|
});
|
||||||
|
authLogger.warn("TOTP verification failed for SSH session", {
|
||||||
|
operation: "terminal_totp_failed",
|
||||||
|
userId: this.context.userId,
|
||||||
|
hostId: this.context.hostId,
|
||||||
|
});
|
||||||
|
|
||||||
this.sendLog("auth", "warning", "Invalid TOTP code");
|
this.sendLog("auth", "warning", "Invalid TOTP code");
|
||||||
|
|
||||||
@@ -262,6 +267,11 @@ export class SSHAuthManager {
|
|||||||
}, 180000);
|
}, 180000);
|
||||||
|
|
||||||
this.sendLog("auth", "info", "TOTP verification required");
|
this.sendLog("auth", "info", "TOTP verification required");
|
||||||
|
authLogger.info("TOTP verification prompt sent to client", {
|
||||||
|
operation: "terminal_totp_prompt",
|
||||||
|
userId: this.context.userId,
|
||||||
|
hostId: this.context.hostId,
|
||||||
|
});
|
||||||
|
|
||||||
this.context.ws.send(
|
this.context.ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
|||||||
import { systemLogger } from "../utils/logger.js";
|
import { systemLogger } from "../utils/logger.js";
|
||||||
import type { SSHHost } from "../../types/index.js";
|
import type { SSHHost } from "../../types/index.js";
|
||||||
|
|
||||||
const dockerConsoleLogger = systemLogger;
|
const sshLogger = systemLogger;
|
||||||
|
|
||||||
interface SSHSession {
|
interface SSHSession {
|
||||||
client: SSHClient;
|
client: SSHClient;
|
||||||
@@ -17,6 +17,7 @@ interface SSHSession {
|
|||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
shell?: string;
|
shell?: string;
|
||||||
|
hostId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeSessions = new Map<string, SSHSession>();
|
const activeSessions = new Map<string, SSHSession>();
|
||||||
@@ -121,7 +122,7 @@ async function createJumpHostChain(
|
|||||||
try {
|
try {
|
||||||
jumpHost.jumpHosts = JSON.parse(jumpHost.jumpHosts);
|
jumpHost.jumpHosts = JSON.parse(jumpHost.jumpHosts);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dockerConsoleLogger.error("Failed to parse jump hosts", e, {
|
sshLogger.error("Failed to parse jump hosts", e, {
|
||||||
hostId: jumpHost.id,
|
hostId: jumpHost.id,
|
||||||
});
|
});
|
||||||
jumpHost.jumpHosts = [];
|
jumpHost.jumpHosts = [];
|
||||||
@@ -226,6 +227,11 @@ async function createJumpHostChain(
|
|||||||
wss.on("connection", async (ws: WebSocket, req) => {
|
wss.on("connection", async (ws: WebSocket, req) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
const sessionId = `docker-console-${Date.now()}-${Math.random()}`;
|
const sessionId = `docker-console-${Date.now()}-${Math.random()}`;
|
||||||
|
sshLogger.info("Docker console WebSocket connected", {
|
||||||
|
operation: "docker_console_connect",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
let sshSession: SSHSession | null = null;
|
let sshSession: SSHSession | null = null;
|
||||||
|
|
||||||
@@ -251,7 +257,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
try {
|
try {
|
||||||
hostConfig.jumpHosts = JSON.parse(hostConfig.jumpHosts);
|
hostConfig.jumpHosts = JSON.parse(hostConfig.jumpHosts);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dockerConsoleLogger.error("Failed to parse jump hosts", e, {
|
sshLogger.error("Failed to parse jump hosts", e, {
|
||||||
hostId: hostConfig.id,
|
hostId: hostConfig.id,
|
||||||
});
|
});
|
||||||
hostConfig.jumpHosts = [];
|
hostConfig.jumpHosts = [];
|
||||||
@@ -383,6 +389,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
stream: null,
|
stream: null,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
containerId,
|
containerId,
|
||||||
|
hostId: hostConfig.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
activeSessions.set(sessionId, sshSession);
|
activeSessions.set(sessionId, sshSession);
|
||||||
@@ -417,7 +424,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
dockerConsoleLogger.warn(
|
sshLogger.warn(
|
||||||
`Requested shell ${shell} not found, detecting available shell`,
|
`Requested shell ${shell} not found, detecting available shell`,
|
||||||
{
|
{
|
||||||
operation: "shell_validation",
|
operation: "shell_validation",
|
||||||
@@ -435,6 +442,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
sshSession.shell = shellToUse;
|
sshSession.shell = shellToUse;
|
||||||
|
|
||||||
const execCommand = `docker exec -it ${containerId} /bin/${shellToUse}`;
|
const execCommand = `docker exec -it ${containerId} /bin/${shellToUse}`;
|
||||||
|
sshLogger.info("Attaching to Docker container", {
|
||||||
|
operation: "docker_attach",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: hostConfig.id,
|
||||||
|
containerId,
|
||||||
|
});
|
||||||
|
|
||||||
client.exec(
|
client.exec(
|
||||||
execCommand,
|
execCommand,
|
||||||
@@ -447,15 +461,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
},
|
},
|
||||||
(err, stream) => {
|
(err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dockerConsoleLogger.error(
|
sshLogger.error("Failed to create docker exec", err, {
|
||||||
"Failed to create docker exec",
|
|
||||||
err,
|
|
||||||
{
|
|
||||||
operation: "docker_exec",
|
operation: "docker_exec",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -467,6 +477,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sshSession!.stream = stream;
|
sshSession!.stream = stream;
|
||||||
|
sshLogger.success("Docker container attached", {
|
||||||
|
operation: "docker_attach_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: hostConfig.id,
|
||||||
|
containerId,
|
||||||
|
});
|
||||||
|
|
||||||
stream.on("data", (data: Buffer) => {
|
stream.on("data", (data: Buffer) => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
@@ -510,7 +527,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerConsoleLogger.error("Failed to connect to container", error, {
|
sshLogger.error("Failed to connect to container", error, {
|
||||||
operation: "console_connect",
|
operation: "console_connect",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId: message.data.containerId,
|
containerId: message.data.containerId,
|
||||||
@@ -570,13 +587,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
dockerConsoleLogger.warn("Unknown message type", {
|
sshLogger.warn("Unknown message type", {
|
||||||
operation: "ws_message",
|
operation: "ws_message",
|
||||||
type: message.type,
|
type: message.type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerConsoleLogger.error("WebSocket message error", error, {
|
sshLogger.error("WebSocket message error", error, {
|
||||||
operation: "ws_message",
|
operation: "ws_message",
|
||||||
sessionId,
|
sessionId,
|
||||||
});
|
});
|
||||||
@@ -591,6 +608,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
|
sshLogger.info("Docker console disconnected", {
|
||||||
|
operation: "docker_console_disconnect",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: sshSession?.hostId,
|
||||||
|
containerId: sshSession?.containerId,
|
||||||
|
});
|
||||||
if (sshSession) {
|
if (sshSession) {
|
||||||
if (sshSession.stream) {
|
if (sshSession.stream) {
|
||||||
sshSession.stream.end();
|
sshSession.stream.end();
|
||||||
@@ -601,7 +625,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on("error", (error) => {
|
ws.on("error", (error) => {
|
||||||
dockerConsoleLogger.error("WebSocket error", error, {
|
sshLogger.error("WebSocket error", error, {
|
||||||
operation: "ws_error",
|
operation: "ws_error",
|
||||||
sessionId,
|
sessionId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import { AuthManager } from "../utils/auth-manager.js";
|
|||||||
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
import type { AuthenticatedRequest, SSHHost } from "../../types/index.js";
|
import type { AuthenticatedRequest, SSHHost } from "../../types/index.js";
|
||||||
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
|
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
|
||||||
|
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
|
||||||
|
|
||||||
const dockerLogger = logger;
|
const sshLogger = logger;
|
||||||
|
|
||||||
function createConnectionLog(
|
function createConnectionLog(
|
||||||
type: "info" | "success" | "warning" | "error",
|
type: "info" | "success" | "warning" | "error",
|
||||||
@@ -79,7 +80,7 @@ function cleanupSession(sessionId: string) {
|
|||||||
const session = sshSessions[sessionId];
|
const session = sshSessions[sessionId];
|
||||||
if (session) {
|
if (session) {
|
||||||
if (session.activeOperations > 0) {
|
if (session.activeOperations > 0) {
|
||||||
dockerLogger.warn(
|
sshLogger.warn(
|
||||||
`Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`,
|
`Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`,
|
||||||
{
|
{
|
||||||
operation: "cleanup_deferred",
|
operation: "cleanup_deferred",
|
||||||
@@ -161,7 +162,7 @@ async function resolveJumpHost(
|
|||||||
|
|
||||||
return host;
|
return host;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerLogger.error("Failed to resolve jump host", error, {
|
sshLogger.error("Failed to resolve jump host", error, {
|
||||||
operation: "resolve_jump_host",
|
operation: "resolve_jump_host",
|
||||||
hostId,
|
hostId,
|
||||||
userId,
|
userId,
|
||||||
@@ -188,7 +189,7 @@ async function createJumpHostChain(
|
|||||||
|
|
||||||
for (let i = 0; i < jumpHostConfigs.length; i++) {
|
for (let i = 0; i < jumpHostConfigs.length; i++) {
|
||||||
if (!jumpHostConfigs[i]) {
|
if (!jumpHostConfigs[i]) {
|
||||||
dockerLogger.error(`Jump host ${i + 1} not found`, undefined, {
|
sshLogger.error(`Jump host ${i + 1} not found`, undefined, {
|
||||||
operation: "jump_host_chain",
|
operation: "jump_host_chain",
|
||||||
hostId: jumpHosts[i].hostId,
|
hostId: jumpHosts[i].hostId,
|
||||||
});
|
});
|
||||||
@@ -203,6 +204,15 @@ async function createJumpHostChain(
|
|||||||
const jumpClient = new SSHClient();
|
const jumpClient = new SSHClient();
|
||||||
clients.push(jumpClient);
|
clients.push(jumpClient);
|
||||||
|
|
||||||
|
const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier(
|
||||||
|
jumpHostConfig.id,
|
||||||
|
jumpHostConfig.ip,
|
||||||
|
jumpHostConfig.port || 22,
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const connected = await new Promise<boolean>((resolve) => {
|
const connected = await new Promise<boolean>((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
@@ -215,7 +225,7 @@ async function createJumpHostChain(
|
|||||||
|
|
||||||
jumpClient.on("error", (err) => {
|
jumpClient.on("error", (err) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
dockerLogger.error(`Jump host ${i + 1} connection failed`, err, {
|
sshLogger.error(`Jump host ${i + 1} connection failed`, err, {
|
||||||
operation: "jump_host_connect",
|
operation: "jump_host_connect",
|
||||||
hostId: jumpHostConfig.id,
|
hostId: jumpHostConfig.id,
|
||||||
ip: jumpHostConfig.ip,
|
ip: jumpHostConfig.ip,
|
||||||
@@ -229,6 +239,7 @@ async function createJumpHostChain(
|
|||||||
username: jumpHostConfig.username,
|
username: jumpHostConfig.username,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
readyTimeout: 30000,
|
readyTimeout: 30000,
|
||||||
|
hostVerifier: jumpHostVerifier,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
||||||
@@ -275,7 +286,7 @@ async function createJumpHostChain(
|
|||||||
|
|
||||||
return currentClient;
|
return currentClient;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerLogger.error("Failed to create jump host chain", error, {
|
sshLogger.error("Failed to create jump host chain", error, {
|
||||||
operation: "jump_host_chain",
|
operation: "jump_host_chain",
|
||||||
});
|
});
|
||||||
clients.forEach((c) => c.end());
|
clients.forEach((c) => c.end());
|
||||||
@@ -286,13 +297,27 @@ async function createJumpHostChain(
|
|||||||
async function executeDockerCommand(
|
async function executeDockerCommand(
|
||||||
session: SSHSession,
|
session: SSHSession,
|
||||||
command: string,
|
command: string,
|
||||||
|
sessionId?: string,
|
||||||
|
userId?: string,
|
||||||
|
hostId?: number,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
sshLogger.info("Executing Docker command", {
|
||||||
|
operation: "docker_command_exec",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
command: command.split(" ")[1],
|
||||||
|
});
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
session.client.exec(command, (err, stream) => {
|
session.client.exec(command, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dockerLogger.error("Docker command execution error", err, {
|
sshLogger.error("Docker command execution error", err, {
|
||||||
operation: "execute_docker_command",
|
operation: "execute_docker_command",
|
||||||
command,
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
command: command.split(" ")[1],
|
||||||
});
|
});
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
@@ -302,14 +327,25 @@ async function executeDockerCommand(
|
|||||||
|
|
||||||
stream.on("close", (code: number) => {
|
stream.on("close", (code: number) => {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
dockerLogger.error("Docker command failed", undefined, {
|
sshLogger.error("Docker command failed", undefined, {
|
||||||
operation: "execute_docker_command",
|
operation: "execute_docker_command",
|
||||||
command,
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
command: command.split(" ")[1],
|
||||||
exitCode: code,
|
exitCode: code,
|
||||||
stderr,
|
stderr,
|
||||||
});
|
});
|
||||||
reject(new Error(stderr || `Command exited with code ${code}`));
|
reject(new Error(stderr || `Command exited with code ${code}`));
|
||||||
} else {
|
} else {
|
||||||
|
sshLogger.success("Docker command completed", {
|
||||||
|
operation: "docker_command_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
command: command.split(" ")[1],
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
});
|
||||||
resolve(stdout);
|
resolve(stdout);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -323,9 +359,12 @@ async function executeDockerCommand(
|
|||||||
});
|
});
|
||||||
|
|
||||||
stream.on("error", (streamErr: Error) => {
|
stream.on("error", (streamErr: Error) => {
|
||||||
dockerLogger.error("Docker command stream error", streamErr, {
|
sshLogger.error("Docker command stream error", streamErr, {
|
||||||
operation: "execute_docker_command",
|
operation: "execute_docker_command",
|
||||||
command,
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
command: command.split(" ")[1],
|
||||||
});
|
});
|
||||||
reject(streamErr);
|
reject(streamErr);
|
||||||
});
|
});
|
||||||
@@ -350,12 +389,7 @@ app.use(
|
|||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedOrigins = [
|
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
||||||
"http://localhost:5173",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:5173",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (allowedOrigins.includes(origin)) {
|
if (allowedOrigins.includes(origin)) {
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
@@ -429,13 +463,10 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
const connectionLogs: Array<Omit<LogEntry, "id" | "timestamp">> = [];
|
const connectionLogs: Array<Omit<LogEntry, "id" | "timestamp">> = [];
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
dockerLogger.error(
|
sshLogger.error("Docker SSH connection rejected: no authenticated user", {
|
||||||
"Docker SSH connection rejected: no authenticated user",
|
|
||||||
{
|
|
||||||
operation: "docker_connect_auth",
|
operation: "docker_connect_auth",
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
connectionLogs.push(
|
connectionLogs.push(
|
||||||
createConnectionLog(
|
createConnectionLog(
|
||||||
"error",
|
"error",
|
||||||
@@ -460,7 +491,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionId || !hostId) {
|
if (!sessionId || !hostId) {
|
||||||
dockerLogger.warn("Missing Docker SSH connection parameters", {
|
sshLogger.warn("Missing Docker SSH connection parameters", {
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
sessionId,
|
sessionId,
|
||||||
hasHostId: !!hostId,
|
hasHostId: !!hostId,
|
||||||
@@ -512,7 +543,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!accessInfo.hasAccess) {
|
if (!accessInfo.hasAccess) {
|
||||||
dockerLogger.warn("User does not have access to host", {
|
sshLogger.warn("User does not have access to host", {
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
hostId,
|
hostId,
|
||||||
userId,
|
userId,
|
||||||
@@ -531,7 +562,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
host.jumpHosts = JSON.parse(host.jumpHosts);
|
host.jumpHosts = JSON.parse(host.jumpHosts);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dockerLogger.error("Failed to parse jump hosts", e, {
|
sshLogger.error("Failed to parse jump hosts", e, {
|
||||||
hostId: host.id,
|
hostId: host.id,
|
||||||
});
|
});
|
||||||
host.jumpHosts = [];
|
host.jumpHosts = [];
|
||||||
@@ -539,7 +570,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!host.enableDocker) {
|
if (!host.enableDocker) {
|
||||||
dockerLogger.warn("Docker not enabled for host", {
|
sshLogger.warn("Docker not enabled for host", {
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
hostId,
|
hostId,
|
||||||
userId,
|
userId,
|
||||||
@@ -618,7 +649,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerLogger.error("Failed to resolve shared credential", error, {
|
sshLogger.error("Failed to resolve shared credential", error, {
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
hostId,
|
hostId,
|
||||||
userId,
|
userId,
|
||||||
@@ -664,6 +695,14 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
readyTimeout: 60000,
|
readyTimeout: 60000,
|
||||||
tcpKeepAlive: true,
|
tcpKeepAlive: true,
|
||||||
tcpKeepAliveInitialDelay: 30000,
|
tcpKeepAliveInitialDelay: 30000,
|
||||||
|
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
|
||||||
|
hostId,
|
||||||
|
host.ip,
|
||||||
|
host.port || 22,
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (resolvedCredentials.authType === "none") {
|
if (resolvedCredentials.authType === "none") {
|
||||||
@@ -671,6 +710,94 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
if (resolvedCredentials.password) {
|
if (resolvedCredentials.password) {
|
||||||
config.password = resolvedCredentials.password;
|
config.password = resolvedCredentials.password;
|
||||||
}
|
}
|
||||||
|
} else if (resolvedCredentials.authType === "opkssh") {
|
||||||
|
try {
|
||||||
|
const { getOPKSSHToken } = await import("./opkssh-auth.js");
|
||||||
|
const token = await getOPKSSHToken(userId, hostId);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"error",
|
||||||
|
"docker_auth",
|
||||||
|
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return res.status(401).json({
|
||||||
|
error:
|
||||||
|
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.",
|
||||||
|
requiresOPKSSHAuth: true,
|
||||||
|
connectionLogs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { promises: fs } = await import("fs");
|
||||||
|
const path = await import("path");
|
||||||
|
const os = await import("os");
|
||||||
|
|
||||||
|
const tempDir = os.tmpdir();
|
||||||
|
const keyPath = path.join(tempDir, `opkssh-docker-${userId}-${hostId}`);
|
||||||
|
const certPath = `${keyPath}-cert.pub`;
|
||||||
|
|
||||||
|
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
|
||||||
|
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
|
||||||
|
|
||||||
|
config.privateKey = await fs.readFile(keyPath);
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"info",
|
||||||
|
"docker_auth",
|
||||||
|
"Using OPKSSH certificate authentication",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const cleanupResults = await Promise.allSettled([
|
||||||
|
fs.unlink(keyPath),
|
||||||
|
fs.unlink(certPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cleanupResults.forEach((result, index) => {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
sshLogger.warn(`Failed to cleanup OPKSSH temp file`, {
|
||||||
|
operation: "opkssh_temp_cleanup_failed",
|
||||||
|
file: index === 0 ? "keyPath" : "certPath",
|
||||||
|
sessionId,
|
||||||
|
error: result.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("Failed to cleanup OPKSSH temp files", {
|
||||||
|
operation: "opkssh_temp_cleanup_error",
|
||||||
|
sessionId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
} catch (opksshError) {
|
||||||
|
sshLogger.error("OPKSSH authentication error for Docker", {
|
||||||
|
operation: "docker_connect",
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
error:
|
||||||
|
opksshError instanceof Error
|
||||||
|
? opksshError.message
|
||||||
|
: "Unknown error",
|
||||||
|
});
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"error",
|
||||||
|
"docker_auth",
|
||||||
|
`OPKSSH authentication failed: ${opksshError instanceof Error ? opksshError.message : "Unknown error"}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "OPKSSH authentication failed",
|
||||||
|
connectionLogs,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
resolvedCredentials.authType === "key" &&
|
resolvedCredentials.authType === "key" &&
|
||||||
resolvedCredentials.sshKey
|
resolvedCredentials.sshKey
|
||||||
@@ -680,7 +807,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
!resolvedCredentials.sshKey.includes("-----BEGIN") ||
|
!resolvedCredentials.sshKey.includes("-----BEGIN") ||
|
||||||
!resolvedCredentials.sshKey.includes("-----END")
|
!resolvedCredentials.sshKey.includes("-----END")
|
||||||
) {
|
) {
|
||||||
dockerLogger.error("Invalid SSH key format", {
|
sshLogger.error("Invalid SSH key format", {
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
sessionId,
|
sessionId,
|
||||||
hostId,
|
hostId,
|
||||||
@@ -707,7 +834,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
config.passphrase = resolvedCredentials.keyPassword;
|
config.passphrase = resolvedCredentials.keyPassword;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerLogger.error("SSH key processing error", error, {
|
sshLogger.error("SSH key processing error", error, {
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
sessionId,
|
sessionId,
|
||||||
hostId,
|
hostId,
|
||||||
@@ -725,14 +852,11 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (resolvedCredentials.authType === "key") {
|
} else if (resolvedCredentials.authType === "key") {
|
||||||
dockerLogger.error(
|
sshLogger.error("SSH key authentication requested but no key provided", {
|
||||||
"SSH key authentication requested but no key provided",
|
|
||||||
{
|
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
sessionId,
|
sessionId,
|
||||||
hostId,
|
hostId,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
connectionLogs.push(
|
connectionLogs.push(
|
||||||
createConnectionLog(
|
createConnectionLog(
|
||||||
"error",
|
"error",
|
||||||
@@ -814,7 +938,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
|
|
||||||
client.on("error", (err) => {
|
client.on("error", (err) => {
|
||||||
if (responseSent) {
|
if (responseSent) {
|
||||||
dockerLogger.error(
|
sshLogger.error(
|
||||||
"Docker SSH connection error after response sent",
|
"Docker SSH connection error after response sent",
|
||||||
err,
|
err,
|
||||||
{
|
{
|
||||||
@@ -832,7 +956,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
}
|
}
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
|
|
||||||
dockerLogger.error("Docker SSH connection failed", err, {
|
sshLogger.error("Docker SSH connection failed", err, {
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
sessionId,
|
sessionId,
|
||||||
hostId,
|
hostId,
|
||||||
@@ -888,6 +1012,15 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
`Authentication failed: ${err.message}`,
|
`Authentication failed: ${err.message}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else if (err.message.includes("verification failed")) {
|
||||||
|
errorStage = "handshake";
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"error",
|
||||||
|
errorStage,
|
||||||
|
`SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
connectionLogs.push(
|
connectionLogs.push(
|
||||||
createConnectionLog(
|
createConnectionLog(
|
||||||
@@ -1177,7 +1310,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (socks5Error) {
|
} catch (socks5Error) {
|
||||||
dockerLogger.error("SOCKS5 connection failed", socks5Error, {
|
sshLogger.error("SOCKS5 connection failed", socks5Error, {
|
||||||
operation: "docker_socks5_connect",
|
operation: "docker_socks5_connect",
|
||||||
sessionId,
|
sessionId,
|
||||||
hostId,
|
hostId,
|
||||||
@@ -1238,7 +1371,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
host.port || 22,
|
host.port || 22,
|
||||||
(err, stream) => {
|
(err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
dockerLogger.error("Failed to forward through jump host", err, {
|
sshLogger.error("Failed to forward through jump host", err, {
|
||||||
operation: "docker_jump_forward",
|
operation: "docker_jump_forward",
|
||||||
sessionId,
|
sessionId,
|
||||||
hostId,
|
hostId,
|
||||||
@@ -1269,7 +1402,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
client.connect(config);
|
client.connect(config);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerLogger.error("Docker SSH connection error", error, {
|
sshLogger.error("Docker SSH connection error", error, {
|
||||||
operation: "docker_connect",
|
operation: "docker_connect",
|
||||||
sessionId,
|
sessionId,
|
||||||
hostId,
|
hostId,
|
||||||
@@ -1359,7 +1492,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
|||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
dockerLogger.error("TOTP verification rejected: no authenticated user", {
|
sshLogger.error("TOTP verification rejected: no authenticated user", {
|
||||||
operation: "docker_totp_auth",
|
operation: "docker_totp_auth",
|
||||||
sessionId,
|
sessionId,
|
||||||
});
|
});
|
||||||
@@ -1373,7 +1506,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
|||||||
const session = pendingTOTPSessions[sessionId];
|
const session = pendingTOTPSessions[sessionId];
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
dockerLogger.warn("TOTP session not found or expired", {
|
sshLogger.warn("TOTP session not found or expired", {
|
||||||
operation: "docker_totp_verify",
|
operation: "docker_totp_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1389,7 +1522,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
session.client.end();
|
session.client.end();
|
||||||
} catch {}
|
} catch {}
|
||||||
dockerLogger.warn("TOTP session timeout before code submission", {
|
sshLogger.warn("TOTP session timeout before code submission", {
|
||||||
operation: "docker_totp_verify",
|
operation: "docker_totp_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1471,7 +1604,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerLogger.warn("Failed to log Docker activity (TOTP)", {
|
sshLogger.warn("Failed to log Docker activity (TOTP)", {
|
||||||
operation: "activity_log_error",
|
operation: "activity_log_error",
|
||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
hostId: session.hostId,
|
hostId: session.hostId,
|
||||||
@@ -1490,7 +1623,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
|||||||
|
|
||||||
delete pendingTOTPSessions[sessionId];
|
delete pendingTOTPSessions[sessionId];
|
||||||
|
|
||||||
dockerLogger.error("TOTP verification failed", {
|
sshLogger.error("TOTP verification failed", {
|
||||||
operation: "docker_totp_verify",
|
operation: "docker_totp_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1504,7 +1637,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
|||||||
if (!responseSent) {
|
if (!responseSent) {
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
delete pendingTOTPSessions[sessionId];
|
delete pendingTOTPSessions[sessionId];
|
||||||
dockerLogger.warn("TOTP verification timeout", {
|
sshLogger.warn("TOTP verification timeout", {
|
||||||
operation: "docker_totp_verify",
|
operation: "docker_totp_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1549,13 +1682,10 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
|||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
dockerLogger.error(
|
sshLogger.error("Warpgate verification rejected: no authenticated user", {
|
||||||
"Warpgate verification rejected: no authenticated user",
|
|
||||||
{
|
|
||||||
operation: "docker_warpgate_auth",
|
operation: "docker_warpgate_auth",
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
return res.status(401).json({ error: "Authentication required" });
|
return res.status(401).json({ error: "Authentication required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1566,7 +1696,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
|||||||
const session = pendingTOTPSessions[sessionId];
|
const session = pendingTOTPSessions[sessionId];
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
dockerLogger.warn("Warpgate session not found or expired", {
|
sshLogger.warn("Warpgate session not found or expired", {
|
||||||
operation: "docker_warpgate_verify",
|
operation: "docker_warpgate_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1586,7 +1716,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
session.client.end();
|
session.client.end();
|
||||||
} catch {}
|
} catch {}
|
||||||
dockerLogger.warn("Warpgate session timeout before completion", {
|
sshLogger.warn("Warpgate session timeout before completion", {
|
||||||
operation: "docker_warpgate_verify",
|
operation: "docker_warpgate_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1658,7 +1788,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dockerLogger.warn("Failed to log Docker activity (Warpgate)", {
|
sshLogger.warn("Failed to log Docker activity (Warpgate)", {
|
||||||
operation: "activity_log_error",
|
operation: "activity_log_error",
|
||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
hostId: session.hostId,
|
hostId: session.hostId,
|
||||||
@@ -1677,7 +1807,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
|||||||
|
|
||||||
delete pendingTOTPSessions[sessionId];
|
delete pendingTOTPSessions[sessionId];
|
||||||
|
|
||||||
dockerLogger.error("Warpgate verification failed", {
|
sshLogger.error("Warpgate verification failed", {
|
||||||
operation: "docker_warpgate_verify",
|
operation: "docker_warpgate_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1693,7 +1823,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
|||||||
if (!responseSent) {
|
if (!responseSent) {
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
delete pendingTOTPSessions[sessionId];
|
delete pendingTOTPSessions[sessionId];
|
||||||
dockerLogger.warn("Warpgate verification timeout", {
|
sshLogger.warn("Warpgate verification timeout", {
|
||||||
operation: "docker_warpgate_verify",
|
operation: "docker_warpgate_verify",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1840,12 +1970,21 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
|
|||||||
const versionOutput = await executeDockerCommand(
|
const versionOutput = await executeDockerCommand(
|
||||||
session,
|
session,
|
||||||
"docker --version",
|
"docker --version",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
);
|
);
|
||||||
const versionMatch = versionOutput.match(/Docker version ([^\s,]+)/);
|
const versionMatch = versionOutput.match(/Docker version ([^\s,]+)/);
|
||||||
const version = versionMatch ? versionMatch[1] : "unknown";
|
const version = versionMatch ? versionMatch[1] : "unknown";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeDockerCommand(session, "docker ps >/dev/null 2>&1");
|
await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
"docker ps >/dev/null 2>&1",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
return res.json({
|
return res.json({
|
||||||
@@ -1892,7 +2031,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
dockerLogger.error("Docker validation error", error, {
|
sshLogger.error("Docker validation error", error, {
|
||||||
operation: "docker_validate",
|
operation: "docker_validate",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -1962,7 +2101,13 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
|
|||||||
const allFlag = all ? "-a " : "";
|
const allFlag = all ? "-a " : "";
|
||||||
const command = `docker ps ${allFlag}--format '{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","created":"{{.CreatedAt}}"}'`;
|
const command = `docker ps ${allFlag}--format '{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","created":"{{.CreatedAt}}"}'`;
|
||||||
|
|
||||||
const output = await executeDockerCommand(session, command);
|
const output = await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
command,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
|
|
||||||
const containers = output
|
const containers = output
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -1971,7 +2116,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(line);
|
return JSON.parse(line);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dockerLogger.warn("Failed to parse container line", {
|
sshLogger.warn("Failed to parse container line", {
|
||||||
operation: "parse_container",
|
operation: "parse_container",
|
||||||
line,
|
line,
|
||||||
});
|
});
|
||||||
@@ -1985,7 +2130,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
|
|||||||
res.json(containers);
|
res.json(containers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
dockerLogger.error("Failed to list Docker containers", error, {
|
sshLogger.error("Failed to list Docker containers", error, {
|
||||||
operation: "list_containers",
|
operation: "list_containers",
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
@@ -2048,7 +2193,13 @@ app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const command = `docker inspect ${containerId}`;
|
const command = `docker inspect ${containerId}`;
|
||||||
const output = await executeDockerCommand(session, command);
|
const output = await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
command,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
const details = JSON.parse(output);
|
const details = JSON.parse(output);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
@@ -2072,7 +2223,7 @@ app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to get container details", error, {
|
sshLogger.error("Failed to get container details", error, {
|
||||||
operation: "get_container_details",
|
operation: "get_container_details",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2136,7 +2287,21 @@ app.post(
|
|||||||
session.activeOperations++;
|
session.activeOperations++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeDockerCommand(session, `docker start ${containerId}`);
|
sshLogger.info("Docker container operation", {
|
||||||
|
operation: "docker_container_op",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: session.hostId,
|
||||||
|
containerId,
|
||||||
|
action: "start",
|
||||||
|
});
|
||||||
|
await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
`docker start ${containerId}`,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
|
|
||||||
@@ -2156,7 +2321,7 @@ app.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to start container", error, {
|
sshLogger.error("Failed to start container", error, {
|
||||||
operation: "start_container",
|
operation: "start_container",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2222,7 +2387,21 @@ app.post(
|
|||||||
session.activeOperations++;
|
session.activeOperations++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeDockerCommand(session, `docker stop ${containerId}`);
|
sshLogger.info("Docker container operation", {
|
||||||
|
operation: "docker_container_op",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: session.hostId,
|
||||||
|
containerId,
|
||||||
|
action: "stop",
|
||||||
|
});
|
||||||
|
await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
`docker stop ${containerId}`,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
|
|
||||||
@@ -2242,7 +2421,7 @@ app.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to stop container", error, {
|
sshLogger.error("Failed to stop container", error, {
|
||||||
operation: "stop_container",
|
operation: "stop_container",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2308,7 +2487,21 @@ app.post(
|
|||||||
session.activeOperations++;
|
session.activeOperations++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeDockerCommand(session, `docker restart ${containerId}`);
|
sshLogger.info("Docker container operation", {
|
||||||
|
operation: "docker_container_op",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: session.hostId,
|
||||||
|
containerId,
|
||||||
|
action: "restart",
|
||||||
|
});
|
||||||
|
await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
`docker restart ${containerId}`,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
|
|
||||||
@@ -2328,7 +2521,7 @@ app.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to restart container", error, {
|
sshLogger.error("Failed to restart container", error, {
|
||||||
operation: "restart_container",
|
operation: "restart_container",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2394,7 +2587,21 @@ app.post(
|
|||||||
session.activeOperations++;
|
session.activeOperations++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeDockerCommand(session, `docker pause ${containerId}`);
|
sshLogger.info("Docker container operation", {
|
||||||
|
operation: "docker_container_op",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: session.hostId,
|
||||||
|
containerId,
|
||||||
|
action: "pause",
|
||||||
|
});
|
||||||
|
await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
`docker pause ${containerId}`,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
|
|
||||||
@@ -2414,7 +2621,7 @@ app.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to pause container", error, {
|
sshLogger.error("Failed to pause container", error, {
|
||||||
operation: "pause_container",
|
operation: "pause_container",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2480,7 +2687,21 @@ app.post(
|
|||||||
session.activeOperations++;
|
session.activeOperations++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeDockerCommand(session, `docker unpause ${containerId}`);
|
sshLogger.info("Docker container operation", {
|
||||||
|
operation: "docker_container_op",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: session.hostId,
|
||||||
|
containerId,
|
||||||
|
action: "unpause",
|
||||||
|
});
|
||||||
|
await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
`docker unpause ${containerId}`,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
|
|
||||||
@@ -2500,7 +2721,7 @@ app.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to unpause container", error, {
|
sshLogger.error("Failed to unpause container", error, {
|
||||||
operation: "unpause_container",
|
operation: "unpause_container",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2571,10 +2792,21 @@ app.delete(
|
|||||||
session.activeOperations++;
|
session.activeOperations++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
sshLogger.info("Docker container operation", {
|
||||||
|
operation: "docker_container_op",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: session.hostId,
|
||||||
|
containerId,
|
||||||
|
action: "remove",
|
||||||
|
});
|
||||||
const forceFlag = force ? "-f " : "";
|
const forceFlag = force ? "-f " : "";
|
||||||
await executeDockerCommand(
|
await executeDockerCommand(
|
||||||
session,
|
session,
|
||||||
`docker rm ${forceFlag}${containerId}`,
|
`docker rm ${forceFlag}${containerId}`,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
);
|
);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
@@ -2604,7 +2836,7 @@ app.delete(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to remove container", error, {
|
sshLogger.error("Failed to remove container", error, {
|
||||||
operation: "remove_container",
|
operation: "remove_container",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2706,7 +2938,13 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
|
|||||||
command += ` --until ${until}`;
|
command += ` --until ${until}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = await executeDockerCommand(session, command);
|
const logs = await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
command,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
|
|
||||||
session.activeOperations--;
|
session.activeOperations--;
|
||||||
|
|
||||||
@@ -2726,7 +2964,7 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to get container logs", error, {
|
sshLogger.error("Failed to get container logs", error, {
|
||||||
operation: "get_logs",
|
operation: "get_logs",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2793,7 +3031,13 @@ app.get(
|
|||||||
try {
|
try {
|
||||||
const command = `docker stats ${containerId} --no-stream --format '{"cpu":"{{.CPUPerc}}","memory":"{{.MemUsage}}","memoryPercent":"{{.MemPerc}}","netIO":"{{.NetIO}}","blockIO":"{{.BlockIO}}","pids":"{{.PIDs}}"}'`;
|
const command = `docker stats ${containerId} --no-stream --format '{"cpu":"{{.CPUPerc}}","memory":"{{.MemUsage}}","memoryPercent":"{{.MemPerc}}","netIO":"{{.NetIO}}","blockIO":"{{.BlockIO}}","pids":"{{.PIDs}}"}'`;
|
||||||
|
|
||||||
const output = await executeDockerCommand(session, command);
|
const output = await executeDockerCommand(
|
||||||
|
session,
|
||||||
|
command,
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
session.hostId,
|
||||||
|
);
|
||||||
const rawStats = JSON.parse(output.trim());
|
const rawStats = JSON.parse(output.trim());
|
||||||
|
|
||||||
const memoryParts = rawStats.memory.split(" / ");
|
const memoryParts = rawStats.memory.split(" / ");
|
||||||
@@ -2835,7 +3079,7 @@ app.get(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerLogger.error("Failed to get container stats", error, {
|
sshLogger.error("Failed to get container stats", error, {
|
||||||
operation: "get_stats",
|
operation: "get_stats",
|
||||||
sessionId,
|
sessionId,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -2856,7 +3100,7 @@ app.listen(PORT, async () => {
|
|||||||
try {
|
try {
|
||||||
await authManager.initialize();
|
await authManager.initialize();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dockerLogger.error("Failed to initialize Docker backend", err, {
|
sshLogger.error("Failed to initialize Docker backend", err, {
|
||||||
operation: "startup",
|
operation: "startup",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { AuthManager } from "../utils/auth-manager.js";
|
|||||||
import type { AuthenticatedRequest } from "../../types/index.js";
|
import type { AuthenticatedRequest } from "../../types/index.js";
|
||||||
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
|
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
|
||||||
|
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
|
||||||
|
|
||||||
function createConnectionLog(
|
function createConnectionLog(
|
||||||
type: "info" | "success" | "warning" | "error",
|
type: "info" | "success" | "warning" | "error",
|
||||||
@@ -118,12 +119,7 @@ app.use(
|
|||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
if (!origin) return callback(null, true);
|
if (!origin) return callback(null, true);
|
||||||
|
|
||||||
const allowedOrigins = [
|
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
||||||
"http://localhost:5173",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:5173",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (origin.startsWith("https://")) {
|
if (origin.startsWith("https://")) {
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
@@ -244,6 +240,15 @@ async function createJumpHostChain(
|
|||||||
const jumpClient = new SSHClient();
|
const jumpClient = new SSHClient();
|
||||||
clients.push(jumpClient);
|
clients.push(jumpClient);
|
||||||
|
|
||||||
|
const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier(
|
||||||
|
jumpHostConfig.id,
|
||||||
|
jumpHostConfig.ip,
|
||||||
|
jumpHostConfig.port || 22,
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const connected = await new Promise<boolean>((resolve) => {
|
const connected = await new Promise<boolean>((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
@@ -270,6 +275,7 @@ async function createJumpHostChain(
|
|||||||
username: jumpHostConfig.username,
|
username: jumpHostConfig.username,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
readyTimeout: 30000,
|
readyTimeout: 30000,
|
||||||
|
hostVerifier: jumpHostVerifier,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
||||||
@@ -785,6 +791,14 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
readyTimeout: 60000,
|
readyTimeout: 60000,
|
||||||
tcpKeepAlive: true,
|
tcpKeepAlive: true,
|
||||||
tcpKeepAliveInitialDelay: 30000,
|
tcpKeepAliveInitialDelay: 30000,
|
||||||
|
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
false,
|
||||||
|
),
|
||||||
env: {
|
env: {
|
||||||
TERM: "xterm-256color",
|
TERM: "xterm-256color",
|
||||||
LANG: "en_US.UTF-8",
|
LANG: "en_US.UTF-8",
|
||||||
@@ -910,6 +924,92 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
connectionLogs.push(
|
connectionLogs.push(
|
||||||
createConnectionLog("info", "sftp_auth", "Using password authentication"),
|
createConnectionLog("info", "sftp_auth", "Using password authentication"),
|
||||||
);
|
);
|
||||||
|
} else if (resolvedCredentials.authType === "opkssh") {
|
||||||
|
try {
|
||||||
|
const { getOPKSSHToken } = await import("./opkssh-auth.js");
|
||||||
|
const token = await getOPKSSHToken(userId, hostId);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"error",
|
||||||
|
"sftp_auth",
|
||||||
|
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return res.status(401).json({
|
||||||
|
error:
|
||||||
|
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.",
|
||||||
|
requiresOPKSSHAuth: true,
|
||||||
|
connectionLogs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { promises: fs } = await import("fs");
|
||||||
|
const path = await import("path");
|
||||||
|
const os = await import("os");
|
||||||
|
|
||||||
|
const tempDir = os.tmpdir();
|
||||||
|
const keyPath = path.join(tempDir, `opkssh-fm-${userId}-${hostId}`);
|
||||||
|
const certPath = `${keyPath}-cert.pub`;
|
||||||
|
|
||||||
|
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
|
||||||
|
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
|
||||||
|
|
||||||
|
config.privateKey = await fs.readFile(keyPath);
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"info",
|
||||||
|
"sftp_auth",
|
||||||
|
"Using OPKSSH certificate authentication",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const cleanupResults = await Promise.allSettled([
|
||||||
|
fs.unlink(keyPath),
|
||||||
|
fs.unlink(certPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cleanupResults.forEach((result, index) => {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
fileLogger.warn(`Failed to cleanup OPKSSH temp file`, {
|
||||||
|
operation: "opkssh_temp_cleanup_failed",
|
||||||
|
file: index === 0 ? "keyPath" : "certPath",
|
||||||
|
sessionId,
|
||||||
|
error: result.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
fileLogger.error("Failed to cleanup OPKSSH temp files", {
|
||||||
|
operation: "opkssh_temp_cleanup_error",
|
||||||
|
sessionId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
} catch (opksshError) {
|
||||||
|
fileLogger.error("OPKSSH authentication error for file manager", {
|
||||||
|
operation: "file_connect",
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
error:
|
||||||
|
opksshError instanceof Error ? opksshError.message : "Unknown error",
|
||||||
|
});
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"error",
|
||||||
|
"sftp_auth",
|
||||||
|
`OPKSSH authentication failed: ${opksshError instanceof Error ? opksshError.message : "Unknown error"}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "OPKSSH authentication failed",
|
||||||
|
connectionLogs,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (resolvedCredentials.authType === "none") {
|
} else if (resolvedCredentials.authType === "none") {
|
||||||
connectionLogs.push(
|
connectionLogs.push(
|
||||||
createConnectionLog(
|
createConnectionLog(
|
||||||
@@ -968,6 +1068,15 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
client.on("ready", () => {
|
client.on("ready", () => {
|
||||||
if (responseSent) return;
|
if (responseSent) return;
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
|
fileLogger.info("File manager SSH connection established", {
|
||||||
|
operation: "file_ssh_connected",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
});
|
||||||
connectionLogs.push(
|
connectionLogs.push(
|
||||||
createConnectionLog(
|
createConnectionLog(
|
||||||
"success",
|
"success",
|
||||||
@@ -1104,6 +1213,16 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
{ errorCode: (err as any).code, errorLevel: (err as any).level },
|
{ errorCode: (err as any).code, errorLevel: (err as any).level },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else if (err.message.includes("verification failed")) {
|
||||||
|
errorStage = "handshake";
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"error",
|
||||||
|
errorStage,
|
||||||
|
`SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`,
|
||||||
|
{ errorCode: (err as any).code, errorLevel: (err as any).level },
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
connectionLogs.push(
|
connectionLogs.push(
|
||||||
createConnectionLog(
|
createConnectionLog(
|
||||||
@@ -1133,6 +1252,12 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.on("close", () => {
|
client.on("close", () => {
|
||||||
|
fileLogger.info("File manager SSH connection closed", {
|
||||||
|
operation: "file_ssh_disconnected",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
});
|
||||||
if (sshSessions[sessionId]) sshSessions[sessionId].isConnected = false;
|
if (sshSessions[sessionId]) sshSessions[sessionId].isConnected = false;
|
||||||
cleanupSession(sessionId);
|
cleanupSession(sessionId);
|
||||||
});
|
});
|
||||||
@@ -1892,6 +2017,12 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
|
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
|
||||||
const { sessionId } = req.body;
|
const { sessionId } = req.body;
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
fileLogger.info("File manager disconnection requested", {
|
||||||
|
operation: "file_disconnect_request",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
cleanupSession(sessionId);
|
cleanupSession(sessionId);
|
||||||
res.json({ status: "success", message: "SSH connection disconnected" });
|
res.json({ status: "success", message: "SSH connection disconnected" });
|
||||||
});
|
});
|
||||||
@@ -2037,6 +2168,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
|
|||||||
const sessionId = req.query.sessionId as string;
|
const sessionId = req.query.sessionId as string;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
const sshPath = decodeURIComponent((req.query.path as string) || "/");
|
const sshPath = decodeURIComponent((req.query.path as string) || "/");
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return res.status(400).json({ error: "Session ID is required" });
|
return res.status(400).json({ error: "Session ID is required" });
|
||||||
@@ -2048,9 +2180,14 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
|
|||||||
|
|
||||||
sshConn.lastActive = Date.now();
|
sshConn.lastActive = Date.now();
|
||||||
sshConn.activeOperations++;
|
sshConn.activeOperations++;
|
||||||
|
|
||||||
const trySFTP = () => {
|
const trySFTP = () => {
|
||||||
try {
|
try {
|
||||||
|
fileLogger.info("Opening SFTP channel", {
|
||||||
|
operation: "file_sftp_open",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: sshPath,
|
||||||
|
});
|
||||||
sshConn.client.sftp((err, sftp) => {
|
sshConn.client.sftp((err, sftp) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
@@ -2062,6 +2199,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
|
|||||||
|
|
||||||
sftp.readdir(sshPath, (readdirErr, list) => {
|
sftp.readdir(sshPath, (readdirErr, list) => {
|
||||||
if (readdirErr) {
|
if (readdirErr) {
|
||||||
|
sftp.end();
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
`SFTP readdir failed, trying fallback: ${readdirErr.message}`,
|
`SFTP readdir failed, trying fallback: ${readdirErr.message}`,
|
||||||
);
|
);
|
||||||
@@ -2115,11 +2253,24 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (symlinks.length === 0) {
|
if (symlinks.length === 0) {
|
||||||
|
sftp.end();
|
||||||
sshConn.activeOperations--;
|
sshConn.activeOperations--;
|
||||||
return res.json({ files, path: sshPath });
|
return res.json({ files, path: sshPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolved = 0;
|
let resolved = 0;
|
||||||
|
let responded = false;
|
||||||
|
|
||||||
|
const sendResponse = () => {
|
||||||
|
if (responded) return;
|
||||||
|
responded = true;
|
||||||
|
sftp.end();
|
||||||
|
sshConn.activeOperations--;
|
||||||
|
res.json({ files, path: sshPath });
|
||||||
|
};
|
||||||
|
|
||||||
|
const readlinkTimeout = setTimeout(sendResponse, 5000);
|
||||||
|
|
||||||
for (const link of symlinks) {
|
for (const link of symlinks) {
|
||||||
sftp.readlink(link.path, (linkErr, target) => {
|
sftp.readlink(link.path, (linkErr, target) => {
|
||||||
resolved++;
|
resolved++;
|
||||||
@@ -2127,8 +2278,8 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
|
|||||||
files[link.index].linkTarget = target;
|
files[link.index].linkTarget = target;
|
||||||
}
|
}
|
||||||
if (resolved === symlinks.length) {
|
if (resolved === symlinks.length) {
|
||||||
sshConn.activeOperations--;
|
clearTimeout(readlinkTimeout);
|
||||||
res.json({ files, path: sshPath });
|
sendResponse();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2502,6 +2653,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
|
|||||||
const sessionId = req.query.sessionId as string;
|
const sessionId = req.query.sessionId as string;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
const filePath = decodeURIComponent(req.query.path as string);
|
const filePath = decodeURIComponent(req.query.path as string);
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return res.status(400).json({ error: "Session ID is required" });
|
return res.status(400).json({ error: "Session ID is required" });
|
||||||
@@ -2515,6 +2667,12 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
|
|||||||
return res.status(400).json({ error: "File path is required" });
|
return res.status(400).json({ error: "File path is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileLogger.info("Reading file", {
|
||||||
|
operation: "file_read",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: filePath,
|
||||||
|
});
|
||||||
sshConn.lastActive = Date.now();
|
sshConn.lastActive = Date.now();
|
||||||
|
|
||||||
const MAX_READ_SIZE = 500 * 1024 * 1024;
|
const MAX_READ_SIZE = 500 * 1024 * 1024;
|
||||||
@@ -2613,6 +2771,13 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isBinary = detectBinary(binaryData);
|
const isBinary = detectBinary(binaryData);
|
||||||
|
fileLogger.success("File read successfully", {
|
||||||
|
operation: "file_read_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: filePath,
|
||||||
|
bytes: binaryData.length,
|
||||||
|
});
|
||||||
|
|
||||||
if (isBinary) {
|
if (isBinary) {
|
||||||
const base64Content = binaryData.toString("base64");
|
const base64Content = binaryData.toString("base64");
|
||||||
@@ -2668,6 +2833,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
|
|||||||
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
||||||
const { sessionId, path: filePath, content } = req.body;
|
const { sessionId, path: filePath, content } = req.body;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return res.status(400).json({ error: "Session ID is required" });
|
return res.status(400).json({ error: "Session ID is required" });
|
||||||
@@ -2685,10 +2851,25 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
|||||||
return res.status(400).json({ error: "File content is required" });
|
return res.status(400).json({ error: "File content is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentLength =
|
||||||
|
typeof content === "string" ? content.length : Buffer.byteLength(content);
|
||||||
|
fileLogger.info("Writing file", {
|
||||||
|
operation: "file_write",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: filePath,
|
||||||
|
bytes: contentLength,
|
||||||
|
});
|
||||||
sshConn.lastActive = Date.now();
|
sshConn.lastActive = Date.now();
|
||||||
|
|
||||||
const trySFTP = () => {
|
const trySFTP = () => {
|
||||||
try {
|
try {
|
||||||
|
fileLogger.info("Opening SFTP channel", {
|
||||||
|
operation: "file_sftp_open",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: filePath,
|
||||||
|
});
|
||||||
sshConn.client.sftp((err, sftp) => {
|
sshConn.client.sftp((err, sftp) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
@@ -2734,6 +2915,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
|||||||
writeStream.on("error", (streamErr) => {
|
writeStream.on("error", (streamErr) => {
|
||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasError = true;
|
hasError = true;
|
||||||
|
sftp.end();
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
||||||
);
|
);
|
||||||
@@ -2743,6 +2925,14 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
|||||||
writeStream.on("finish", () => {
|
writeStream.on("finish", () => {
|
||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasFinished = true;
|
hasFinished = true;
|
||||||
|
sftp.end();
|
||||||
|
fileLogger.success("File written successfully", {
|
||||||
|
operation: "file_write_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: filePath,
|
||||||
|
bytes: fileBuffer.length,
|
||||||
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({
|
res.json({
|
||||||
message: "File written successfully",
|
message: "File written successfully",
|
||||||
@@ -2755,6 +2945,14 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
|||||||
writeStream.on("close", () => {
|
writeStream.on("close", () => {
|
||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasFinished = true;
|
hasFinished = true;
|
||||||
|
sftp.end();
|
||||||
|
fileLogger.success("File written successfully", {
|
||||||
|
operation: "file_write_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: filePath,
|
||||||
|
bytes: fileBuffer.length,
|
||||||
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({
|
res.json({
|
||||||
message: "File written successfully",
|
message: "File written successfully",
|
||||||
@@ -2913,6 +3111,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
|||||||
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
||||||
const { sessionId, path: filePath, content, fileName } = req.body;
|
const { sessionId, path: filePath, content, fileName } = req.body;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return res.status(400).json({ error: "Session ID is required" });
|
return res.status(400).json({ error: "Session ID is required" });
|
||||||
@@ -2938,9 +3137,23 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
|||||||
const fullPath = filePath.endsWith("/")
|
const fullPath = filePath.endsWith("/")
|
||||||
? filePath + fileName
|
? filePath + fileName
|
||||||
: filePath + "/" + fileName;
|
: filePath + "/" + fileName;
|
||||||
|
const uploadStartTime = Date.now();
|
||||||
|
fileLogger.info("File upload started", {
|
||||||
|
operation: "file_upload_start",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: fullPath,
|
||||||
|
bytes: contentSize,
|
||||||
|
});
|
||||||
|
|
||||||
const trySFTP = () => {
|
const trySFTP = () => {
|
||||||
try {
|
try {
|
||||||
|
fileLogger.info("Opening SFTP channel", {
|
||||||
|
operation: "file_sftp_open",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: fullPath,
|
||||||
|
});
|
||||||
sshConn.client.sftp((err, sftp) => {
|
sshConn.client.sftp((err, sftp) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
@@ -2977,6 +3190,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
|||||||
writeStream.on("error", (streamErr) => {
|
writeStream.on("error", (streamErr) => {
|
||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasError = true;
|
hasError = true;
|
||||||
|
sftp.end();
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
||||||
{
|
{
|
||||||
@@ -2993,6 +3207,15 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
|||||||
writeStream.on("finish", () => {
|
writeStream.on("finish", () => {
|
||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasFinished = true;
|
hasFinished = true;
|
||||||
|
sftp.end();
|
||||||
|
fileLogger.success("File upload completed", {
|
||||||
|
operation: "file_upload_complete",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: fullPath,
|
||||||
|
bytes: fileBuffer.length,
|
||||||
|
duration: Date.now() - uploadStartTime,
|
||||||
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({
|
res.json({
|
||||||
message: "File uploaded successfully",
|
message: "File uploaded successfully",
|
||||||
@@ -3005,6 +3228,15 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
|||||||
writeStream.on("close", () => {
|
writeStream.on("close", () => {
|
||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasFinished = true;
|
hasFinished = true;
|
||||||
|
sftp.end();
|
||||||
|
fileLogger.success("File upload completed", {
|
||||||
|
operation: "file_upload_complete",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: fullPath,
|
||||||
|
bytes: fileBuffer.length,
|
||||||
|
duration: Date.now() - uploadStartTime,
|
||||||
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({
|
res.json({
|
||||||
message: "File uploaded successfully",
|
message: "File uploaded successfully",
|
||||||
@@ -3376,6 +3608,7 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
|
|||||||
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
|
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
|
||||||
const { sessionId, path: folderPath, folderName } = req.body;
|
const { sessionId, path: folderPath, folderName } = req.body;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return res.status(400).json({ error: "Session ID is required" });
|
return res.status(400).json({ error: "Session ID is required" });
|
||||||
@@ -3394,6 +3627,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
|
|||||||
const fullPath = folderPath.endsWith("/")
|
const fullPath = folderPath.endsWith("/")
|
||||||
? folderPath + folderName
|
? folderPath + folderName
|
||||||
: folderPath + "/" + folderName;
|
: folderPath + "/" + folderName;
|
||||||
|
fileLogger.info("Creating directory", {
|
||||||
|
operation: "file_mkdir",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: fullPath,
|
||||||
|
});
|
||||||
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
||||||
|
|
||||||
const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`;
|
const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`;
|
||||||
@@ -3430,6 +3669,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
|
|||||||
|
|
||||||
stream.on("close", (code) => {
|
stream.on("close", (code) => {
|
||||||
if (outputData.includes("SUCCESS")) {
|
if (outputData.includes("SUCCESS")) {
|
||||||
|
fileLogger.success("Directory created successfully", {
|
||||||
|
operation: "file_mkdir_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: fullPath,
|
||||||
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({
|
res.json({
|
||||||
message: "Folder created successfully",
|
message: "Folder created successfully",
|
||||||
@@ -3456,6 +3701,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileLogger.success("Directory created successfully", {
|
||||||
|
operation: "file_mkdir_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: fullPath,
|
||||||
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({
|
res.json({
|
||||||
message: "Folder created successfully",
|
message: "Folder created successfully",
|
||||||
@@ -3508,6 +3759,7 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
|
|||||||
app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
|
app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
|
||||||
const { sessionId, path: itemPath, isDirectory } = req.body;
|
const { sessionId, path: itemPath, isDirectory } = req.body;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return res.status(400).json({ error: "Session ID is required" });
|
return res.status(400).json({ error: "Session ID is required" });
|
||||||
@@ -3521,6 +3773,13 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
|
|||||||
return res.status(400).json({ error: "Item path is required" });
|
return res.status(400).json({ error: "Item path is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileLogger.info("Deleting item", {
|
||||||
|
operation: "file_delete",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: itemPath,
|
||||||
|
type: isDirectory ? "directory" : "file",
|
||||||
|
});
|
||||||
sshConn.lastActive = Date.now();
|
sshConn.lastActive = Date.now();
|
||||||
const escapedPath = itemPath.replace(/'/g, "'\"'\"'");
|
const escapedPath = itemPath.replace(/'/g, "'\"'\"'");
|
||||||
|
|
||||||
@@ -3598,6 +3857,12 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (outputData.includes("SUCCESS") || code === 0) {
|
if (outputData.includes("SUCCESS") || code === 0) {
|
||||||
|
fileLogger.success("Item deleted successfully", {
|
||||||
|
operation: "file_delete_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: itemPath,
|
||||||
|
});
|
||||||
res.json({
|
res.json({
|
||||||
message: "Item deleted successfully",
|
message: "Item deleted successfully",
|
||||||
path: itemPath,
|
path: itemPath,
|
||||||
@@ -3663,6 +3928,7 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
|
|||||||
app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
|
app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
|
||||||
const { sessionId, oldPath, newName } = req.body;
|
const { sessionId, oldPath, newName } = req.body;
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return res.status(400).json({ error: "Session ID is required" });
|
return res.status(400).json({ error: "Session ID is required" });
|
||||||
@@ -3682,6 +3948,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
|
|||||||
|
|
||||||
const oldDir = oldPath.substring(0, oldPath.lastIndexOf("/") + 1);
|
const oldDir = oldPath.substring(0, oldPath.lastIndexOf("/") + 1);
|
||||||
const newPath = oldDir + newName;
|
const newPath = oldDir + newName;
|
||||||
|
fileLogger.info("Renaming item", {
|
||||||
|
operation: "file_rename",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
from: oldPath,
|
||||||
|
to: newPath,
|
||||||
|
});
|
||||||
const escapedOldPath = oldPath.replace(/'/g, "'\"'\"'");
|
const escapedOldPath = oldPath.replace(/'/g, "'\"'\"'");
|
||||||
const escapedNewPath = newPath.replace(/'/g, "'\"'\"'");
|
const escapedNewPath = newPath.replace(/'/g, "'\"'\"'");
|
||||||
|
|
||||||
@@ -3719,6 +3992,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
|
|||||||
|
|
||||||
stream.on("close", (code) => {
|
stream.on("close", (code) => {
|
||||||
if (outputData.includes("SUCCESS")) {
|
if (outputData.includes("SUCCESS")) {
|
||||||
|
fileLogger.success("Item renamed successfully", {
|
||||||
|
operation: "file_rename_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
from: oldPath,
|
||||||
|
to: newPath,
|
||||||
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({
|
res.json({
|
||||||
message: "Item renamed successfully",
|
message: "Item renamed successfully",
|
||||||
@@ -3746,6 +4026,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileLogger.success("Item renamed successfully", {
|
||||||
|
operation: "file_rename_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
from: oldPath,
|
||||||
|
to: newPath,
|
||||||
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({
|
res.json({
|
||||||
message: "Item renamed successfully",
|
message: "Item renamed successfully",
|
||||||
@@ -3959,6 +4246,7 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
|
app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
|
||||||
const { sessionId, path: filePath, hostId, userId } = req.body;
|
const { sessionId, path: filePath, hostId, userId } = req.body;
|
||||||
|
const downloadStartTime = Date.now();
|
||||||
|
|
||||||
if (!sessionId || !filePath) {
|
if (!sessionId || !filePath) {
|
||||||
fileLogger.warn("Missing download parameters", {
|
fileLogger.warn("Missing download parameters", {
|
||||||
@@ -3969,6 +4257,13 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
|
|||||||
return res.status(400).json({ error: "Missing download parameters" });
|
return res.status(400).json({ error: "Missing download parameters" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileLogger.info("File download started", {
|
||||||
|
operation: "file_download_start",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: filePath,
|
||||||
|
});
|
||||||
|
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
if (!sshConn || !sshConn.isConnected) {
|
if (!sshConn || !sshConn.isConnected) {
|
||||||
fileLogger.warn("SSH session not found or not connected for download", {
|
fileLogger.warn("SSH session not found or not connected for download", {
|
||||||
@@ -3983,6 +4278,12 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
|
|||||||
|
|
||||||
sshConn.lastActive = Date.now();
|
sshConn.lastActive = Date.now();
|
||||||
scheduleSessionCleanup(sessionId);
|
scheduleSessionCleanup(sessionId);
|
||||||
|
fileLogger.info("Opening SFTP channel", {
|
||||||
|
operation: "file_sftp_open",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
path: filePath,
|
||||||
|
});
|
||||||
|
|
||||||
sshConn.client.sftp((err, sftp) => {
|
sshConn.client.sftp((err, sftp) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -3992,6 +4293,7 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
|
|||||||
|
|
||||||
sftp.stat(filePath, (statErr, stats) => {
|
sftp.stat(filePath, (statErr, stats) => {
|
||||||
if (statErr) {
|
if (statErr) {
|
||||||
|
sftp.end();
|
||||||
fileLogger.error("File stat failed for download:", statErr);
|
fileLogger.error("File stat failed for download:", statErr);
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
@@ -4027,23 +4329,24 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
|
|||||||
|
|
||||||
sftp.readFile(filePath, (readErr, data) => {
|
sftp.readFile(filePath, (readErr, data) => {
|
||||||
if (readErr) {
|
if (readErr) {
|
||||||
|
sftp.end();
|
||||||
fileLogger.error("File read failed for download:", readErr);
|
fileLogger.error("File read failed for download:", readErr);
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
.json({ error: `Failed to read file: ${readErr.message}` });
|
.json({ error: `Failed to read file: ${readErr.message}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sftp.end();
|
||||||
const base64Content = data.toString("base64");
|
const base64Content = data.toString("base64");
|
||||||
const fileName = filePath.split("/").pop() || "download";
|
const fileName = filePath.split("/").pop() || "download";
|
||||||
|
fileLogger.success("File download completed", {
|
||||||
fileLogger.success("File downloaded successfully", {
|
operation: "file_download_complete",
|
||||||
operation: "file_download",
|
|
||||||
sessionId,
|
sessionId,
|
||||||
filePath,
|
|
||||||
fileName,
|
|
||||||
fileSize: stats.size,
|
|
||||||
hostId,
|
|
||||||
userId,
|
userId,
|
||||||
|
hostId,
|
||||||
|
path: filePath,
|
||||||
|
bytes: stats.size,
|
||||||
|
duration: Date.now() - downloadStartTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -0,0 +1,471 @@
|
|||||||
|
import type { WebSocket } from "ws";
|
||||||
|
import { db } from "../database/db/index.js";
|
||||||
|
import { sshData } from "../database/db/schema.js";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { sshLogger } from "../utils/logger.js";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
interface HostKeyVerificationData {
|
||||||
|
scenario: "new" | "changed";
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
hostname?: string;
|
||||||
|
fingerprint: string;
|
||||||
|
oldFingerprint?: string;
|
||||||
|
keyType: string;
|
||||||
|
oldKeyType?: string;
|
||||||
|
algorithm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VerificationResponse {
|
||||||
|
action: "accept" | "reject";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SSHHostKeyVerifier {
|
||||||
|
/**
|
||||||
|
* Creates a hostVerifier callback for ssh2 Client.connect()
|
||||||
|
*
|
||||||
|
* @param hostId - Database ID of the host (null for quick connect)
|
||||||
|
* @param ip - IP address or hostname
|
||||||
|
* @param port - SSH port
|
||||||
|
* @param ws - WebSocket for user prompts (null for non-interactive connections)
|
||||||
|
* @param userId - User ID for logging
|
||||||
|
* @param isJumpHost - If true, auto-accepts without prompting
|
||||||
|
* @returns async hostVerifier callback
|
||||||
|
*/
|
||||||
|
static async createHostVerifier(
|
||||||
|
hostId: number | null,
|
||||||
|
ip: string,
|
||||||
|
port: number,
|
||||||
|
ws: WebSocket | null,
|
||||||
|
userId: string,
|
||||||
|
isJumpHost: boolean = false,
|
||||||
|
): Promise<(hostkey: Buffer, verify: (valid: boolean) => void) => void> {
|
||||||
|
return (hostkey: Buffer, verify: (valid: boolean) => void): void => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fingerprint = hostkey.toString("hex");
|
||||||
|
const keyType = this.getKeyType(hostkey);
|
||||||
|
const algorithm = "sha256";
|
||||||
|
|
||||||
|
if (!hostId) {
|
||||||
|
sshLogger.info(
|
||||||
|
"Host key verification skipped (no hostId - quick connect)",
|
||||||
|
{
|
||||||
|
operation: "host_key_skip",
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
verify(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = await db.query.sshData.findFirst({
|
||||||
|
where: eq(sshData.id, hostId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
sshLogger.warn(
|
||||||
|
"Host not found in database during key verification",
|
||||||
|
{
|
||||||
|
operation: "host_key_no_host",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
verify(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!host.hostKeyFingerprint) {
|
||||||
|
if (isJumpHost) {
|
||||||
|
await this.storeHostKey(hostId, fingerprint, keyType, algorithm);
|
||||||
|
sshLogger.info("Jump host key auto-accepted and stored", {
|
||||||
|
operation: "host_key_stored",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
userId,
|
||||||
|
isJumpHost: true,
|
||||||
|
});
|
||||||
|
verify(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ws) {
|
||||||
|
sshLogger.warn(
|
||||||
|
"No WebSocket available for host key verification prompt",
|
||||||
|
{
|
||||||
|
operation: "host_key_no_ws",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
verify(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accepted = await this.promptUserForNewKey(
|
||||||
|
ws,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
host.name || undefined,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
algorithm,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accepted) {
|
||||||
|
await this.storeHostKey(hostId, fingerprint, keyType, algorithm);
|
||||||
|
sshLogger.info("New host key accepted by user and stored", {
|
||||||
|
operation: "host_key_stored",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sshLogger.warn("User rejected new host key", {
|
||||||
|
operation: "host_key_rejected",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(accepted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.hostKeyFingerprint === fingerprint) {
|
||||||
|
await db
|
||||||
|
.update(sshData)
|
||||||
|
.set({
|
||||||
|
hostKeyLastVerified: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where(eq(sshData.id, hostId));
|
||||||
|
|
||||||
|
sshLogger.info("Host key verified successfully", {
|
||||||
|
operation: "host_key_verified",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
verify(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sshLogger.error("Host key mismatch detected - SECURITY WARNING", {
|
||||||
|
operation: "host_key_mismatch",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
oldFingerprint: host.hostKeyFingerprint,
|
||||||
|
newFingerprint: fingerprint,
|
||||||
|
oldKeyType: host.hostKeyType,
|
||||||
|
newKeyType: keyType,
|
||||||
|
userId,
|
||||||
|
changeCount: host.hostKeyChangedCount || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isJumpHost) {
|
||||||
|
await this.updateHostKey(
|
||||||
|
hostId,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
algorithm,
|
||||||
|
host.hostKeyChangedCount || 0,
|
||||||
|
);
|
||||||
|
sshLogger.warn("Jump host key changed - auto-accepted", {
|
||||||
|
operation: "host_key_updated",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
userId,
|
||||||
|
isJumpHost: true,
|
||||||
|
});
|
||||||
|
verify(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ws) {
|
||||||
|
sshLogger.error(
|
||||||
|
"Host key changed - please connect via Terminal to verify the new key",
|
||||||
|
{
|
||||||
|
operation: "host_key_no_ws_reject",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
userId,
|
||||||
|
message:
|
||||||
|
"SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
verify(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accepted = await this.promptUserForChangedKey(
|
||||||
|
ws,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
host.name || undefined,
|
||||||
|
fingerprint,
|
||||||
|
host.hostKeyFingerprint,
|
||||||
|
keyType,
|
||||||
|
host.hostKeyType || "unknown",
|
||||||
|
algorithm,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accepted) {
|
||||||
|
await this.updateHostKey(
|
||||||
|
hostId,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
algorithm,
|
||||||
|
host.hostKeyChangedCount || 0,
|
||||||
|
);
|
||||||
|
sshLogger.warn("Changed host key accepted by user", {
|
||||||
|
operation: "host_key_updated",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
oldFingerprint: host.hostKeyFingerprint,
|
||||||
|
newFingerprint: fingerprint,
|
||||||
|
userId,
|
||||||
|
changeCount: (host.hostKeyChangedCount || 0) + 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sshLogger.error("User rejected changed host key", {
|
||||||
|
operation: "host_key_change_rejected",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(accepted);
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("Error in host key verification", error, {
|
||||||
|
operation: "host_key_error",
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
verify(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async storeHostKey(
|
||||||
|
hostId: number,
|
||||||
|
fingerprint: string,
|
||||||
|
keyType: string,
|
||||||
|
algorithm: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await db
|
||||||
|
.update(sshData)
|
||||||
|
.set({
|
||||||
|
hostKeyFingerprint: fingerprint,
|
||||||
|
hostKeyType: keyType,
|
||||||
|
hostKeyAlgorithm: algorithm,
|
||||||
|
hostKeyFirstSeen: new Date().toISOString(),
|
||||||
|
hostKeyLastVerified: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where(eq(sshData.id, hostId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async updateHostKey(
|
||||||
|
hostId: number,
|
||||||
|
fingerprint: string,
|
||||||
|
keyType: string,
|
||||||
|
algorithm: string,
|
||||||
|
currentChangeCount: number,
|
||||||
|
): Promise<void> {
|
||||||
|
await db
|
||||||
|
.update(sshData)
|
||||||
|
.set({
|
||||||
|
hostKeyFingerprint: fingerprint,
|
||||||
|
hostKeyType: keyType,
|
||||||
|
hostKeyAlgorithm: algorithm,
|
||||||
|
hostKeyLastVerified: new Date().toISOString(),
|
||||||
|
hostKeyChangedCount: currentChangeCount + 1,
|
||||||
|
})
|
||||||
|
.where(eq(sshData.id, hostId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async promptUserForNewKey(
|
||||||
|
ws: WebSocket,
|
||||||
|
ip: string,
|
||||||
|
port: number,
|
||||||
|
hostname: string | undefined,
|
||||||
|
fingerprint: string,
|
||||||
|
keyType: string,
|
||||||
|
algorithm: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ws.removeListener("message", messageHandler);
|
||||||
|
sshLogger.warn("Host key verification timeout (new key)", {
|
||||||
|
operation: "host_key_timeout",
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
});
|
||||||
|
resolve(false);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
const messageHandler = (data: Buffer) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data.toString());
|
||||||
|
|
||||||
|
if (message.type === "host_key_verification_response") {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.removeListener("message", messageHandler);
|
||||||
|
|
||||||
|
const response = message.data as VerificationResponse;
|
||||||
|
resolve(response.action === "accept");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error(
|
||||||
|
"Error parsing host key verification response",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.on("message", messageHandler);
|
||||||
|
|
||||||
|
const verificationData: HostKeyVerificationData = {
|
||||||
|
scenario: "new",
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
fingerprint,
|
||||||
|
keyType,
|
||||||
|
algorithm,
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "host_key_verification_required",
|
||||||
|
data: verificationData,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async promptUserForChangedKey(
|
||||||
|
ws: WebSocket,
|
||||||
|
ip: string,
|
||||||
|
port: number,
|
||||||
|
hostname: string | undefined,
|
||||||
|
fingerprint: string,
|
||||||
|
oldFingerprint: string,
|
||||||
|
keyType: string,
|
||||||
|
oldKeyType: string,
|
||||||
|
algorithm: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ws.removeListener("message", messageHandler);
|
||||||
|
sshLogger.error("Host key verification timeout (changed key)", {
|
||||||
|
operation: "host_key_timeout",
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
});
|
||||||
|
resolve(false);
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
const messageHandler = (data: Buffer) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data.toString());
|
||||||
|
|
||||||
|
if (message.type === "host_key_verification_response") {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.removeListener("message", messageHandler);
|
||||||
|
|
||||||
|
const response = message.data as VerificationResponse;
|
||||||
|
resolve(response.action === "accept");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error(
|
||||||
|
"Error parsing host key verification response",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.on("message", messageHandler);
|
||||||
|
|
||||||
|
const verificationData: HostKeyVerificationData = {
|
||||||
|
scenario: "changed",
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
fingerprint,
|
||||||
|
oldFingerprint,
|
||||||
|
keyType,
|
||||||
|
oldKeyType,
|
||||||
|
algorithm,
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "host_key_changed",
|
||||||
|
data: verificationData,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getKeyType(key: Buffer): string {
|
||||||
|
try {
|
||||||
|
if (key.length < 4) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeLength = key.readUInt32BE(0);
|
||||||
|
if (typeLength > key.length - 4 || typeLength > 256) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyType = key.toString("utf8", 4, 4 + typeLength);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(keyType && keyType.startsWith("ssh-")) ||
|
||||||
|
keyType.startsWith("ecdsa-")
|
||||||
|
) {
|
||||||
|
return keyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("Error parsing SSH key type", error);
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,803 @@
|
|||||||
|
import { spawn, ChildProcess } from "child_process";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { WebSocket } from "ws";
|
||||||
|
import { IncomingMessage } from "http";
|
||||||
|
import { OPKSSHBinaryManager } from "../utils/opkssh-binary-manager.js";
|
||||||
|
import { sshLogger } from "../utils/logger.js";
|
||||||
|
import { getDb } from "../database/db/index.js";
|
||||||
|
import { opksshTokens } from "../database/db/schema.js";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { UserCrypto } from "../utils/user-crypto.js";
|
||||||
|
import { FieldCrypto } from "../utils/field-crypto.js";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const AUTH_TIMEOUT = 60 * 1000;
|
||||||
|
|
||||||
|
interface OPKSSHAuthSession {
|
||||||
|
requestId: string;
|
||||||
|
userId: string;
|
||||||
|
hostId: number;
|
||||||
|
hostname: string;
|
||||||
|
process: ChildProcess;
|
||||||
|
localPort: number;
|
||||||
|
callbackPort: number;
|
||||||
|
remoteRedirectUri: string;
|
||||||
|
status:
|
||||||
|
| "starting"
|
||||||
|
| "waiting_for_auth"
|
||||||
|
| "authenticating"
|
||||||
|
| "completed"
|
||||||
|
| "error";
|
||||||
|
ws: WebSocket;
|
||||||
|
stdoutBuffer: string;
|
||||||
|
privateKeyBuffer: string;
|
||||||
|
sshCertBuffer: string;
|
||||||
|
identity: {
|
||||||
|
email?: string;
|
||||||
|
sub?: string;
|
||||||
|
issuer?: string;
|
||||||
|
audience?: string;
|
||||||
|
};
|
||||||
|
createdAt: Date;
|
||||||
|
approvalTimeout: NodeJS.Timeout;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeAuthSessions = new Map<string, OPKSSHAuthSession>();
|
||||||
|
const cleanupInProgress = new Set<string>();
|
||||||
|
|
||||||
|
export function getRequestOrigin(req: IncomingMessage): string {
|
||||||
|
const protoHeader =
|
||||||
|
req.headers["x-forwarded-proto"] ||
|
||||||
|
((req.socket as any).encrypted ? "https" : "http");
|
||||||
|
const proto =
|
||||||
|
typeof protoHeader === "string"
|
||||||
|
? protoHeader.split(",")[0].trim()
|
||||||
|
: String(protoHeader);
|
||||||
|
|
||||||
|
const portHeader = req.headers["x-forwarded-port"];
|
||||||
|
const port =
|
||||||
|
typeof portHeader === "string"
|
||||||
|
? portHeader.split(",")[0].trim()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const hostHeaderRaw =
|
||||||
|
req.headers["x-forwarded-host"] || req.headers.host || "localhost";
|
||||||
|
const hostHeader =
|
||||||
|
typeof hostHeaderRaw === "string"
|
||||||
|
? hostHeaderRaw.split(",")[0].trim()
|
||||||
|
: String(hostHeaderRaw);
|
||||||
|
|
||||||
|
if (port) {
|
||||||
|
const hostWithoutPort = hostHeader.split(":")[0];
|
||||||
|
const isDefaultPort =
|
||||||
|
(proto === "http" && port === "80") ||
|
||||||
|
(proto === "https" && port === "443");
|
||||||
|
return isDefaultPort
|
||||||
|
? `${proto}://${hostWithoutPort}`
|
||||||
|
: `${proto}://${hostWithoutPort}:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${proto}://${hostHeader}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOPKConfigPath(): string {
|
||||||
|
const dataDir =
|
||||||
|
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
|
||||||
|
return path.join(dataDir, ".opk", "config.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureOPKConfigDir(): Promise<void> {
|
||||||
|
const configPath = getOPKConfigPath();
|
||||||
|
const configDir = path.dirname(configPath);
|
||||||
|
await fs.mkdir(configDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTemplateConfig(): Promise<void> {
|
||||||
|
const configPath = getOPKConfigPath();
|
||||||
|
const template = `
|
||||||
|
# OPKSSH Configuration
|
||||||
|
# OPKSSH Documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md
|
||||||
|
# Termix Documentation: https://docs.termix.site/opkssh
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureOPKConfigDir();
|
||||||
|
await fs.writeFile(configPath, template, "utf8");
|
||||||
|
sshLogger.info(`Created template OPKSSH config at ${configPath}`);
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.warn("Failed to create template OPKSSH config", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkOPKConfigExists(): Promise<{
|
||||||
|
exists: boolean;
|
||||||
|
error?: string;
|
||||||
|
configPath?: string;
|
||||||
|
}> {
|
||||||
|
const configPath = getOPKConfigPath();
|
||||||
|
const isDocker =
|
||||||
|
!!process.env.DATA_DIR && process.env.DATA_DIR.startsWith("/app");
|
||||||
|
const dockerHint = isDocker
|
||||||
|
? "\n\nDocker: Ensure /app/data is mounted as a volume with write permissions for node:node user."
|
||||||
|
: "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(configPath, "utf8");
|
||||||
|
|
||||||
|
if (!content.includes("providers:")) {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
configPath,
|
||||||
|
error: `OPKSSH configuration is missing 'providers' section. Please edit the config file at:\n${configPath}\n\n.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = content.split("\n");
|
||||||
|
|
||||||
|
const hasUncommentedProvider = lines.some((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
return (
|
||||||
|
trimmed.startsWith("- alias:") ||
|
||||||
|
(trimmed.startsWith("issuer:") && !line.trimStart().startsWith("#"))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasUncommentedProvider) {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
configPath,
|
||||||
|
error: `OPKSSH configuration has no active providers. Please edit the config file at:\n${configPath}\n\nUncomment and configure at least one OIDC provider.\nSee documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md${dockerHint}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.includes("redirect_uris:")) {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
configPath,
|
||||||
|
error: `OPKSSH configuration is missing 'redirect_uris' field.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { exists: true, configPath };
|
||||||
|
} catch {
|
||||||
|
await createTemplateConfig();
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
configPath,
|
||||||
|
error: `OPKSSH configuration not found. A template config file has been created at:\n${configPath}\n\nPlease edit this file and configure your OIDC provider (Google, GitHub, Microsoft, etc.).\nSee documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md${dockerHint}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startOPKSSHAuth(
|
||||||
|
userId: string,
|
||||||
|
hostId: number,
|
||||||
|
hostname: string,
|
||||||
|
ws: WebSocket,
|
||||||
|
requestOrigin: string,
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
await ensureOPKConfigDir();
|
||||||
|
const configDir = path.dirname(getOPKConfigPath());
|
||||||
|
await fs.access(configDir, fs.constants.R_OK | fs.constants.W_OK);
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("OPKSSH directory not accessible", error);
|
||||||
|
const isDocker =
|
||||||
|
!!process.env.DATA_DIR && process.env.DATA_DIR.startsWith("/app");
|
||||||
|
const dockerHint = isDocker
|
||||||
|
? "\n\nDocker: Ensure /app/data is mounted as a volume with write permissions for node:node user."
|
||||||
|
: "";
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
error: `OPKSSH directory initialization failed: ${error.message}${dockerHint}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const configCheck = await checkOPKConfigExists();
|
||||||
|
if (!configCheck.exists) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_config_error",
|
||||||
|
requestId: "",
|
||||||
|
error: configCheck.error,
|
||||||
|
instructions: configCheck.error,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = randomUUID();
|
||||||
|
const remoteRedirectUri = `${requestOrigin}/ssh/opkssh-callback`;
|
||||||
|
|
||||||
|
const session: Partial<OPKSSHAuthSession> = {
|
||||||
|
requestId,
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
hostname,
|
||||||
|
localPort: 0,
|
||||||
|
callbackPort: 0,
|
||||||
|
remoteRedirectUri,
|
||||||
|
status: "starting",
|
||||||
|
ws,
|
||||||
|
stdoutBuffer: "",
|
||||||
|
privateKeyBuffer: "",
|
||||||
|
sshCertBuffer: "",
|
||||||
|
identity: {},
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const binaryPath = OPKSSHBinaryManager.getBinaryPath();
|
||||||
|
const configPath = getOPKConfigPath();
|
||||||
|
const configDir = path.dirname(configPath);
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
"login",
|
||||||
|
"--print-key",
|
||||||
|
`--config-path=${configPath}`,
|
||||||
|
`--remote-redirect-uri=${remoteRedirectUri}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const opksshProcess = spawn(binaryPath, args, {
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
session.process = opksshProcess;
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
await cleanupAuthSession(requestId);
|
||||||
|
};
|
||||||
|
session.cleanup = cleanup;
|
||||||
|
|
||||||
|
const timeout = setTimeout(async () => {
|
||||||
|
sshLogger.warn(`OPKSSH auth timeout for session ${requestId}`);
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_timeout",
|
||||||
|
requestId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await cleanup();
|
||||||
|
}, AUTH_TIMEOUT);
|
||||||
|
|
||||||
|
session.approvalTimeout = timeout;
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
activeAuthSessions.set(requestId, session as OPKSSHAuthSession);
|
||||||
|
|
||||||
|
opksshProcess.stdout?.on("data", (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
handleOPKSSHOutput(requestId, output);
|
||||||
|
});
|
||||||
|
|
||||||
|
opksshProcess.stderr?.on("data", async (data) => {
|
||||||
|
const stderr = data.toString();
|
||||||
|
|
||||||
|
if (stderr.includes("Opening browser to")) {
|
||||||
|
handleOPKSSHOutput(requestId, stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr.includes("listening on")) {
|
||||||
|
handleOPKSSHOutput(requestId, stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr.includes("provider not found") || stderr.includes("config")) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_config_error",
|
||||||
|
requestId,
|
||||||
|
error:
|
||||||
|
"OPKSSH configuration error. Please verify your config file contains valid OIDC provider settings.",
|
||||||
|
instructions:
|
||||||
|
"See documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
stderr.includes("level=error") ||
|
||||||
|
stderr.includes("Error:") ||
|
||||||
|
stderr.includes("failed")
|
||||||
|
) {
|
||||||
|
const isXdgOpenError = stderr.includes('exec: "xdg-open"');
|
||||||
|
if (!isXdgOpenError) {
|
||||||
|
if (
|
||||||
|
stderr.includes("bind: address already in use") ||
|
||||||
|
stderr.includes("error logging in") ||
|
||||||
|
stderr.includes("failed to start")
|
||||||
|
) {
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
opksshProcess.on("error", (error) => {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId,
|
||||||
|
error: `OPKSSH process error: ${error.message}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
opksshProcess.on("exit", (code) => {
|
||||||
|
if (code !== 0 && session.status !== "completed") {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId,
|
||||||
|
error: `OPKSSH process exited with code ${code}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
return requestId;
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error(`Failed to start OPKSSH auth session`, error);
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId,
|
||||||
|
error: `Failed to start OPKSSH authentication: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOPKSSHOutput(requestId: string, output: string): void {
|
||||||
|
const session = activeAuthSessions.get(requestId);
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.stdoutBuffer += output;
|
||||||
|
|
||||||
|
const chooserUrlMatch = session.stdoutBuffer.match(
|
||||||
|
/Opening browser to http:\/\/localhost:(\d+)\/chooser/,
|
||||||
|
);
|
||||||
|
if (chooserUrlMatch && session.status === "starting") {
|
||||||
|
const actualPort = parseInt(chooserUrlMatch[1], 10);
|
||||||
|
const localChooserUrl = `http://localhost:${actualPort}/chooser`;
|
||||||
|
|
||||||
|
session.localPort = actualPort;
|
||||||
|
|
||||||
|
const baseUrl = session.remoteRedirectUri.replace(
|
||||||
|
/\/ssh\/opkssh-callback$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const proxiedChooserUrl = `${baseUrl}/ssh/opkssh-chooser/${requestId}`;
|
||||||
|
|
||||||
|
session.status = "waiting_for_auth";
|
||||||
|
session.ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_status",
|
||||||
|
requestId,
|
||||||
|
stage: "chooser",
|
||||||
|
url: proxiedChooserUrl,
|
||||||
|
localUrl: localChooserUrl,
|
||||||
|
message: "Please authenticate in your browser",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const callbackPortMatch = session.stdoutBuffer.match(
|
||||||
|
/listening on http:\/\/127\.0\.0\.1:(\d+)\//,
|
||||||
|
);
|
||||||
|
if (callbackPortMatch && !session.callbackPort) {
|
||||||
|
session.callbackPort = parseInt(callbackPortMatch[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.includes("BEGIN OPENSSH PRIVATE KEY")) {
|
||||||
|
session.status = "authenticating";
|
||||||
|
session.ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_status",
|
||||||
|
requestId,
|
||||||
|
stage: "authenticating",
|
||||||
|
message: "Processing authentication...",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKeyMatch = session.stdoutBuffer.match(
|
||||||
|
/(-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]*?-----END OPENSSH PRIVATE KEY-----)/,
|
||||||
|
);
|
||||||
|
if (privateKeyMatch) {
|
||||||
|
session.privateKeyBuffer = privateKeyMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const certMatch = session.stdoutBuffer.match(
|
||||||
|
/(ecdsa-sha2-nistp256-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+|ssh-rsa-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+|ssh-ed25519-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+)/,
|
||||||
|
);
|
||||||
|
if (certMatch) {
|
||||||
|
session.sshCertBuffer = certMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityMatch = session.stdoutBuffer.match(
|
||||||
|
/Email, sub, issuer, audience:\s*\n?\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/,
|
||||||
|
);
|
||||||
|
if (identityMatch) {
|
||||||
|
session.identity = {
|
||||||
|
email: identityMatch[1],
|
||||||
|
sub: identityMatch[2],
|
||||||
|
issuer: identityMatch[3],
|
||||||
|
audience: identityMatch[4],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.privateKeyBuffer && session.sshCertBuffer) {
|
||||||
|
if (!session.privateKeyBuffer.includes("BEGIN OPENSSH PRIVATE KEY")) {
|
||||||
|
sshLogger.error(`Invalid private key extracted [${requestId}]`, {
|
||||||
|
bufferPrefix: session.privateKeyBuffer.substring(0, 50),
|
||||||
|
});
|
||||||
|
session.ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId,
|
||||||
|
error: "Failed to extract valid private key from OPKSSH output",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.sshCertBuffer.match(/-cert-v01@openssh\.com/)) {
|
||||||
|
sshLogger.error(`Invalid SSH certificate extracted [${requestId}]`, {
|
||||||
|
bufferPrefix: session.sshCertBuffer.substring(0, 50),
|
||||||
|
});
|
||||||
|
session.ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId,
|
||||||
|
error: "Failed to extract valid SSH certificate from OPKSSH output",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
storeOPKSSHToken(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storeOPKSSHToken(session: OPKSSHAuthSession): Promise<void> {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const userCrypto = UserCrypto.getInstance();
|
||||||
|
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setHours(expiresAt.getHours() + 24);
|
||||||
|
|
||||||
|
const userDataKey = userCrypto.getUserDataKey(session.userId);
|
||||||
|
if (!userDataKey) {
|
||||||
|
throw new Error("User data key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenId = `opkssh-${session.userId}-${session.hostId}`;
|
||||||
|
|
||||||
|
const encryptedCert = FieldCrypto.encryptField(
|
||||||
|
session.sshCertBuffer,
|
||||||
|
userDataKey,
|
||||||
|
tokenId,
|
||||||
|
"ssh_cert",
|
||||||
|
);
|
||||||
|
const encryptedKey = FieldCrypto.encryptField(
|
||||||
|
session.privateKeyBuffer,
|
||||||
|
userDataKey,
|
||||||
|
tokenId,
|
||||||
|
"private_key",
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(opksshTokens)
|
||||||
|
.values({
|
||||||
|
userId: session.userId,
|
||||||
|
hostId: session.hostId,
|
||||||
|
sshCert: encryptedCert,
|
||||||
|
privateKey: encryptedKey,
|
||||||
|
email: session.identity.email,
|
||||||
|
sub: session.identity.sub,
|
||||||
|
issuer: session.identity.issuer,
|
||||||
|
audience: session.identity.audience,
|
||||||
|
expiresAt: expiresAt.toISOString(),
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [opksshTokens.userId, opksshTokens.hostId],
|
||||||
|
set: {
|
||||||
|
sshCert: encryptedCert,
|
||||||
|
privateKey: encryptedKey,
|
||||||
|
email: session.identity.email,
|
||||||
|
sub: session.identity.sub,
|
||||||
|
issuer: session.identity.issuer,
|
||||||
|
audience: session.identity.audience,
|
||||||
|
expiresAt: expiresAt.toISOString(),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
session.status = "completed";
|
||||||
|
session.ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_completed",
|
||||||
|
requestId: session.requestId,
|
||||||
|
expiresAt: expiresAt.toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
"http://localhost:30006/activity/log",
|
||||||
|
{
|
||||||
|
type: "opkssh_authentication",
|
||||||
|
hostId: session.hostId,
|
||||||
|
hostName: session.hostname,
|
||||||
|
status: "approved",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.INTERNAL_AUTH_TOKEN}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (activityError) {
|
||||||
|
sshLogger.warn("Failed to log OPKSSH activity", activityError);
|
||||||
|
}
|
||||||
|
|
||||||
|
await session.cleanup();
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error(
|
||||||
|
`Failed to store OPKSSH token for session ${session.requestId}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
session.ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId: session.requestId,
|
||||||
|
error: "Failed to store authentication token",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await session.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOPKSSHToken(
|
||||||
|
userId: string,
|
||||||
|
hostId: number,
|
||||||
|
): Promise<{ sshCert: string; privateKey: string } | null> {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const token = await db
|
||||||
|
.select()
|
||||||
|
.from(opksshTokens)
|
||||||
|
.where(
|
||||||
|
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!token || token.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = token[0];
|
||||||
|
const expiresAt = new Date(tokenData.expiresAt);
|
||||||
|
|
||||||
|
if (expiresAt < new Date()) {
|
||||||
|
await db
|
||||||
|
.delete(opksshTokens)
|
||||||
|
.where(
|
||||||
|
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCrypto = UserCrypto.getInstance();
|
||||||
|
const userDataKey = userCrypto.getUserDataKey(userId);
|
||||||
|
if (!userDataKey) {
|
||||||
|
throw new Error("User data key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenId = `opkssh-${userId}-${hostId}`;
|
||||||
|
const decryptedCert = FieldCrypto.decryptField(
|
||||||
|
tokenData.sshCert,
|
||||||
|
userDataKey,
|
||||||
|
tokenId,
|
||||||
|
"ssh_cert",
|
||||||
|
);
|
||||||
|
const decryptedKey = FieldCrypto.decryptField(
|
||||||
|
tokenData.privateKey,
|
||||||
|
userDataKey,
|
||||||
|
tokenId,
|
||||||
|
"private_key",
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(opksshTokens)
|
||||||
|
.set({ lastUsed: new Date().toISOString() })
|
||||||
|
.where(
|
||||||
|
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sshCert: decryptedCert,
|
||||||
|
privateKey: decryptedKey,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error(`Failed to retrieve OPKSSH token`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteOPKSSHToken(
|
||||||
|
userId: string,
|
||||||
|
hostId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const db = getDb();
|
||||||
|
await db
|
||||||
|
.delete(opksshTokens)
|
||||||
|
.where(
|
||||||
|
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invalidateOPKSSHToken(
|
||||||
|
userId: string,
|
||||||
|
hostId: number,
|
||||||
|
reason: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
await db
|
||||||
|
.delete(opksshTokens)
|
||||||
|
.where(
|
||||||
|
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error(`Failed to invalidate OPKSSH token`, {
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
reason,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleOAuthCallback(
|
||||||
|
requestId: string,
|
||||||
|
queryString: string,
|
||||||
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
|
const session = activeAuthSessions.get(requestId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return { success: false, message: "Invalid authentication session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const callbackUrl = `http://localhost:${session.localPort}/login-callback?${queryString}`;
|
||||||
|
await axios.get(callbackUrl, {
|
||||||
|
timeout: 10000,
|
||||||
|
validateStatus: () => true,
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
} catch {
|
||||||
|
session.ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId,
|
||||||
|
error: "Failed to complete authentication",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await session.cleanup();
|
||||||
|
return { success: false, message: "Authentication failed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupAuthSession(requestId: string): Promise<void> {
|
||||||
|
if (cleanupInProgress.has(requestId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupInProgress.add(requestId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = activeAuthSessions.get(requestId);
|
||||||
|
if (!session) {
|
||||||
|
cleanupInProgress.delete(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.approvalTimeout) {
|
||||||
|
clearTimeout(session.approvalTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.process) {
|
||||||
|
try {
|
||||||
|
session.process.kill("SIGTERM");
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const killTimeout = setTimeout(() => {
|
||||||
|
if (session.process && !session.process.killed) {
|
||||||
|
session.process.kill("SIGKILL");
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
session.process.once("exit", () => {
|
||||||
|
clearTimeout(killTimeout);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
} catch (killError) {
|
||||||
|
sshLogger.warn(
|
||||||
|
`Failed to kill OPKSSH process for session ${requestId}`,
|
||||||
|
killError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeAuthSessions.delete(requestId);
|
||||||
|
} finally {
|
||||||
|
cleanupInProgress.delete(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelAuthSession(requestId: string): void {
|
||||||
|
const session = activeAuthSessions.get(requestId);
|
||||||
|
if (session) {
|
||||||
|
session.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveAuthSession(
|
||||||
|
requestId: string,
|
||||||
|
): OPKSSHAuthSession | undefined {
|
||||||
|
return activeAuthSessions.get(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveSessionsForUser(userId: string): OPKSSHAuthSession[] {
|
||||||
|
const sessions: OPKSSHAuthSession[] = [];
|
||||||
|
for (const session of activeAuthSessions.values()) {
|
||||||
|
if (session.userId === userId) {
|
||||||
|
sessions.push(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveSessionsAll(): OPKSSHAuthSession[] {
|
||||||
|
return Array.from(activeAuthSessions.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserIdFromRequest(req: any): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const { AuthManager } = await import("../utils/auth-manager.js");
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
|
const token =
|
||||||
|
req.cookies?.jwt || req.headers.authorization?.replace("Bearer ", "");
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = await authManager.verifyJWTToken(token);
|
||||||
|
return decoded?.userId || null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import { collectLoginStats } from "./widgets/login-stats-collector.js";
|
|||||||
import { collectPortsMetrics } from "./widgets/ports-collector.js";
|
import { collectPortsMetrics } from "./widgets/ports-collector.js";
|
||||||
import { collectFirewallMetrics } from "./widgets/firewall-collector.js";
|
import { collectFirewallMetrics } from "./widgets/firewall-collector.js";
|
||||||
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
|
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
|
||||||
|
|
||||||
function createConnectionLog(
|
function createConnectionLog(
|
||||||
type: "info" | "success" | "warning" | "error",
|
type: "info" | "success" | "warning" | "error",
|
||||||
@@ -125,6 +126,15 @@ async function createJumpHostChain(
|
|||||||
const jumpClient = new Client();
|
const jumpClient = new Client();
|
||||||
clients.push(jumpClient);
|
clients.push(jumpClient);
|
||||||
|
|
||||||
|
const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier(
|
||||||
|
jumpHostConfig.id,
|
||||||
|
jumpHostConfig.ip,
|
||||||
|
jumpHostConfig.port || 22,
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const connected = await new Promise<boolean>((resolve) => {
|
const connected = await new Promise<boolean>((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
@@ -151,6 +161,7 @@ async function createJumpHostChain(
|
|||||||
username: jumpHostConfig.username,
|
username: jumpHostConfig.username,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
readyTimeout: 30000,
|
readyTimeout: 30000,
|
||||||
|
hostVerifier: jumpHostVerifier,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
||||||
@@ -378,8 +389,8 @@ class SSHConnectionPool {
|
|||||||
private async createConnection(
|
private async createConnection(
|
||||||
host: SSHHostWithCredentials,
|
host: SSHHostWithCredentials,
|
||||||
): Promise<Client> {
|
): Promise<Client> {
|
||||||
|
const config = await buildSshConfig(host);
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const config = buildSshConfig(host);
|
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
client.end();
|
client.end();
|
||||||
@@ -1023,6 +1034,20 @@ class PollingManager {
|
|||||||
const statusOnly = options?.statusOnly ?? false;
|
const statusOnly = options?.statusOnly ?? false;
|
||||||
const viewerUserId = options?.viewerUserId;
|
const viewerUserId = options?.viewerUserId;
|
||||||
|
|
||||||
|
const enabledCollectors: string[] = [];
|
||||||
|
if (statsConfig.statusCheckEnabled) enabledCollectors.push("status");
|
||||||
|
if (!statusOnly && statsConfig.metricsEnabled) {
|
||||||
|
enabledCollectors.push(
|
||||||
|
"cpu",
|
||||||
|
"memory",
|
||||||
|
"disk",
|
||||||
|
"network",
|
||||||
|
"uptime",
|
||||||
|
"processes",
|
||||||
|
"system",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const existingConfig = this.pollingConfigs.get(host.id);
|
const existingConfig = this.pollingConfigs.get(host.id);
|
||||||
|
|
||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
@@ -1150,12 +1175,10 @@ class PollingManager {
|
|||||||
const latestConfig = this.pollingConfigs.get(refreshedHost.id);
|
const latestConfig = this.pollingConfigs.get(refreshedHost.id);
|
||||||
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
|
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
|
||||||
const backoffInfo = pollingBackoff.getBackoffInfo(refreshedHost.id);
|
const backoffInfo = pollingBackoff.getBackoffInfo(refreshedHost.id);
|
||||||
statsLogger.error("Failed to collect metrics for host", {
|
statsLogger.error("Stats collector connection failed", error, {
|
||||||
operation: "metrics_poll_failed",
|
operation: "stats_connect_failed",
|
||||||
hostId: refreshedHost.id,
|
hostId: refreshedHost.id,
|
||||||
hostName: refreshedHost.name,
|
retryInfo: backoffInfo,
|
||||||
error: errorMessage,
|
|
||||||
backoff: backoffInfo,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1172,6 +1195,7 @@ class PollingManager {
|
|||||||
clearInterval(config.metricsTimer);
|
clearInterval(config.metricsTimer);
|
||||||
config.metricsTimer = undefined;
|
config.metricsTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pollingConfigs.delete(hostId);
|
this.pollingConfigs.delete(hostId);
|
||||||
if (clearData) {
|
if (clearData) {
|
||||||
this.statusStore.delete(hostId);
|
this.statusStore.delete(hostId);
|
||||||
@@ -1333,12 +1357,7 @@ app.use(
|
|||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
if (!origin) return callback(null, true);
|
if (!origin) return callback(null, true);
|
||||||
|
|
||||||
const allowedOrigins = [
|
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
||||||
"http://localhost:5173",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:5173",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (allowedOrigins.includes(origin)) {
|
if (allowedOrigins.includes(origin)) {
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
@@ -1530,7 +1549,15 @@ async function resolveHostCredentials(
|
|||||||
if (credentials.length > 0) {
|
if (credentials.length > 0) {
|
||||||
const credential = credentials[0];
|
const credential = credentials[0];
|
||||||
baseHost.credentialId = credential.id;
|
baseHost.credentialId = credential.id;
|
||||||
baseHost.authType = credential.auth_type || credential.authType;
|
baseHost.authType =
|
||||||
|
credential.auth_type ||
|
||||||
|
credential.authType ||
|
||||||
|
(credential.password
|
||||||
|
? "password"
|
||||||
|
: credential.key ||
|
||||||
|
(credential as Record<string, unknown>).private_key
|
||||||
|
? "key"
|
||||||
|
: "none");
|
||||||
|
|
||||||
if (!host.overrideCredentialUsername) {
|
if (!host.overrideCredentialUsername) {
|
||||||
baseHost.username = credential.username;
|
baseHost.username = credential.username;
|
||||||
@@ -1551,6 +1578,13 @@ async function resolveHostCredentials(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addLegacyCredentials(baseHost, host);
|
addLegacyCredentials(baseHost, host);
|
||||||
|
if (baseHost.authType === "credential") {
|
||||||
|
baseHost.authType = baseHost.password
|
||||||
|
? "password"
|
||||||
|
: baseHost.key
|
||||||
|
? "key"
|
||||||
|
: "none";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1558,6 +1592,13 @@ async function resolveHostCredentials(
|
|||||||
`Failed to resolve credential ${host.credentialId} for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
`Failed to resolve credential ${host.credentialId} for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
);
|
);
|
||||||
addLegacyCredentials(baseHost, host);
|
addLegacyCredentials(baseHost, host);
|
||||||
|
if (baseHost.authType === "credential") {
|
||||||
|
baseHost.authType = baseHost.password
|
||||||
|
? "password"
|
||||||
|
: baseHost.key
|
||||||
|
? "key"
|
||||||
|
: "none";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addLegacyCredentials(baseHost, host);
|
addLegacyCredentials(baseHost, host);
|
||||||
@@ -1582,7 +1623,9 @@ function addLegacyCredentials(
|
|||||||
baseHost.keyType = host.keyType;
|
baseHost.keyType = host.keyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
|
async function buildSshConfig(
|
||||||
|
host: SSHHostWithCredentials,
|
||||||
|
): Promise<ConnectConfig> {
|
||||||
const base: ConnectConfig = {
|
const base: ConnectConfig = {
|
||||||
host: host.ip,
|
host: host.ip,
|
||||||
port: host.port,
|
port: host.port,
|
||||||
@@ -1593,6 +1636,14 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
|
|||||||
readyTimeout: 60000,
|
readyTimeout: 60000,
|
||||||
tcpKeepAlive: true,
|
tcpKeepAlive: true,
|
||||||
tcpKeepAliveInitialDelay: 30000,
|
tcpKeepAliveInitialDelay: 30000,
|
||||||
|
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
|
||||||
|
host.id,
|
||||||
|
host.ip,
|
||||||
|
host.port,
|
||||||
|
null,
|
||||||
|
host.userId || "",
|
||||||
|
false,
|
||||||
|
),
|
||||||
env: {
|
env: {
|
||||||
TERM: "xterm-256color",
|
TERM: "xterm-256color",
|
||||||
LANG: "en_US.UTF-8",
|
LANG: "en_US.UTF-8",
|
||||||
@@ -1687,6 +1738,25 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
|
|||||||
throw new Error(`Invalid SSH key format for host ${host.ip}`);
|
throw new Error(`Invalid SSH key format for host ${host.ip}`);
|
||||||
}
|
}
|
||||||
} else if (host.authType === "none") {
|
} else if (host.authType === "none") {
|
||||||
|
} else if (host.authType === "opkssh") {
|
||||||
|
} else if (host.authType === "credential") {
|
||||||
|
if (host.password) {
|
||||||
|
base.password = host.password;
|
||||||
|
} else if (host.key) {
|
||||||
|
const cleanKey = host.key
|
||||||
|
.trim()
|
||||||
|
.replace(/\r\n/g, "\n")
|
||||||
|
.replace(/\r/g, "\n");
|
||||||
|
(base as Record<string, unknown>).privateKey = Buffer.from(
|
||||||
|
cleanKey,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
if (host.keyPassword) {
|
||||||
|
(base as Record<string, unknown>).passphrase = host.keyPassword;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Credential for host ${host.ip} could not be resolved`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unsupported authentication type '${host.authType}' for host ${host.ip}`,
|
`Unsupported authentication type '${host.authType}' for host ${host.ip}`,
|
||||||
@@ -1809,12 +1879,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
ports = await collectPortsMetrics(client);
|
ports = await collectPortsMetrics(client);
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
statsLogger.debug("Failed to collect ports metrics", {
|
|
||||||
operation: "ports_metrics_failed",
|
|
||||||
error: e instanceof Error ? e.message : String(e),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let firewall: {
|
let firewall: {
|
||||||
type: "iptables" | "nftables" | "none";
|
type: "iptables" | "nftables" | "none";
|
||||||
@@ -1842,12 +1907,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
firewall = await collectFirewallMetrics(client);
|
firewall = await collectFirewallMetrics(client);
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
statsLogger.debug("Failed to collect firewall metrics", {
|
|
||||||
operation: "firewall_metrics_failed",
|
|
||||||
error: e instanceof Error ? e.message : String(e),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
cpu,
|
cpu,
|
||||||
@@ -2349,7 +2409,7 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
|
|||||||
return res.json({ success: true, connectionLogs });
|
return res.json({ success: true, connectionLogs });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = buildSshConfig(host);
|
const config = await buildSshConfig(host);
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
|
|
||||||
const connectionPromise = new Promise<{
|
const connectionPromise = new Promise<{
|
||||||
@@ -2524,6 +2584,15 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
|
|||||||
`Authentication failed: ${errorMessage}`,
|
`Authentication failed: ${errorMessage}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else if (errorMessage.includes("verification failed")) {
|
||||||
|
errorStage = "handshake";
|
||||||
|
connectionLogs.push(
|
||||||
|
createConnectionLog(
|
||||||
|
"error",
|
||||||
|
errorStage,
|
||||||
|
`SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
connectionLogs.push(
|
connectionLogs.push(
|
||||||
createConnectionLog(
|
createConnectionLog(
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import axios from "axios";
|
|||||||
import { getDb } from "../database/db/index.js";
|
import { getDb } from "../database/db/index.js";
|
||||||
import { sshCredentials, sshData } from "../database/db/schema.js";
|
import { sshCredentials, sshData } from "../database/db/schema.js";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { sshLogger } from "../utils/logger.js";
|
import { sshLogger, authLogger } from "../utils/logger.js";
|
||||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||||
import { AuthManager } from "../utils/auth-manager.js";
|
import { AuthManager } from "../utils/auth-manager.js";
|
||||||
import { UserCrypto } from "../utils/user-crypto.js";
|
import { UserCrypto } from "../utils/user-crypto.js";
|
||||||
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
import { SSHAuthManager } from "./auth-manager.js";
|
import { SSHAuthManager } from "./auth-manager.js";
|
||||||
|
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
|
||||||
|
|
||||||
interface ConnectToHostData {
|
interface ConnectToHostData {
|
||||||
cols: number;
|
cols: number;
|
||||||
@@ -70,6 +71,11 @@ async function resolveJumpHost(
|
|||||||
hostId: number,
|
hostId: number,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<any | null> {
|
): Promise<any | null> {
|
||||||
|
sshLogger.info("Resolving jump host", {
|
||||||
|
operation: "terminal_jumphost_resolve",
|
||||||
|
userId,
|
||||||
|
hostId,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const hosts = await SimpleDBOps.select(
|
const hosts = await SimpleDBOps.select(
|
||||||
getDb()
|
getDb()
|
||||||
@@ -159,6 +165,15 @@ async function createJumpHostChain(
|
|||||||
const jumpClient = new Client();
|
const jumpClient = new Client();
|
||||||
clients.push(jumpClient);
|
clients.push(jumpClient);
|
||||||
|
|
||||||
|
const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier(
|
||||||
|
jumpHostConfig.id,
|
||||||
|
jumpHostConfig.ip,
|
||||||
|
jumpHostConfig.port || 22,
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const connected = await new Promise<boolean>((resolve) => {
|
const connected = await new Promise<boolean>((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
@@ -166,6 +181,13 @@ async function createJumpHostChain(
|
|||||||
|
|
||||||
jumpClient.on("ready", () => {
|
jumpClient.on("ready", () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
sshLogger.success("Jump host connection established", {
|
||||||
|
operation: "terminal_jumphost_connected",
|
||||||
|
userId,
|
||||||
|
hostId: jumpHostConfig.id,
|
||||||
|
ip: jumpHostConfig.ip,
|
||||||
|
depth: i,
|
||||||
|
});
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,6 +207,7 @@ async function createJumpHostChain(
|
|||||||
username: jumpHostConfig.username,
|
username: jumpHostConfig.username,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
readyTimeout: 30000,
|
readyTimeout: 30000,
|
||||||
|
hostVerifier: jumpHostVerifier,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
|
||||||
@@ -279,6 +302,7 @@ const wss = new WebSocketServer({
|
|||||||
|
|
||||||
wss.on("connection", async (ws: WebSocket, req) => {
|
wss.on("connection", async (ws: WebSocket, req) => {
|
||||||
let userId: string | undefined;
|
let userId: string | undefined;
|
||||||
|
let sessionId: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = parseUrl(req.url!, true);
|
const url = parseUrl(req.url!, true);
|
||||||
@@ -296,6 +320,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userId = payload.userId;
|
userId = payload.userId;
|
||||||
|
sessionId = payload.sessionId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sshLogger.error(
|
sshLogger.error(
|
||||||
"WebSocket JWT verification failed during connection",
|
"WebSocket JWT verification failed during connection",
|
||||||
@@ -327,6 +352,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}
|
}
|
||||||
const userWs = userConnections.get(userId)!;
|
const userWs = userConnections.get(userId)!;
|
||||||
userWs.add(ws);
|
userWs.add(ws);
|
||||||
|
sshLogger.info("Terminal WebSocket connection established", {
|
||||||
|
operation: "terminal_ws_connect",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
let sshConn: Client | null = null;
|
let sshConn: Client | null = null;
|
||||||
let sshStream: ClientChannel | null = null;
|
let sshStream: ClientChannel | null = null;
|
||||||
@@ -342,8 +372,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
let warpgateAuthPromptSent = false;
|
let warpgateAuthPromptSent = false;
|
||||||
let warpgateAuthTimeout: NodeJS.Timeout | null = null;
|
let warpgateAuthTimeout: NodeJS.Timeout | null = null;
|
||||||
let isAwaitingAuthCredentials = false;
|
let isAwaitingAuthCredentials = false;
|
||||||
|
let opksshTempFiles: { keyPath: string; certPath: string } | null = null;
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
|
sshLogger.info("Terminal WebSocket disconnected", {
|
||||||
|
operation: "terminal_ws_disconnect",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
const userWs = userConnections.get(userId);
|
const userWs = userConnections.get(userId);
|
||||||
if (userWs) {
|
if (userWs) {
|
||||||
userWs.delete(ws);
|
userWs.delete(ws);
|
||||||
@@ -355,7 +391,17 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
cleanupSSH();
|
cleanupSSH();
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("message", (msg: RawData) => {
|
function resetConnectionState() {
|
||||||
|
isConnecting = false;
|
||||||
|
isConnected = false;
|
||||||
|
isKeyboardInteractive = false;
|
||||||
|
keyboardInteractiveResponded = false;
|
||||||
|
keyboardInteractiveFinish = null;
|
||||||
|
totpPromptSent = false;
|
||||||
|
warpgateAuthPromptSent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.on("message", async (msg: RawData) => {
|
||||||
const currentDataKey = userCrypto.getUserDataKey(userId);
|
const currentDataKey = userCrypto.getUserDataKey(userId);
|
||||||
if (!currentDataKey) {
|
if (!currentDataKey) {
|
||||||
ws.send(
|
ws.send(
|
||||||
@@ -568,6 +614,116 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "opkssh_start_auth": {
|
||||||
|
const opksshData = data as { hostId: number };
|
||||||
|
try {
|
||||||
|
const { startOPKSSHAuth, getRequestOrigin } =
|
||||||
|
await import("./opkssh-auth.js");
|
||||||
|
const db = getDb();
|
||||||
|
const hostRow = await db
|
||||||
|
.select()
|
||||||
|
.from(sshData)
|
||||||
|
.where(eq(sshData.id, opksshData.hostId))
|
||||||
|
.limit(1);
|
||||||
|
if (!hostRow || hostRow.length === 0) {
|
||||||
|
sshLogger.error(
|
||||||
|
`Host ${opksshData.hostId} not found for OPKSSH auth`,
|
||||||
|
{
|
||||||
|
operation: "opkssh_start_auth_host_not_found",
|
||||||
|
userId,
|
||||||
|
hostId: opksshData.hostId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId: "",
|
||||||
|
error: "Host not found",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const hostname = hostRow[0].name || hostRow[0].ip;
|
||||||
|
const requestOrigin = getRequestOrigin(req);
|
||||||
|
await startOPKSSHAuth(
|
||||||
|
userId,
|
||||||
|
opksshData.hostId,
|
||||||
|
hostname,
|
||||||
|
ws,
|
||||||
|
requestOrigin,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("Failed to start OPKSSH auth", error, {
|
||||||
|
operation: "opkssh_start_auth_error",
|
||||||
|
userId,
|
||||||
|
hostId: opksshData.hostId,
|
||||||
|
});
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_error",
|
||||||
|
requestId: "",
|
||||||
|
error: "Failed to start OPKSSH authentication",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "opkssh_cancel": {
|
||||||
|
const cancelData = data as { requestId: string };
|
||||||
|
try {
|
||||||
|
const { cancelAuthSession } = await import("./opkssh-auth.js");
|
||||||
|
cancelAuthSession(cancelData.requestId);
|
||||||
|
resetConnectionState();
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("Failed to cancel OPKSSH auth", error, {
|
||||||
|
operation: "opkssh_cancel_error",
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "opkssh_browser_opened": {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "opkssh_auth_completed": {
|
||||||
|
const completedData = data as {
|
||||||
|
hostId: number;
|
||||||
|
cols?: number;
|
||||||
|
rows?: number;
|
||||||
|
hostConfig?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
resetConnectionState();
|
||||||
|
|
||||||
|
const reconnectConfig: ConnectToHostData = {
|
||||||
|
cols: completedData.cols || 80,
|
||||||
|
rows: completedData.rows || 24,
|
||||||
|
hostConfig:
|
||||||
|
completedData.hostConfig ||
|
||||||
|
({ id: completedData.hostId, userId } as any),
|
||||||
|
};
|
||||||
|
|
||||||
|
handleConnectToHost(reconnectConfig).catch((error) => {
|
||||||
|
sshLogger.error("Failed to reconnect after OPKSSH auth", error, {
|
||||||
|
operation: "opkssh_reconnect_error",
|
||||||
|
userId,
|
||||||
|
hostId: completedData.hostId,
|
||||||
|
});
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Failed to connect after authentication: " +
|
||||||
|
(error instanceof Error ? error.message : "Unknown error"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
sshLogger.warn("Unknown message type received", {
|
sshLogger.warn("Unknown message type received", {
|
||||||
operation: "websocket_message_unknown_type",
|
operation: "websocket_message_unknown_type",
|
||||||
@@ -591,6 +747,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
authType,
|
authType,
|
||||||
credentialId,
|
credentialId,
|
||||||
} = hostConfig;
|
} = hostConfig;
|
||||||
|
sshLogger.info("Resolving SSH host configuration", {
|
||||||
|
operation: "terminal_host_resolve",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
});
|
||||||
|
|
||||||
const sendLog = (
|
const sendLog = (
|
||||||
stage: string,
|
stage: string,
|
||||||
@@ -745,7 +907,21 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
|
|
||||||
sshConn.on("ready", () => {
|
sshConn.on("ready", () => {
|
||||||
clearTimeout(connectionTimeout);
|
clearTimeout(connectionTimeout);
|
||||||
|
sshLogger.success("SSH connection established", {
|
||||||
|
operation: "terminal_ssh_connected",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
ip,
|
||||||
|
});
|
||||||
|
if (totpPromptSent) {
|
||||||
|
authLogger.success("TOTP verification successful for SSH session", {
|
||||||
|
operation: "terminal_totp_success",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
sendLog("handshake", "success", "SSH handshake completed");
|
sendLog("handshake", "success", "SSH handshake completed");
|
||||||
sendLog("auth", "success", `Authentication successful for ${username}`);
|
sendLog("auth", "success", `Authentication successful for ${username}`);
|
||||||
sendLog("connected", "success", "Connection established");
|
sendLog("connected", "success", "Connection established");
|
||||||
@@ -858,6 +1034,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sshStream = stream;
|
sshStream = stream;
|
||||||
|
sshLogger.success("Terminal shell channel opened", {
|
||||||
|
operation: "terminal_shell_opened",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
termType: "xterm-256color",
|
||||||
|
});
|
||||||
|
|
||||||
stream.on("data", (data: Buffer) => {
|
stream.on("data", (data: Buffer) => {
|
||||||
try {
|
try {
|
||||||
@@ -987,6 +1170,57 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
keyboardInteractiveResponded,
|
keyboardInteractiveResponded,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
resolvedCredentials.authType === "opkssh" &&
|
||||||
|
err.message.includes("All configured authentication methods failed")
|
||||||
|
) {
|
||||||
|
sshLogger.warn("OPKSSH authentication failed - invalidating token", {
|
||||||
|
operation: "opkssh_auth_failed",
|
||||||
|
hostId: id,
|
||||||
|
userId,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { invalidateOPKSSHToken } = await import("./opkssh-auth.js");
|
||||||
|
await invalidateOPKSSHToken(userId, id, "SSH auth failed");
|
||||||
|
} catch (invalidateError) {
|
||||||
|
sshLogger.error("Failed to invalidate OPKSSH token", {
|
||||||
|
operation: "opkssh_token_invalidation_error",
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
error: invalidateError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
clearTimeout(connectionTimeout);
|
||||||
|
if (sshConn) {
|
||||||
|
try {
|
||||||
|
sshConn.end();
|
||||||
|
} catch (e) {}
|
||||||
|
sshConn = null;
|
||||||
|
}
|
||||||
|
resetConnectionState();
|
||||||
|
|
||||||
|
sendLog(
|
||||||
|
"auth",
|
||||||
|
"error",
|
||||||
|
"OPKSSH certificate authentication failed. Please authenticate again.",
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_auth_required",
|
||||||
|
hostId: id,
|
||||||
|
message:
|
||||||
|
"OPKSSH authentication failed or expired. Please authenticate again.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
authMethodNotAvailable &&
|
authMethodNotAvailable &&
|
||||||
resolvedCredentials.authType === "none" &&
|
resolvedCredentials.authType === "none" &&
|
||||||
@@ -1005,8 +1239,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
sshConn = null;
|
sshConn = null;
|
||||||
}
|
}
|
||||||
isConnecting = false;
|
resetConnectionState();
|
||||||
isConnected = false;
|
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "auth_method_not_available",
|
type: "auth_method_not_available",
|
||||||
@@ -1031,8 +1264,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
sshConn = null;
|
sshConn = null;
|
||||||
}
|
}
|
||||||
isConnecting = false;
|
resetConnectionState();
|
||||||
isConnected = false;
|
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "auth_method_not_available",
|
type: "auth_method_not_available",
|
||||||
@@ -1056,6 +1288,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
error: err.message,
|
error: err.message,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
resetConnectionState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1069,6 +1302,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
err.message.includes("authentication") ||
|
err.message.includes("authentication") ||
|
||||||
err.message.includes("Authentication")
|
err.message.includes("Authentication")
|
||||||
) {
|
) {
|
||||||
|
authLogger.error("SSH authentication failed", err, {
|
||||||
|
operation: "terminal_ssh_auth_failed",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
authType: resolvedCredentials.authType,
|
||||||
|
});
|
||||||
sendLog("auth", "error", `Authentication failed: ${err.message}`);
|
sendLog("auth", "error", `Authentication failed: ${err.message}`);
|
||||||
} else {
|
} else {
|
||||||
sendLog("error", "error", `Connection failed: ${err.message}`);
|
sendLog("error", "error", `Connection failed: ${err.message}`);
|
||||||
@@ -1116,6 +1356,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
|
|
||||||
sshConn.on("close", () => {
|
sshConn.on("close", () => {
|
||||||
clearTimeout(connectionTimeout);
|
clearTimeout(connectionTimeout);
|
||||||
|
sshLogger.info("SSH connection closed", {
|
||||||
|
operation: "terminal_ssh_disconnected",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
});
|
||||||
|
|
||||||
if (isAwaitingAuthCredentials) {
|
if (isAwaitingAuthCredentials) {
|
||||||
cleanupSSH(connectionTimeout);
|
cleanupSSH(connectionTimeout);
|
||||||
@@ -1209,6 +1455,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
tcpKeepAlive: true,
|
tcpKeepAlive: true,
|
||||||
tcpKeepAliveInitialDelay: 30000,
|
tcpKeepAliveInitialDelay: 30000,
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
|
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
|
||||||
|
id,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
ws,
|
||||||
|
userId,
|
||||||
|
false,
|
||||||
|
),
|
||||||
env: {
|
env: {
|
||||||
TERM: "xterm-256color",
|
TERM: "xterm-256color",
|
||||||
LANG: "en_US.UTF-8",
|
LANG: "en_US.UTF-8",
|
||||||
@@ -1229,6 +1483,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
"ecdh-sha2-nistp384",
|
"ecdh-sha2-nistp384",
|
||||||
"ecdh-sha2-nistp256",
|
"ecdh-sha2-nistp256",
|
||||||
"diffie-hellman-group-exchange-sha256",
|
"diffie-hellman-group-exchange-sha256",
|
||||||
|
"diffie-hellman-group18-sha512",
|
||||||
|
"diffie-hellman-group17-sha512",
|
||||||
|
"diffie-hellman-group16-sha512",
|
||||||
|
"diffie-hellman-group15-sha512",
|
||||||
"diffie-hellman-group14-sha256",
|
"diffie-hellman-group14-sha256",
|
||||||
"diffie-hellman-group14-sha1",
|
"diffie-hellman-group14-sha1",
|
||||||
"diffie-hellman-group-exchange-sha1",
|
"diffie-hellman-group-exchange-sha1",
|
||||||
@@ -1287,7 +1545,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
if (!hostConfig.forceKeyboardInteractive) {
|
if (!hostConfig.forceKeyboardInteractive) {
|
||||||
connectConfig.password = resolvedCredentials.password;
|
connectConfig.password = resolvedCredentials.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendLog("auth", "info", "Using password authentication");
|
sendLog("auth", "info", "Using password authentication");
|
||||||
} else if (
|
} else if (
|
||||||
resolvedCredentials.authType === "key" &&
|
resolvedCredentials.authType === "key" &&
|
||||||
@@ -1336,6 +1593,60 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
} else if (resolvedCredentials.authType === "opkssh") {
|
||||||
|
sendLog("auth", "info", "Using OPKSSH certificate authentication");
|
||||||
|
try {
|
||||||
|
const { getOPKSSHToken } = await import("./opkssh-auth.js");
|
||||||
|
const token = await getOPKSSHToken(userId, id);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
sendLog(
|
||||||
|
"auth",
|
||||||
|
"info",
|
||||||
|
"No valid OPKSSH token found, requesting authentication",
|
||||||
|
);
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "opkssh_auth_required",
|
||||||
|
hostId: id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendLog("auth", "info", "Using cached OPKSSH certificate");
|
||||||
|
|
||||||
|
const { promises: fs } = await import("fs");
|
||||||
|
const path = await import("path");
|
||||||
|
const os = await import("os");
|
||||||
|
|
||||||
|
const tempDir = os.tmpdir();
|
||||||
|
const keyPath = path.join(tempDir, `opkssh-${userId}-${id}`);
|
||||||
|
const certPath = `${keyPath}-cert.pub`;
|
||||||
|
|
||||||
|
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
|
||||||
|
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
|
||||||
|
|
||||||
|
opksshTempFiles = { keyPath, certPath };
|
||||||
|
connectConfig.privateKey = await fs.readFile(keyPath);
|
||||||
|
} catch (opksshError) {
|
||||||
|
sshLogger.error("OPKSSH authentication error", opksshError, {
|
||||||
|
operation: "opkssh_auth_error",
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
});
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"OPKSSH authentication failed: " +
|
||||||
|
(opksshError instanceof Error
|
||||||
|
? opksshError.message
|
||||||
|
: "Unknown error"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sendLog("auth", "info", "Using keyboard-interactive authentication");
|
sendLog("auth", "info", "Using keyboard-interactive authentication");
|
||||||
sshLogger.error("No valid authentication method provided");
|
sshLogger.error("No valid authentication method provided");
|
||||||
@@ -1440,6 +1751,16 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
"Starting SSH session through jump host",
|
"Starting SSH session through jump host",
|
||||||
);
|
);
|
||||||
sendLog("auth", "info", `Authenticating as ${username}`);
|
sendLog("auth", "info", `Authenticating as ${username}`);
|
||||||
|
sshLogger.info("Initiating SSH connection", {
|
||||||
|
operation: "terminal_ssh_connect_attempt",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
authType: resolvedCredentials.authType,
|
||||||
|
});
|
||||||
sshConn.connect(connectConfig);
|
sshConn.connect(connectConfig);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1459,6 +1780,16 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
} else {
|
} else {
|
||||||
sendLog("handshake", "info", "Starting SSH session");
|
sendLog("handshake", "info", "Starting SSH session");
|
||||||
sendLog("auth", "info", `Authenticating as ${username}`);
|
sendLog("auth", "info", `Authenticating as ${username}`);
|
||||||
|
sshLogger.info("Initiating SSH connection", {
|
||||||
|
operation: "terminal_ssh_connect_attempt",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
hostId: id,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
authType: resolvedCredentials.authType,
|
||||||
|
});
|
||||||
sshConn.connect(connectConfig);
|
sshConn.connect(connectConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1529,15 +1860,43 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
sshConn = null;
|
sshConn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
totpPromptSent = false;
|
resetConnectionState();
|
||||||
warpgateAuthPromptSent = false;
|
|
||||||
isKeyboardInteractive = false;
|
|
||||||
keyboardInteractiveResponded = false;
|
|
||||||
keyboardInteractiveFinish = null;
|
|
||||||
isConnecting = false;
|
|
||||||
isConnected = false;
|
|
||||||
isCleaningUp = false;
|
isCleaningUp = false;
|
||||||
isAwaitingAuthCredentials = false;
|
isAwaitingAuthCredentials = false;
|
||||||
|
|
||||||
|
if (opksshTempFiles) {
|
||||||
|
const tempFilesToClean = opksshTempFiles;
|
||||||
|
opksshTempFiles = null;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { promises: fs } = await import("fs");
|
||||||
|
const cleanupResults = await Promise.allSettled([
|
||||||
|
fs.unlink(tempFilesToClean.keyPath),
|
||||||
|
fs.unlink(tempFilesToClean.certPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cleanupResults.forEach((result, index) => {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
sshLogger.warn(`Failed to cleanup OPKSSH temp file`, {
|
||||||
|
operation: "opkssh_temp_cleanup_failed",
|
||||||
|
file: index === 0 ? "keyPath" : "certPath",
|
||||||
|
path:
|
||||||
|
index === 0
|
||||||
|
? tempFilesToClean.keyPath
|
||||||
|
: tempFilesToClean.certPath,
|
||||||
|
error: result.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("Failed to cleanup OPKSSH temp files", {
|
||||||
|
operation: "opkssh_temp_cleanup_error",
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: PTY-level keepalive (writing \x00 to the stream) was removed.
|
// Note: PTY-level keepalive (writing \x00 to the stream) was removed.
|
||||||
|
|||||||
@@ -30,12 +30,7 @@ app.use(
|
|||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
if (!origin) return callback(null, true);
|
if (!origin) return callback(null, true);
|
||||||
|
|
||||||
const allowedOrigins = [
|
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
||||||
"http://localhost:5173",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:5173",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (allowedOrigins.includes(origin)) {
|
if (allowedOrigins.includes(origin)) {
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
@@ -294,7 +289,12 @@ async function cleanupTunnelResources(
|
|||||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||||
try {
|
try {
|
||||||
verification?.conn.end();
|
verification?.conn.end();
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
tunnelLogger.error("Error during tunnel cleanup", error, {
|
||||||
|
operation: "tunnel_cleanup_error",
|
||||||
|
tunnelName,
|
||||||
|
});
|
||||||
|
}
|
||||||
tunnelVerifications.delete(tunnelName);
|
tunnelVerifications.delete(tunnelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +359,12 @@ async function handleDisconnect(
|
|||||||
const verification = tunnelVerifications.get(tunnelName);
|
const verification = tunnelVerifications.get(tunnelName);
|
||||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||||
verification?.conn.end();
|
verification?.conn.end();
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
tunnelLogger.error("Error during tunnel cleanup", error, {
|
||||||
|
operation: "tunnel_cleanup_error",
|
||||||
|
tunnelName,
|
||||||
|
});
|
||||||
|
}
|
||||||
tunnelVerifications.delete(tunnelName);
|
tunnelVerifications.delete(tunnelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,6 +524,16 @@ async function connectSSHTunnel(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const tunnelName = tunnelConfig.name;
|
const tunnelName = tunnelConfig.name;
|
||||||
const tunnelMarker = getTunnelMarker(tunnelName);
|
const tunnelMarker = getTunnelMarker(tunnelName);
|
||||||
|
tunnelLogger.info("Tunnel creation request received", {
|
||||||
|
operation: "tunnel_create_request",
|
||||||
|
userId: tunnelConfig.sourceUserId,
|
||||||
|
hostId: tunnelConfig.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
tunnelType: tunnelConfig.tunnelType || "remote",
|
||||||
|
sourcePort: tunnelConfig.sourcePort,
|
||||||
|
endpointHost: tunnelConfig.endpointHost,
|
||||||
|
endpointPort: tunnelConfig.endpointPort,
|
||||||
|
});
|
||||||
|
|
||||||
if (manualDisconnects.has(tunnelName)) {
|
if (manualDisconnects.has(tunnelName)) {
|
||||||
return;
|
return;
|
||||||
@@ -875,6 +890,12 @@ async function connectSSHTunnel(
|
|||||||
|
|
||||||
conn.on("ready", () => {
|
conn.on("ready", () => {
|
||||||
clearTimeout(connectionTimeout);
|
clearTimeout(connectionTimeout);
|
||||||
|
tunnelLogger.info("Creating new SSH connection for tunnel", {
|
||||||
|
operation: "tunnel_connection_create",
|
||||||
|
userId: tunnelConfig.sourceUserId,
|
||||||
|
hostId: tunnelConfig.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
});
|
||||||
|
|
||||||
const isAlreadyVerifying = tunnelVerifications.has(tunnelName);
|
const isAlreadyVerifying = tunnelVerifications.has(tunnelName);
|
||||||
if (isAlreadyVerifying) {
|
if (isAlreadyVerifying) {
|
||||||
@@ -933,6 +954,14 @@ async function connectSSHTunnel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeTunnels.set(tunnelName, conn);
|
activeTunnels.set(tunnelName, conn);
|
||||||
|
tunnelLogger.success("Tunnel port binding successful", {
|
||||||
|
operation: "tunnel_port_bound",
|
||||||
|
userId: tunnelConfig.sourceUserId,
|
||||||
|
hostId: tunnelConfig.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
sourcePort: tunnelConfig.sourcePort,
|
||||||
|
endpointPort: tunnelConfig.endpointPort,
|
||||||
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (
|
if (
|
||||||
@@ -940,6 +969,12 @@ async function connectSSHTunnel(
|
|||||||
activeTunnels.has(tunnelName)
|
activeTunnels.has(tunnelName)
|
||||||
) {
|
) {
|
||||||
tunnelConnecting.delete(tunnelName);
|
tunnelConnecting.delete(tunnelName);
|
||||||
|
tunnelLogger.success("Tunnel creation complete", {
|
||||||
|
operation: "tunnel_create_complete",
|
||||||
|
userId: tunnelConfig.sourceUserId,
|
||||||
|
hostId: tunnelConfig.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
});
|
||||||
|
|
||||||
broadcastTunnelStatus(tunnelName, {
|
broadcastTunnelStatus(tunnelName, {
|
||||||
connected: true,
|
connected: true,
|
||||||
@@ -1279,6 +1314,13 @@ async function killRemoteTunnelByMarker(
|
|||||||
callback: (err?: Error) => void,
|
callback: (err?: Error) => void,
|
||||||
) {
|
) {
|
||||||
const tunnelMarker = getTunnelMarker(tunnelName);
|
const tunnelMarker = getTunnelMarker(tunnelName);
|
||||||
|
tunnelLogger.info("Killing remote tunnel process", {
|
||||||
|
operation: "tunnel_remote_kill",
|
||||||
|
userId: tunnelConfig.sourceUserId,
|
||||||
|
hostId: tunnelConfig.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
marker: tunnelMarker,
|
||||||
|
});
|
||||||
|
|
||||||
let resolvedSourceCredentials = {
|
let resolvedSourceCredentials = {
|
||||||
password: tunnelConfig.sourcePassword,
|
password: tunnelConfig.sourcePassword,
|
||||||
@@ -1418,10 +1460,24 @@ async function killRemoteTunnelByMarker(
|
|||||||
|
|
||||||
stream.on("close", () => {
|
stream.on("close", () => {
|
||||||
if (!foundProcesses) {
|
if (!foundProcesses) {
|
||||||
|
tunnelLogger.warn("Remote tunnel process not found", {
|
||||||
|
operation: "tunnel_remote_not_found",
|
||||||
|
userId: tunnelConfig.sourceUserId,
|
||||||
|
hostId: tunnelConfig.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
marker: tunnelMarker,
|
||||||
|
});
|
||||||
conn.end();
|
conn.end();
|
||||||
callback();
|
callback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
tunnelLogger.info("Remote tunnel process found, proceeding to kill", {
|
||||||
|
operation: "tunnel_remote_found",
|
||||||
|
userId: tunnelConfig.sourceUserId,
|
||||||
|
hostId: tunnelConfig.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
marker: tunnelMarker,
|
||||||
|
});
|
||||||
|
|
||||||
const killCmds = [
|
const killCmds = [
|
||||||
`pkill -TERM -f '${tunnelMarker}'`,
|
`pkill -TERM -f '${tunnelMarker}'`,
|
||||||
@@ -1452,6 +1508,13 @@ async function killRemoteTunnelByMarker(
|
|||||||
tunnelLogger.warn(
|
tunnelLogger.warn(
|
||||||
`Some tunnel processes may still be running for '${tunnelName}'`,
|
`Some tunnel processes may still be running for '${tunnelName}'`,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
tunnelLogger.success("Remote tunnel process killed", {
|
||||||
|
operation: "tunnel_remote_killed",
|
||||||
|
userId: tunnelConfig.sourceUserId,
|
||||||
|
hostId: tunnelConfig.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
conn.end();
|
conn.end();
|
||||||
callback();
|
callback();
|
||||||
@@ -1843,6 +1906,12 @@ app.post(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tunnelLogger.info("Tunnel stop request received", {
|
||||||
|
operation: "tunnel_stop_request",
|
||||||
|
userId,
|
||||||
|
hostId: config?.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
});
|
||||||
manualDisconnects.add(tunnelName);
|
manualDisconnects.add(tunnelName);
|
||||||
retryCounters.delete(tunnelName);
|
retryCounters.delete(tunnelName);
|
||||||
retryExhaustedTunnels.delete(tunnelName);
|
retryExhaustedTunnels.delete(tunnelName);
|
||||||
@@ -1853,6 +1922,12 @@ app.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await cleanupTunnelResources(tunnelName, true);
|
await cleanupTunnelResources(tunnelName, true);
|
||||||
|
tunnelLogger.info("Tunnel cleanup completed", {
|
||||||
|
operation: "tunnel_cleanup_complete",
|
||||||
|
userId,
|
||||||
|
hostId: config?.sourceHostId,
|
||||||
|
tunnelName,
|
||||||
|
});
|
||||||
|
|
||||||
broadcastTunnelStatus(tunnelName, {
|
broadcastTunnelStatus(tunnelName, {
|
||||||
connected: false,
|
connected: false,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { SystemCrypto } from "./utils/system-crypto.js";
|
|||||||
import { systemLogger, versionLogger } from "./utils/logger.js";
|
import { systemLogger, versionLogger } from "./utils/logger.js";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const initStartTime = Date.now();
|
||||||
try {
|
try {
|
||||||
dotenv.config({ quiet: true });
|
dotenv.config({ quiet: true });
|
||||||
|
|
||||||
@@ -23,6 +24,12 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
|||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
|
systemLogger.info("Termix backend initialization started", {
|
||||||
|
operation: "backend_init_start",
|
||||||
|
nodeEnv: process.env.NODE_ENV || "production",
|
||||||
|
port: process.env.PORT || 4090,
|
||||||
|
});
|
||||||
|
|
||||||
let version = "unknown";
|
let version = "unknown";
|
||||||
|
|
||||||
const versionSources = [
|
const versionSources = [
|
||||||
@@ -88,14 +95,41 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
|||||||
await systemCrypto.initializeInternalAuthToken();
|
await systemCrypto.initializeInternalAuthToken();
|
||||||
|
|
||||||
await AutoSSLSetup.initialize();
|
await AutoSSLSetup.initialize();
|
||||||
|
systemLogger.success("SSL setup completed", {
|
||||||
|
operation: "backend_init_ssl",
|
||||||
|
sslEnabled: process.env.SSL_ENABLED === "true",
|
||||||
|
});
|
||||||
|
|
||||||
const dbModule = await import("./database/db/index.js");
|
const dbModule = await import("./database/db/index.js");
|
||||||
await dbModule.initializeDatabase();
|
await dbModule.initializeDatabase();
|
||||||
|
systemLogger.success("Database initialized", {
|
||||||
|
operation: "backend_init_db",
|
||||||
|
});
|
||||||
|
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
await authManager.initialize();
|
await authManager.initialize();
|
||||||
DataCrypto.initialize();
|
DataCrypto.initialize();
|
||||||
|
|
||||||
|
const { OPKSSHBinaryManager } =
|
||||||
|
await import("./utils/opkssh-binary-manager.js");
|
||||||
|
try {
|
||||||
|
await OPKSSHBinaryManager.ensureBinary();
|
||||||
|
} catch (error) {
|
||||||
|
const dataDir =
|
||||||
|
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
|
||||||
|
systemLogger.warn(
|
||||||
|
"Failed to initialize OPKSSH binary - OPKSSH authentication will not be available",
|
||||||
|
{
|
||||||
|
operation: "opkssh_binary_init_failed",
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
dataDir,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await import("./database/database.js");
|
await import("./database/database.js");
|
||||||
|
|
||||||
await import("./ssh/terminal.js");
|
await import("./ssh/terminal.js");
|
||||||
@@ -106,6 +140,13 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
|||||||
await import("./ssh/docker-console.js");
|
await import("./ssh/docker-console.js");
|
||||||
await import("./dashboard.js");
|
await import("./dashboard.js");
|
||||||
|
|
||||||
|
systemLogger.success("Termix backend started successfully", {
|
||||||
|
operation: "backend_init_complete",
|
||||||
|
port: process.env.PORT || 4090,
|
||||||
|
ssl: process.env.SSL_ENABLED === "true",
|
||||||
|
duration: Date.now() - initStartTime,
|
||||||
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
systemLogger.info(
|
systemLogger.info(
|
||||||
"Received SIGINT signal, initiating graceful shutdown...",
|
"Received SIGINT signal, initiating graceful shutdown...",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import swaggerJSDoc from "swagger-jsdoc";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
|
import { systemLogger } from "./utils/logger.js";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -125,6 +126,10 @@ const swaggerOptions: swaggerJSDoc.Options = {
|
|||||||
|
|
||||||
async function generateOpenAPISpec() {
|
async function generateOpenAPISpec() {
|
||||||
try {
|
try {
|
||||||
|
systemLogger.info("Generating OpenAPI specification", {
|
||||||
|
operation: "openapi_generate_start",
|
||||||
|
});
|
||||||
|
|
||||||
const swaggerSpec = swaggerJSDoc(swaggerOptions);
|
const swaggerSpec = swaggerJSDoc(swaggerOptions);
|
||||||
|
|
||||||
const outputPath = path.join(projectRoot, "openapi.json");
|
const outputPath = path.join(projectRoot, "openapi.json");
|
||||||
@@ -134,8 +139,14 @@ async function generateOpenAPISpec() {
|
|||||||
JSON.stringify(swaggerSpec, null, 2),
|
JSON.stringify(swaggerSpec, null, 2),
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
systemLogger.success("OpenAPI specification generated", {
|
||||||
|
operation: "openapi_generate_success",
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to generate OpenAPI specification:", error);
|
systemLogger.error("Failed to generate OpenAPI specification", error, {
|
||||||
|
operation: "openapi_generation",
|
||||||
|
});
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
|
|||||||
import { UserCrypto } from "./user-crypto.js";
|
import { UserCrypto } from "./user-crypto.js";
|
||||||
import { SystemCrypto } from "./system-crypto.js";
|
import { SystemCrypto } from "./system-crypto.js";
|
||||||
import { DataCrypto } from "./data-crypto.js";
|
import { DataCrypto } from "./data-crypto.js";
|
||||||
import { databaseLogger } from "./logger.js";
|
import { databaseLogger, authLogger } from "./logger.js";
|
||||||
import type { Request, Response, NextFunction } from "express";
|
import type { Request, Response, NextFunction } from "express";
|
||||||
import { db } from "../database/db/index.js";
|
import { db } from "../database/db/index.js";
|
||||||
import { sessions } from "../database/db/schema.js";
|
import { sessions } from "../database/db/schema.js";
|
||||||
@@ -346,6 +346,11 @@ class AuthManager {
|
|||||||
|
|
||||||
async revokeSession(sessionId: string): Promise<boolean> {
|
async revokeSession(sessionId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
authLogger.info("User session invalidated", {
|
||||||
|
operation: "user_logout",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -387,6 +392,12 @@ class AuthManager {
|
|||||||
(s) => !exceptSessionId || s.id !== exceptSessionId,
|
(s) => !exceptSessionId || s.id !== exceptSessionId,
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
|
authLogger.info("All user sessions invalidated", {
|
||||||
|
operation: "user_logout_all",
|
||||||
|
userId,
|
||||||
|
sessionCount: deletedCount,
|
||||||
|
});
|
||||||
|
|
||||||
if (exceptSessionId) {
|
if (exceptSessionId) {
|
||||||
await db
|
await db
|
||||||
.delete(sessions)
|
.delete(sessions)
|
||||||
|
|||||||
@@ -372,89 +372,19 @@ class DatabaseFileEncryption {
|
|||||||
encryptedPath: string,
|
encryptedPath: string,
|
||||||
targetPath?: string,
|
targetPath?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!fs.existsSync(encryptedPath)) {
|
|
||||||
throw new Error(
|
|
||||||
`Encrypted database file does not exist: ${encryptedPath}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
|
|
||||||
if (!fs.existsSync(metadataPath)) {
|
|
||||||
throw new Error(`Metadata file does not exist: ${metadataPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptedPath =
|
const decryptedPath =
|
||||||
targetPath || encryptedPath.replace(this.ENCRYPTED_FILE_SUFFIX, "");
|
targetPath || encryptedPath.replace(this.ENCRYPTED_FILE_SUFFIX, "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
const decryptedBuffer = await this.decryptDatabaseToBuffer(encryptedPath);
|
||||||
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
|
||||||
|
|
||||||
const encryptedData = fs.readFileSync(encryptedPath);
|
fs.writeFileSync(decryptedPath, decryptedBuffer);
|
||||||
|
|
||||||
if (
|
|
||||||
metadata.dataSize !== undefined &&
|
|
||||||
encryptedData.length !== metadata.dataSize
|
|
||||||
) {
|
|
||||||
databaseLogger.error(
|
|
||||||
"Encrypted file size mismatch - possible corrupted write or mismatched metadata",
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
operation: "database_file_size_mismatch",
|
|
||||||
encryptedPath,
|
|
||||||
actualSize: encryptedData.length,
|
|
||||||
expectedSize: metadata.dataSize,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`Encrypted file size mismatch: expected ${metadata.dataSize} bytes but got ${encryptedData.length} bytes. ` +
|
|
||||||
`This indicates corrupted files or interrupted write operation.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let key: Buffer;
|
|
||||||
if (metadata.version === "v2") {
|
|
||||||
key = await this.systemCrypto.getDatabaseKey();
|
|
||||||
} else if (metadata.version === "v1") {
|
|
||||||
databaseLogger.warn(
|
|
||||||
"Decrypting legacy v1 encrypted database - consider upgrading",
|
|
||||||
{
|
|
||||||
operation: "decrypt_legacy_v1",
|
|
||||||
path: encryptedPath,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!metadata.salt) {
|
|
||||||
throw new Error("v1 encrypted file missing required salt field");
|
|
||||||
}
|
|
||||||
const salt = Buffer.from(metadata.salt, "hex");
|
|
||||||
const fixedSeed =
|
|
||||||
process.env.DB_FILE_KEY || "termix-database-file-encryption-seed-v1";
|
|
||||||
key = crypto.pbkdf2Sync(fixedSeed, salt, 100000, 32, "sha256");
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unsupported encryption version: ${metadata.version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const decipher = crypto.createDecipheriv(
|
|
||||||
metadata.algorithm,
|
|
||||||
key,
|
|
||||||
Buffer.from(metadata.iv, "hex"),
|
|
||||||
) as crypto.DecipherGCM;
|
|
||||||
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
|
|
||||||
|
|
||||||
const decrypted = Buffer.concat([
|
|
||||||
decipher.update(encryptedData),
|
|
||||||
decipher.final(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
fs.writeFileSync(decryptedPath, decrypted);
|
|
||||||
|
|
||||||
databaseLogger.info("Database file decrypted successfully", {
|
databaseLogger.info("Database file decrypted successfully", {
|
||||||
operation: "database_file_decryption",
|
operation: "database_file_decryption",
|
||||||
encryptedPath,
|
encryptedPath,
|
||||||
decryptedPath,
|
decryptedPath,
|
||||||
encryptedSize: encryptedData.length,
|
decryptedSize: decryptedBuffer.length,
|
||||||
decryptedSize: decrypted.length,
|
|
||||||
fingerprintPrefix: metadata.fingerprint,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return decryptedPath;
|
return decryptedPath;
|
||||||
@@ -480,12 +410,14 @@ class DatabaseFileEncryption {
|
|||||||
try {
|
try {
|
||||||
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
||||||
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
||||||
return (
|
if (
|
||||||
metadata.version === this.VERSION &&
|
metadata.version === this.VERSION &&
|
||||||
metadata.algorithm === this.ALGORITHM
|
metadata.algorithm === this.ALGORITHM
|
||||||
);
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
// .meta parse failed, fall through to single-file detection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,11 +458,32 @@ class DatabaseFileEncryption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
|
|
||||||
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
|
||||||
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
|
||||||
|
|
||||||
const fileStats = fs.statSync(encryptedPath);
|
const fileStats = fs.statSync(encryptedPath);
|
||||||
|
let metadata: EncryptedFileMetadata | null = null;
|
||||||
|
|
||||||
|
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
|
||||||
|
if (fs.existsSync(metadataPath)) {
|
||||||
|
try {
|
||||||
|
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
||||||
|
metadata = JSON.parse(metadataContent);
|
||||||
|
} catch {
|
||||||
|
// .meta parse failed, try single-file format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!metadata) {
|
||||||
|
const fileBuffer = fs.readFileSync(encryptedPath);
|
||||||
|
const metadataLength = fileBuffer.readUInt32BE(0);
|
||||||
|
const metadataEnd = 4 + metadataLength;
|
||||||
|
const metadataJson = fileBuffer
|
||||||
|
.subarray(4, metadataEnd)
|
||||||
|
.toString("utf8");
|
||||||
|
metadata = JSON.parse(metadataJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!metadata) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: metadata.version,
|
version: metadata.version,
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ class FieldCrypto {
|
|||||||
"public_key",
|
"public_key",
|
||||||
"publicKey",
|
"publicKey",
|
||||||
]),
|
]),
|
||||||
|
opkssh_tokens: new Set([
|
||||||
|
"ssh_cert",
|
||||||
|
"sshCert",
|
||||||
|
"private_key",
|
||||||
|
"privateKey",
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
static encryptField(
|
static encryptField(
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { createWriteStream } from "fs";
|
||||||
|
import { pipeline } from "stream/promises";
|
||||||
|
import { systemLogger } from "./logger.js";
|
||||||
|
|
||||||
|
const OPKSSH_REPO = "openpubkey/opkssh";
|
||||||
|
|
||||||
|
function getBinaryDir(): string {
|
||||||
|
const dataDir =
|
||||||
|
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
|
||||||
|
return path.join(dataDir, "opkssh");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionFile(): string {
|
||||||
|
return path.join(getBinaryDir(), "version.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GitHubAsset {
|
||||||
|
name: string;
|
||||||
|
browser_download_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GitHubRelease {
|
||||||
|
tag_name: string;
|
||||||
|
assets: GitHubAsset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OPKSSHBinaryManager {
|
||||||
|
private static binaryPath: string | null = null;
|
||||||
|
|
||||||
|
static async ensureBinary(): Promise<string> {
|
||||||
|
if (this.binaryPath) {
|
||||||
|
return this.binaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryName = this.getBinaryName();
|
||||||
|
const expectedPath = path.join(getBinaryDir(), binaryName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(expectedPath);
|
||||||
|
const needsUpdate = await this.checkForUpdate();
|
||||||
|
if (needsUpdate) {
|
||||||
|
systemLogger.info("Newer OPKSSH version available, updating...", {
|
||||||
|
operation: "opkssh_binary_update_start",
|
||||||
|
});
|
||||||
|
await this.downloadBinary();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.binaryPath = expectedPath;
|
||||||
|
return expectedPath;
|
||||||
|
} catch {
|
||||||
|
systemLogger.info("OPKSSH binary not found, downloading...", {
|
||||||
|
operation: "opkssh_binary_download_start",
|
||||||
|
});
|
||||||
|
await this.downloadBinary();
|
||||||
|
this.binaryPath = expectedPath;
|
||||||
|
return expectedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async downloadBinary(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(getBinaryDir(), { recursive: true });
|
||||||
|
|
||||||
|
const release = await this.getLatestRelease();
|
||||||
|
|
||||||
|
const asset = this.findMatchingAsset(release.assets);
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error(
|
||||||
|
`No matching OPKSSH binary found for platform ${process.platform} ${process.arch}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryName = this.getBinaryName();
|
||||||
|
const binaryPath = path.join(getBinaryDir(), binaryName);
|
||||||
|
|
||||||
|
const response = await fetch(asset.browser_download_url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error("Response body is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = createWriteStream(binaryPath);
|
||||||
|
await pipeline(
|
||||||
|
response.body as unknown as NodeJS.ReadableStream,
|
||||||
|
fileStream,
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.chmod(binaryPath, 0o755);
|
||||||
|
|
||||||
|
await fs.writeFile(getVersionFile(), release.tag_name, "utf8");
|
||||||
|
|
||||||
|
systemLogger.info(
|
||||||
|
`OPKSSH binary downloaded successfully to ${binaryPath}`,
|
||||||
|
{
|
||||||
|
operation: "opkssh_binary_download_complete",
|
||||||
|
path: binaryPath,
|
||||||
|
version: release.tag_name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
systemLogger.error("Failed to download OPKSSH binary", error, {
|
||||||
|
operation: "opkssh_binary_download_error",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getBinaryPath(): string {
|
||||||
|
if (!this.binaryPath) {
|
||||||
|
throw new Error(
|
||||||
|
"OPKSSH binary not initialized. Call ensureBinary() first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.binaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async checkForUpdate(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
let localVersion: string | null = null;
|
||||||
|
try {
|
||||||
|
localVersion = await fs.readFile(getVersionFile(), "utf8");
|
||||||
|
localVersion = localVersion.trim();
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const release = await this.getLatestRelease();
|
||||||
|
const latestVersion = release.tag_name;
|
||||||
|
|
||||||
|
if (localVersion !== latestVersion) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
systemLogger.warn("Failed to check for OPKSSH updates", {
|
||||||
|
operation: "opkssh_update_check_failed",
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getLatestRelease(): Promise<GitHubRelease> {
|
||||||
|
const url = `https://api.github.com/repos/${OPKSSH_REPO}/releases/latest`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Termix",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch release info: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as GitHubRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static findMatchingAsset(assets: GitHubAsset[]): GitHubAsset | null {
|
||||||
|
const platform = process.platform;
|
||||||
|
const arch = process.arch;
|
||||||
|
|
||||||
|
const osMap: Record<string, string> = {
|
||||||
|
win32: "windows",
|
||||||
|
linux: "linux",
|
||||||
|
darwin: "darwin",
|
||||||
|
};
|
||||||
|
|
||||||
|
const archMap: Record<string, string> = {
|
||||||
|
x64: "amd64",
|
||||||
|
arm64: "arm64",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mappedOs = osMap[platform];
|
||||||
|
const mappedArch = archMap[arch];
|
||||||
|
|
||||||
|
if (!mappedOs || !mappedArch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns = [
|
||||||
|
`opkssh-${mappedOs}-${mappedArch}.exe`,
|
||||||
|
`opkssh-${mappedOs}-${mappedArch}`,
|
||||||
|
`opkssh_${mappedOs}_${mappedArch}.exe`,
|
||||||
|
`opkssh_${mappedOs}_${mappedArch}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const asset = assets.find(
|
||||||
|
(a) => a.name.toLowerCase() === pattern.toLowerCase(),
|
||||||
|
);
|
||||||
|
if (asset) {
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getBinaryName(): string {
|
||||||
|
const platform = process.platform;
|
||||||
|
const arch = process.arch;
|
||||||
|
|
||||||
|
const osMap: Record<string, string> = {
|
||||||
|
win32: "windows",
|
||||||
|
linux: "linux",
|
||||||
|
darwin: "darwin",
|
||||||
|
};
|
||||||
|
|
||||||
|
const archMap: Record<string, string> = {
|
||||||
|
x64: "amd64",
|
||||||
|
arm64: "arm64",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mappedOs = osMap[platform] || platform;
|
||||||
|
const mappedArch = archMap[arch] || arch;
|
||||||
|
|
||||||
|
const extension = platform === "win32" ? ".exe" : "";
|
||||||
|
return `opkssh-${mappedOs}-${mappedArch}${extension}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||||
|
import type { Agent } from "http";
|
||||||
|
|
||||||
|
export function getProxyAgent(targetUrl?: string): Agent | undefined {
|
||||||
|
const proxyUrl =
|
||||||
|
process.env.https_proxy ||
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
process.env.HTTP_PROXY;
|
||||||
|
|
||||||
|
if (!proxyUrl) return undefined;
|
||||||
|
|
||||||
|
if (targetUrl) {
|
||||||
|
const noProxy = process.env.no_proxy || process.env.NO_PROXY || "";
|
||||||
|
const hostname = new URL(targetUrl).hostname.toLowerCase();
|
||||||
|
|
||||||
|
for (const entry of noProxy.split(",")) {
|
||||||
|
const trimmed = entry.trim().toLowerCase();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
|
||||||
|
const normalized = trimmed
|
||||||
|
.replace(/^\*\./, "")
|
||||||
|
.replace(/^\./, "");
|
||||||
|
|
||||||
|
if (hostname === normalized || hostname.endsWith(`.${normalized}`)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HttpsProxyAgent(proxyUrl);
|
||||||
|
}
|
||||||
@@ -303,9 +303,8 @@ class UserCrypto {
|
|||||||
await this.storeKEKSalt(userId, newKekSalt);
|
await this.storeKEKSalt(userId, newKekSalt);
|
||||||
await this.storeEncryptedDEK(userId, newEncryptedDEK);
|
await this.storeEncryptedDEK(userId, newEncryptedDEK);
|
||||||
|
|
||||||
const { saveMemoryDatabaseToFile } = await import(
|
const { saveMemoryDatabaseToFile } =
|
||||||
"../database/db/index.js"
|
await import("../database/db/index.js");
|
||||||
);
|
|
||||||
await saveMemoryDatabaseToFile();
|
await saveMemoryDatabaseToFile();
|
||||||
|
|
||||||
oldKEK.fill(0);
|
oldKEK.fill(0);
|
||||||
@@ -341,9 +340,8 @@ class UserCrypto {
|
|||||||
await this.storeKEKSalt(userId, newKekSalt);
|
await this.storeKEKSalt(userId, newKekSalt);
|
||||||
await this.storeEncryptedDEK(userId, newEncryptedDEK);
|
await this.storeEncryptedDEK(userId, newEncryptedDEK);
|
||||||
|
|
||||||
const { saveMemoryDatabaseToFile } = await import(
|
const { saveMemoryDatabaseToFile } =
|
||||||
"../database/db/index.js"
|
await import("../database/db/index.js");
|
||||||
);
|
|
||||||
await saveMemoryDatabaseToFile();
|
await saveMemoryDatabaseToFile();
|
||||||
|
|
||||||
newKEK.fill(0);
|
newKEK.fill(0);
|
||||||
@@ -364,15 +362,6 @@ class UserCrypto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a password-based user's encryption to DUAL-AUTH encryption.
|
|
||||||
* This is used when linking an OIDC account to a password account for dual-auth.
|
|
||||||
*
|
|
||||||
* IMPORTANT: This does NOT delete the password-based KEK salt!
|
|
||||||
* The user needs to maintain BOTH password and OIDC authentication methods.
|
|
||||||
* We keep the password KEK salt so password login still works.
|
|
||||||
* We also store the DEK encrypted with OIDC system key for OIDC login.
|
|
||||||
*/
|
|
||||||
async convertToOIDCEncryption(userId: string): Promise<void> {
|
async convertToOIDCEncryption(userId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const existingEncryptedDEK = await this.getEncryptedDEK(userId);
|
const existingEncryptedDEK = await this.getEncryptedDEK(userId);
|
||||||
@@ -427,9 +416,8 @@ class UserCrypto {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { saveMemoryDatabaseToFile } = await import(
|
const { saveMemoryDatabaseToFile } =
|
||||||
"../database/db/index.js"
|
await import("../database/db/index.js");
|
||||||
);
|
|
||||||
await saveMemoryDatabaseToFile();
|
await saveMemoryDatabaseToFile();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
databaseLogger.error("Failed to convert to OIDC encryption", error, {
|
databaseLogger.error("Failed to convert to OIDC encryption", error, {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { isElectron } from "@/ui/main-axios";
|
import { isElectron } from "@/ui/main-axios";
|
||||||
|
import { getBasePath } from "@/lib/base-path";
|
||||||
|
|
||||||
interface ServiceWorkerState {
|
interface ServiceWorkerState {
|
||||||
isSupported: boolean;
|
isSupported: boolean;
|
||||||
@@ -41,7 +42,9 @@ export function useServiceWorker(): ServiceWorkerState {
|
|||||||
|
|
||||||
const registerSW = async () => {
|
const registerSW = async () => {
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.register("/sw.js");
|
const registration = await navigator.serviceWorker.register(
|
||||||
|
`${getBasePath()}/sw.js`,
|
||||||
|
);
|
||||||
setState((prev) => ({ ...prev, isRegistered: true }));
|
setState((prev) => ({ ...prev, isRegistered: true }));
|
||||||
|
|
||||||
registration.addEventListener("updatefound", () =>
|
registration.addEventListener("updatefound", () =>
|
||||||
|
|||||||
@@ -3,38 +3,40 @@ import { initReactI18next } from "react-i18next";
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
|
||||||
import enTranslation from "../locales/en.json";
|
import enTranslation from "../locales/en.json";
|
||||||
import afTranslation from "../locales/translated/af.json";
|
import afTranslation from "../locales/translated/af_ZA.json";
|
||||||
import arTranslation from "../locales/translated/ar.json";
|
import arTranslation from "../locales/translated/ar_SA.json";
|
||||||
import bnTranslation from "../locales/translated/bn.json";
|
import bnTranslation from "../locales/translated/bn_BD.json";
|
||||||
import bgTranslation from "../locales/translated/bg.json";
|
import bgTranslation from "../locales/translated/bg_BG.json";
|
||||||
import caTranslation from "../locales/translated/ca.json";
|
import caTranslation from "../locales/translated/ca_ES.json";
|
||||||
import csTranslation from "../locales/translated/cs.json";
|
import csTranslation from "../locales/translated/cs_CZ.json";
|
||||||
import daTranslation from "../locales/translated/da.json";
|
import daTranslation from "../locales/translated/da_DK.json";
|
||||||
import deTranslation from "../locales/translated/de.json";
|
import deTranslation from "../locales/translated/de_DE.json";
|
||||||
import elTranslation from "../locales/translated/el.json";
|
import elTranslation from "../locales/translated/el_GR.json";
|
||||||
import esTranslation from "../locales/translated/es.json";
|
import esESTranslation from "../locales/translated/es_ES.json";
|
||||||
import fiTranslation from "../locales/translated/fi.json";
|
import fiTranslation from "../locales/translated/fi_FI.json";
|
||||||
import frTranslation from "../locales/translated/fr.json";
|
import frTranslation from "../locales/translated/fr_FR.json";
|
||||||
import heTranslation from "../locales/translated/he.json";
|
import heTranslation from "../locales/translated/he_IL.json";
|
||||||
import hiTranslation from "../locales/translated/hi.json";
|
import hiTranslation from "../locales/translated/hi_IN.json";
|
||||||
import huTranslation from "../locales/translated/hu.json";
|
import huTranslation from "../locales/translated/hu_HU.json";
|
||||||
import idTranslation from "../locales/translated/id.json";
|
import idTranslation from "../locales/translated/id_ID.json";
|
||||||
import itTranslation from "../locales/translated/it.json";
|
import itTranslation from "../locales/translated/it_IT.json";
|
||||||
import jaTranslation from "../locales/translated/ja.json";
|
import jaTranslation from "../locales/translated/ja_JP.json";
|
||||||
import koTranslation from "../locales/translated/ko.json";
|
import koTranslation from "../locales/translated/ko_KR.json";
|
||||||
import nlTranslation from "../locales/translated/nl.json";
|
import nlTranslation from "../locales/translated/nl_NL.json";
|
||||||
import noTranslation from "../locales/translated/no.json";
|
import noTranslation from "../locales/translated/no_NO.json";
|
||||||
import plTranslation from "../locales/translated/pl.json";
|
import plTranslation from "../locales/translated/pl_PL.json";
|
||||||
import ptTranslation from "../locales/translated/pt.json";
|
import ptPTTranslation from "../locales/translated/pt_PT.json";
|
||||||
import roTranslation from "../locales/translated/ro.json";
|
import ptBRTranslation from "../locales/translated/pt_BR.json";
|
||||||
import ruTranslation from "../locales/translated/ru.json";
|
import roTranslation from "../locales/translated/ro_RO.json";
|
||||||
import srTranslation from "../locales/translated/sr.json";
|
import ruTranslation from "../locales/translated/ru_RU.json";
|
||||||
import svTranslation from "../locales/translated/sv.json";
|
import srTranslation from "../locales/translated/sr_SP.json";
|
||||||
import thTranslation from "../locales/translated/th.json";
|
import svSETranslation from "../locales/translated/sv_SE.json";
|
||||||
import trTranslation from "../locales/translated/tr.json";
|
import thTranslation from "../locales/translated/th_TH.json";
|
||||||
import ukTranslation from "../locales/translated/uk.json";
|
import trTranslation from "../locales/translated/tr_TR.json";
|
||||||
import viTranslation from "../locales/translated/vi.json";
|
import ukTranslation from "../locales/translated/uk_UA.json";
|
||||||
import zhTranslation from "../locales/translated/zh.json";
|
import viTranslation from "../locales/translated/vi_VN.json";
|
||||||
|
import zhCNTranslation from "../locales/translated/zh_CN.json";
|
||||||
|
import zhTWTranslation from "../locales/translated/zh_TW.json";
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
@@ -51,7 +53,7 @@ i18n
|
|||||||
"da",
|
"da",
|
||||||
"de",
|
"de",
|
||||||
"el",
|
"el",
|
||||||
"es",
|
"es-ES",
|
||||||
"fi",
|
"fi",
|
||||||
"fr",
|
"fr",
|
||||||
"he",
|
"he",
|
||||||
@@ -64,16 +66,18 @@ i18n
|
|||||||
"nl",
|
"nl",
|
||||||
"no",
|
"no",
|
||||||
"pl",
|
"pl",
|
||||||
"pt",
|
"pt-PT",
|
||||||
|
"pt-BR",
|
||||||
"ro",
|
"ro",
|
||||||
"ru",
|
"ru",
|
||||||
"sr",
|
"sr",
|
||||||
"sv",
|
"sv-SE",
|
||||||
"th",
|
"th",
|
||||||
"tr",
|
"tr",
|
||||||
"uk",
|
"uk",
|
||||||
"vi",
|
"vi",
|
||||||
"zh",
|
"zh-CN",
|
||||||
|
"zh-TW",
|
||||||
],
|
],
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
debug: false,
|
debug: false,
|
||||||
@@ -117,8 +121,8 @@ i18n
|
|||||||
el: {
|
el: {
|
||||||
translation: elTranslation,
|
translation: elTranslation,
|
||||||
},
|
},
|
||||||
es: {
|
"es-ES": {
|
||||||
translation: esTranslation,
|
translation: esESTranslation,
|
||||||
},
|
},
|
||||||
fi: {
|
fi: {
|
||||||
translation: fiTranslation,
|
translation: fiTranslation,
|
||||||
@@ -156,8 +160,11 @@ i18n
|
|||||||
pl: {
|
pl: {
|
||||||
translation: plTranslation,
|
translation: plTranslation,
|
||||||
},
|
},
|
||||||
pt: {
|
"pt-PT": {
|
||||||
translation: ptTranslation,
|
translation: ptPTTranslation,
|
||||||
|
},
|
||||||
|
"pt-BR": {
|
||||||
|
translation: ptBRTranslation,
|
||||||
},
|
},
|
||||||
ro: {
|
ro: {
|
||||||
translation: roTranslation,
|
translation: roTranslation,
|
||||||
@@ -168,8 +175,8 @@ i18n
|
|||||||
sr: {
|
sr: {
|
||||||
translation: srTranslation,
|
translation: srTranslation,
|
||||||
},
|
},
|
||||||
sv: {
|
"sv-SE": {
|
||||||
translation: svTranslation,
|
translation: svSETranslation,
|
||||||
},
|
},
|
||||||
th: {
|
th: {
|
||||||
translation: thTranslation,
|
translation: thTranslation,
|
||||||
@@ -183,8 +190,11 @@ i18n
|
|||||||
vi: {
|
vi: {
|
||||||
translation: viTranslation,
|
translation: viTranslation,
|
||||||
},
|
},
|
||||||
zh: {
|
"zh-CN": {
|
||||||
translation: zhTranslation,
|
translation: zhCNTranslation,
|
||||||
|
},
|
||||||
|
"zh-TW": {
|
||||||
|
translation: zhTWTranslation,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export function getBasePath(): string {
|
||||||
|
const base = import.meta.env.BASE_URL || "/";
|
||||||
|
if (base === "./" || base === "/") return "";
|
||||||
|
return base.endsWith("/") ? base.slice(0, -1) : base;
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ class DatabaseHealthMonitor {
|
|||||||
private lastCheckTime: number = 0;
|
private lastCheckTime: number = 0;
|
||||||
private checkInProgress: boolean = false;
|
private checkInProgress: boolean = false;
|
||||||
private listeners: Map<string, EventListener[]> = new Map();
|
private listeners: Map<string, EventListener[]> = new Map();
|
||||||
|
private consecutiveErrorTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private confirmedUnhealthy: boolean = false;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@@ -40,10 +42,13 @@ class DatabaseHealthMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reportDatabaseError(error: any, wasAuthenticated: boolean = false) {
|
reportSessionExpired() {
|
||||||
|
this.emit("session-expired", { timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
reportDatabaseError(error: any, _wasAuthenticated: boolean = false) {
|
||||||
const errorMessage = error?.response?.data?.error || error?.message || "";
|
const errorMessage = error?.response?.data?.error || error?.message || "";
|
||||||
const errorCode = error?.response?.data?.code || error?.code;
|
const errorCode = error?.response?.data?.code || error?.code;
|
||||||
const httpStatus = error?.response?.status;
|
|
||||||
|
|
||||||
const isDatabaseError =
|
const isDatabaseError =
|
||||||
errorMessage.toLowerCase().includes("database") ||
|
errorMessage.toLowerCase().includes("database") ||
|
||||||
@@ -58,35 +63,44 @@ class DatabaseHealthMonitor {
|
|||||||
(errorMessage.toLowerCase().includes("network error") &&
|
(errorMessage.toLowerCase().includes("network error") &&
|
||||||
error?.response === undefined);
|
error?.response === undefined);
|
||||||
|
|
||||||
const isAuthenticationLost =
|
if (!(isDatabaseError || isBackendUnreachable)) {
|
||||||
wasAuthenticated &&
|
return;
|
||||||
httpStatus === 401 &&
|
}
|
||||||
(errorCode === "AUTH_REQUIRED" ||
|
|
||||||
errorCode === "SESSION_EXPIRED" ||
|
|
||||||
errorCode === "SESSION_NOT_FOUND" ||
|
|
||||||
errorMessage === "Missing authentication token" ||
|
|
||||||
errorMessage === "Invalid token" ||
|
|
||||||
errorMessage === "Authentication required");
|
|
||||||
|
|
||||||
if (
|
if (this.dbHealthy && !this.consecutiveErrorTimer) {
|
||||||
(isDatabaseError || isBackendUnreachable || isAuthenticationLost) &&
|
this.consecutiveErrorTimer = setTimeout(() => {
|
||||||
this.dbHealthy
|
this.consecutiveErrorTimer = null;
|
||||||
) {
|
if (this.dbHealthy) {
|
||||||
this.dbHealthy = false;
|
this.dbHealthy = false;
|
||||||
|
this.confirmedUnhealthy = true;
|
||||||
this.emit("database-connection-lost", {
|
this.emit("database-connection-lost", {
|
||||||
error: errorMessage || "Backend server unreachable",
|
error: errorMessage || "Backend server unreachable",
|
||||||
code: errorCode,
|
code: errorCode,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reportDatabaseSuccess() {
|
reportDatabaseSuccess() {
|
||||||
if (!this.dbHealthy) {
|
this.clearErrorTimer();
|
||||||
|
|
||||||
|
if (this.confirmedUnhealthy) {
|
||||||
this.dbHealthy = true;
|
this.dbHealthy = true;
|
||||||
|
this.confirmedUnhealthy = false;
|
||||||
this.emit("database-connection-restored", {
|
this.emit("database-connection-restored", {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
} else if (!this.dbHealthy) {
|
||||||
|
this.dbHealthy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearErrorTimer(): void {
|
||||||
|
if (this.consecutiveErrorTimer !== null) {
|
||||||
|
clearTimeout(this.consecutiveErrorTimer);
|
||||||
|
this.consecutiveErrorTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +110,10 @@ class DatabaseHealthMonitor {
|
|||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.dbHealthy = true;
|
this.dbHealthy = true;
|
||||||
|
this.confirmedUnhealthy = false;
|
||||||
this.lastCheckTime = 0;
|
this.lastCheckTime = 0;
|
||||||
this.checkInProgress = false;
|
this.checkInProgress = false;
|
||||||
|
this.clearErrorTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"credentials": {
|
"credentials": {
|
||||||
|
"credentials": "Credentials",
|
||||||
"credentialsViewer": "Credentials Viewer",
|
"credentialsViewer": "Credentials Viewer",
|
||||||
"manageYourSSHCredentials": "Manage your SSH credentials securely",
|
"manageYourSSHCredentials": "Manage your SSH credentials securely",
|
||||||
"addCredential": "Add Credential",
|
"addCredential": "Add Credential",
|
||||||
@@ -99,6 +100,10 @@
|
|||||||
"deploymentProcessDescription": "This will safely add the public key to the target host's ~/.ssh/authorized_keys file without overwriting existing keys. The operation is reversible.",
|
"deploymentProcessDescription": "This will safely add the public key to the target host's ~/.ssh/authorized_keys file without overwriting existing keys. The operation is reversible.",
|
||||||
"chooseHostToDeploy": "Choose a host to deploy to...",
|
"chooseHostToDeploy": "Choose a host to deploy to...",
|
||||||
"deploying": "Deploying...",
|
"deploying": "Deploying...",
|
||||||
|
"copyDeployCommand": "Copy Deploy Command",
|
||||||
|
"copyDeployCommandDescription": "Copy a shell command to manually add the public key to a host's authorized_keys file. Useful when password authentication is disabled.",
|
||||||
|
"deployCommandCopied": "Deploy command copied to clipboard",
|
||||||
|
"manualDeployInfo": "Paste this command in the target host's terminal (e.g., Proxmox console, IPMI, or physical access) to add the SSH key.",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"noHostsAvailable": "No hosts available",
|
"noHostsAvailable": "No hosts available",
|
||||||
"noHostsMatchSearch": "No hosts match your search",
|
"noHostsMatchSearch": "No hosts match your search",
|
||||||
@@ -298,7 +303,9 @@
|
|||||||
"createFolder": "Create Folder",
|
"createFolder": "Create Folder",
|
||||||
"editFolder": "Edit Folder",
|
"editFolder": "Edit Folder",
|
||||||
"editFolderDescription": "Customize your snippet folder",
|
"editFolderDescription": "Customize your snippet folder",
|
||||||
"createFolderDescription": "Organize your snippets into folders"
|
"createFolderDescription": "Organize your snippets into folders",
|
||||||
|
"confirmExecution": "Execute \"{{name}}\"?",
|
||||||
|
"confirmExecutionDesc": "Are you sure you want to execute this snippet?"
|
||||||
},
|
},
|
||||||
"commandHistory": {
|
"commandHistory": {
|
||||||
"title": "History",
|
"title": "History",
|
||||||
@@ -402,6 +409,7 @@
|
|||||||
"required": "Required",
|
"required": "Required",
|
||||||
"optional": "Optional",
|
"optional": "Optional",
|
||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
|
"copied": "Copied",
|
||||||
"connecting": "Connecting...",
|
"connecting": "Connecting...",
|
||||||
"creating": "Creating...",
|
"creating": "Creating...",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@@ -494,6 +502,7 @@
|
|||||||
"checking": "Checking...",
|
"checking": "Checking...",
|
||||||
"checkingDatabase": "Checking database connection...",
|
"checkingDatabase": "Checking database connection...",
|
||||||
"checkingAuthentication": "Checking authentication...",
|
"checkingAuthentication": "Checking authentication...",
|
||||||
|
"backendReconnected": "Server connection restored",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"revoke": "Revoke",
|
"revoke": "Revoke",
|
||||||
@@ -814,6 +823,7 @@
|
|||||||
},
|
},
|
||||||
"hosts": {
|
"hosts": {
|
||||||
"title": "Host Manager",
|
"title": "Host Manager",
|
||||||
|
"hosts": "Hosts",
|
||||||
"sshHosts": "SSH Hosts",
|
"sshHosts": "SSH Hosts",
|
||||||
"noHosts": "No SSH Hosts",
|
"noHosts": "No SSH Hosts",
|
||||||
"noHostsMessage": "You haven't added any SSH hosts yet. Click \"Add Host\" to get started.",
|
"noHostsMessage": "You haven't added any SSH hosts yet. Click \"Add Host\" to get started.",
|
||||||
@@ -844,7 +854,7 @@
|
|||||||
"failedToImportJson": "Failed to import JSON file",
|
"failedToImportJson": "Failed to import JSON file",
|
||||||
"connectionDetails": "Connection Details",
|
"connectionDetails": "Connection Details",
|
||||||
"organization": "Organization",
|
"organization": "Organization",
|
||||||
"ipAddress": "IP Address",
|
"ipAddress": "IP Address or Hostname",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
@@ -923,6 +933,8 @@
|
|||||||
"key": "Key",
|
"key": "Key",
|
||||||
"credential": "Credential",
|
"credential": "Credential",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
|
"opkssh": "OPKSSH",
|
||||||
|
"opksshAuthDescription": "Prompts you for OPKSSH web-auth upon every connect. Your auth token will last for 24 hours until it must be renewed.",
|
||||||
"selectCredential": "Select Credential",
|
"selectCredential": "Select Credential",
|
||||||
"selectCredentialPlaceholder": "Choose a credential...",
|
"selectCredentialPlaceholder": "Choose a credential...",
|
||||||
"credentialRequired": "Credential is required when using credential authentication",
|
"credentialRequired": "Credential is required when using credential authentication",
|
||||||
@@ -1093,6 +1105,7 @@
|
|||||||
"noneAuthTitle": "Keyboard-Interactive Authentication",
|
"noneAuthTitle": "Keyboard-Interactive Authentication",
|
||||||
"noneAuthDescription": "This authentication method will use keyboard-interactive authentication when connecting to the SSH server.",
|
"noneAuthDescription": "This authentication method will use keyboard-interactive authentication when connecting to the SSH server.",
|
||||||
"noneAuthDetails": "Keyboard-interactive authentication allows the server to prompt you for credentials during connection. This is useful for servers that require multi-factor authentication or if you do not want to save credentials locally.",
|
"noneAuthDetails": "Keyboard-interactive authentication allows the server to prompt you for credentials during connection. This is useful for servers that require multi-factor authentication or if you do not want to save credentials locally.",
|
||||||
|
"opksshAuthTitle": "OPKSSH Authentication",
|
||||||
"forceKeyboardInteractive": "Force Keyboard-Interactive",
|
"forceKeyboardInteractive": "Force Keyboard-Interactive",
|
||||||
"forceKeyboardInteractiveDesc": "Forces the use of keyboard-interactive authentication. This is sometimes required for servers that use Two-Factor Authentication (TOTP/2FA).",
|
"forceKeyboardInteractiveDesc": "Forces the use of keyboard-interactive authentication. This is sometimes required for servers that use Two-Factor Authentication (TOTP/2FA).",
|
||||||
"overrideCredentialUsername": "Override Credential Username",
|
"overrideCredentialUsername": "Override Credential Username",
|
||||||
@@ -1349,6 +1362,15 @@
|
|||||||
"warpgateOpenBrowser": "Open in Browser",
|
"warpgateOpenBrowser": "Open in Browser",
|
||||||
"warpgateContinue": "I've Completed Authentication",
|
"warpgateContinue": "I've Completed Authentication",
|
||||||
"warpgateTimeout": "Warpgate authentication timeout. Please reconnect.",
|
"warpgateTimeout": "Warpgate authentication timeout. Please reconnect.",
|
||||||
|
"opksshAuthRequired": "OPKSSH Authentication Required",
|
||||||
|
"opksshAuthDescription": "Complete authentication in your browser to continue. This session will remain valid for 24 hours.",
|
||||||
|
"opksshAuthUrl": "Authentication URL",
|
||||||
|
"opksshOpenBrowser": "Open Browser to Authenticate",
|
||||||
|
"opksshWaitingForAuth": "Waiting for authentication in browser...",
|
||||||
|
"opksshAuthenticating": "Processing authentication...",
|
||||||
|
"opksshTimeout": "Authentication timed out. Please try again.",
|
||||||
|
"opksshAuthFailed": "Authentication failed. Please check your credentials and try again.",
|
||||||
|
"opksshConfigMissing": "OPKSSH configuration not found. Please create ~/.opk/config.yml with your OIDC provider settings. See documentation: https://github.com/openpubkey/opkssh#configuration",
|
||||||
"sudoPasswordPopupTitle": "Insert Password?",
|
"sudoPasswordPopupTitle": "Insert Password?",
|
||||||
"sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss",
|
"sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss",
|
||||||
"sudoPasswordPopupConfirm": "Insert",
|
"sudoPasswordPopupConfirm": "Insert",
|
||||||
@@ -1370,7 +1392,8 @@
|
|||||||
"automaticFallback": "Automatically trying {{method}} authentication...",
|
"automaticFallback": "Automatically trying {{method}} authentication...",
|
||||||
"totpTimeout": "TOTP verification timeout. Please reconnect.",
|
"totpTimeout": "TOTP verification timeout. Please reconnect.",
|
||||||
"passwordTimeout": "Password verification timeout. Please reconnect.",
|
"passwordTimeout": "Password verification timeout. Please reconnect.",
|
||||||
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration."
|
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.",
|
||||||
|
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled."
|
||||||
},
|
},
|
||||||
"fileManager": {
|
"fileManager": {
|
||||||
"title": "File Manager",
|
"title": "File Manager",
|
||||||
@@ -1973,6 +1996,20 @@
|
|||||||
"passwordResetSuccess": "Password Reset Successful",
|
"passwordResetSuccess": "Password Reset Successful",
|
||||||
"passwordResetSuccessDesc": "Your password has been reset successfully. You can now log in with your new password."
|
"passwordResetSuccessDesc": "Your password has been reset successfully. You can now log in with your new password."
|
||||||
},
|
},
|
||||||
|
"hostKey": {
|
||||||
|
"verifyNewHost": "Verify SSH Host Key",
|
||||||
|
"keyChangedWarning": "SSH Host Key Changed",
|
||||||
|
"firstConnectionTitle": "First time connecting to this host",
|
||||||
|
"firstConnectionDescription": "The authenticity of this host cannot be established. Verify the fingerprint matches what you expect.",
|
||||||
|
"keyChangedDescription": "The host key for this server has changed since your last connection. This could indicate a security issue.",
|
||||||
|
"previousKey": "Previous Key",
|
||||||
|
"newFingerprint": "New Fingerprint",
|
||||||
|
"fingerprint": "Fingerprint",
|
||||||
|
"verifyInstructions": "If you trust this host, click Accept to continue and save this fingerprint for future connections.",
|
||||||
|
"securityWarning": "Security Warning",
|
||||||
|
"acceptAndContinue": "Accept & Continue",
|
||||||
|
"acceptNewKey": "Accept New Key & Continue"
|
||||||
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"notFound": "Page not found",
|
"notFound": "Page not found",
|
||||||
"unauthorized": "Unauthorized access",
|
"unauthorized": "Unauthorized access",
|
||||||
@@ -2065,6 +2102,8 @@
|
|||||||
"terminalSettings": "Terminal",
|
"terminalSettings": "Terminal",
|
||||||
"hostSidebarSettings": "Host & Sidebar",
|
"hostSidebarSettings": "Host & Sidebar",
|
||||||
"snippetsSettings": "Snippets",
|
"snippetsSettings": "Snippets",
|
||||||
|
"confirmSnippetExecution": "Confirm Snippet Execution",
|
||||||
|
"confirmSnippetExecutionDesc": "Show confirmation dialog before executing snippets",
|
||||||
"updateSettings": "Updates",
|
"updateSettings": "Updates",
|
||||||
"disableUpdateCheck": "Disable Update Check",
|
"disableUpdateCheck": "Disable Update Check",
|
||||||
"disableUpdateCheckDesc": "Stop checking for new versions on startup and dashboard. Reduces network requests.",
|
"disableUpdateCheckDesc": "Stop checking for new versions on startup and dashboard. Reduces network requests.",
|
||||||
@@ -2085,7 +2124,7 @@
|
|||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"enterCode": "000000",
|
"enterCode": "000000",
|
||||||
"ipAddress": "127.0.0.1",
|
"ipAddress": "192.168.1.1 or example.com",
|
||||||
"port": "22",
|
"port": "22",
|
||||||
"maxRetries": "3",
|
"maxRetries": "3",
|
||||||
"retryInterval": "10",
|
"retryInterval": "10",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface SSHHost {
|
|||||||
folder: string;
|
folder: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
pin: boolean;
|
pin: boolean;
|
||||||
authType: "password" | "key" | "credential" | "none";
|
authType: "password" | "key" | "credential" | "none" | "opkssh";
|
||||||
password?: string;
|
password?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
@@ -95,7 +95,7 @@ export interface SSHHostData {
|
|||||||
folder?: string;
|
folder?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
pin?: boolean;
|
pin?: boolean;
|
||||||
authType: "password" | "key" | "credential" | "none";
|
authType: "password" | "key" | "credential" | "none" | "opkssh";
|
||||||
password?: string;
|
password?: string;
|
||||||
key?: File | null;
|
key?: File | null;
|
||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
@@ -150,7 +150,7 @@ export interface Credential {
|
|||||||
folder?: string;
|
folder?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
authType: "password" | "key";
|
authType: "password" | "key";
|
||||||
username: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
publicKey?: string;
|
publicKey?: string;
|
||||||
@@ -170,7 +170,7 @@ export interface CredentialBackend {
|
|||||||
folder: string | null;
|
folder: string | null;
|
||||||
tags: string;
|
tags: string;
|
||||||
authType: "password" | "key";
|
authType: "password" | "key";
|
||||||
username: string;
|
username: string | null;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
key: string;
|
key: string;
|
||||||
private_key?: string;
|
private_key?: string;
|
||||||
@@ -190,7 +190,7 @@ export interface CredentialData {
|
|||||||
folder?: string;
|
folder?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
authType: "password" | "key";
|
authType: "password" | "key";
|
||||||
username: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
publicKey?: string;
|
publicKey?: string;
|
||||||
@@ -465,7 +465,7 @@ export type ErrorType =
|
|||||||
// AUTHENTICATION TYPES
|
// AUTHENTICATION TYPES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export type AuthType = "password" | "key" | "credential" | "none";
|
export type AuthType = "password" | "key" | "credential" | "none" | "opkssh";
|
||||||
|
|
||||||
export type KeyType = "rsa" | "ecdsa" | "ed25519";
|
export type KeyType = "rsa" | "ecdsa" | "ed25519";
|
||||||
|
|
||||||
@@ -486,11 +486,13 @@ export interface ApiResponse<T = unknown> {
|
|||||||
|
|
||||||
export interface CredentialsManagerProps {
|
export interface CredentialsManagerProps {
|
||||||
onEditCredential?: (credential: Credential) => void;
|
onEditCredential?: (credential: Credential) => void;
|
||||||
|
onAddCredential?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CredentialEditorProps {
|
export interface CredentialEditorProps {
|
||||||
editingCredential?: Credential | null;
|
editingCredential?: Credential | null;
|
||||||
onFormSubmit?: () => void;
|
onFormSubmit?: () => void;
|
||||||
|
onBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CredentialViewerProps {
|
export interface CredentialViewerProps {
|
||||||
@@ -523,6 +525,7 @@ export interface SSHManagerHostEditorProps {
|
|||||||
|
|
||||||
export interface SSHManagerHostViewerProps {
|
export interface SSHManagerHostViewerProps {
|
||||||
onEditHost?: (host: SSHHost) => void;
|
onEditHost?: (host: SSHHost) => void;
|
||||||
|
onAddHost?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HostProps {
|
export interface HostProps {
|
||||||
|
|||||||
@@ -59,12 +59,15 @@ function AppContent({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleDatabaseConnectionLost = () => {
|
const handleDatabaseConnectionLost = () => {
|
||||||
setDbConnectionFailed(true);
|
setDbConnectionFailed(true);
|
||||||
setIsAuthenticated(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDatabaseConnectionRestored = () => {
|
const handleDatabaseConnectionRestored = () => {
|
||||||
setDbConnectionFailed(false);
|
setDbConnectionFailed(false);
|
||||||
window.location.reload();
|
toast.success(t("common.backendReconnected"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSessionExpired = () => {
|
||||||
|
setIsAuthenticated(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
dbHealthMonitor.on(
|
dbHealthMonitor.on(
|
||||||
@@ -75,6 +78,7 @@ function AppContent({
|
|||||||
"database-connection-restored",
|
"database-connection-restored",
|
||||||
handleDatabaseConnectionRestored,
|
handleDatabaseConnectionRestored,
|
||||||
);
|
);
|
||||||
|
dbHealthMonitor.on("session-expired", handleSessionExpired);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dbHealthMonitor.off(
|
dbHealthMonitor.off(
|
||||||
@@ -85,6 +89,7 @@ function AppContent({
|
|||||||
"database-connection-restored",
|
"database-connection-restored",
|
||||||
handleDatabaseConnectionRestored,
|
handleDatabaseConnectionRestored,
|
||||||
);
|
);
|
||||||
|
dbHealthMonitor.off("session-expired", handleSessionExpired);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Download, Upload } from "lucide-react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { isElectron } from "@/ui/main-axios.ts";
|
import { isElectron } from "@/ui/main-axios.ts";
|
||||||
|
import { getBasePath } from "@/lib/base-path";
|
||||||
|
|
||||||
interface DatabaseSecurityTabProps {
|
interface DatabaseSecurityTabProps {
|
||||||
currentUser: {
|
currentUser: {
|
||||||
@@ -56,7 +57,7 @@ export function DatabaseSecurityTab({
|
|||||||
? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/export`
|
? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/export`
|
||||||
: isDev
|
: isDev
|
||||||
? `http://localhost:30001/database/export`
|
? `http://localhost:30001/database/export`
|
||||||
: `${window.location.protocol}//${window.location.host}/database/export`;
|
: `${window.location.protocol}//${window.location.host}${getBasePath()}/database/export`;
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -127,7 +128,7 @@ export function DatabaseSecurityTab({
|
|||||||
? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/import`
|
? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/import`
|
||||||
: isDev
|
: isDev
|
||||||
? `http://localhost:30001/database/import`
|
? `http://localhost:30001/database/import`
|
||||||
: `${window.location.protocol}//${window.location.host}/database/import`;
|
: `${window.location.protocol}//${window.location.host}${getBasePath()}/database/import`;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", importFile);
|
formData.append("file", importFile);
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ export function Dashboard({
|
|||||||
const [totalServers, setTotalServers] = useState<number>(0);
|
const [totalServers, setTotalServers] = useState<number>(0);
|
||||||
const [totalTunnels, setTotalTunnels] = useState<number>(0);
|
const [totalTunnels, setTotalTunnels] = useState<number>(0);
|
||||||
const [totalCredentials, setTotalCredentials] = useState<number>(0);
|
const [totalCredentials, setTotalCredentials] = useState<number>(0);
|
||||||
|
const [updateCheckDisabled, setUpdateCheckDisabled] = useState<boolean>(
|
||||||
|
localStorage.getItem("disableUpdateCheck") === "true",
|
||||||
|
);
|
||||||
const [recentActivity, setRecentActivity] = useState<RecentActivityItem[]>(
|
const [recentActivity, setRecentActivity] = useState<RecentActivityItem[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
@@ -159,9 +162,10 @@ export function Dashboard({
|
|||||||
const uptimeInfo = await getUptime();
|
const uptimeInfo = await getUptime();
|
||||||
setUptime(uptimeInfo.formatted);
|
setUptime(uptimeInfo.formatted);
|
||||||
|
|
||||||
const updateCheckDisabled =
|
const updateDisabled =
|
||||||
localStorage.getItem("disableUpdateCheck") === "true";
|
localStorage.getItem("disableUpdateCheck") === "true";
|
||||||
if (!updateCheckDisabled) {
|
setUpdateCheckDisabled(updateDisabled);
|
||||||
|
if (!updateDisabled) {
|
||||||
const versionInfo = await getVersionInfo();
|
const versionInfo = await getVersionInfo();
|
||||||
setVersionText(`v${versionInfo.localVersion}`);
|
setVersionText(`v${versionInfo.localVersion}`);
|
||||||
if (
|
if (
|
||||||
@@ -170,6 +174,9 @@ export function Dashboard({
|
|||||||
) {
|
) {
|
||||||
setVersionStatus(versionInfo.status);
|
setVersionStatus(versionInfo.status);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const versionInfo = await getVersionInfo();
|
||||||
|
setVersionText(`v${versionInfo.localVersion}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -246,6 +253,10 @@ export function Dashboard({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (host.authType === "opkssh") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const existingSession = viewerSessions.get(host.id);
|
const existingSession = viewerSessions.get(host.id);
|
||||||
let sessionId = existingSession;
|
let sessionId = existingSession;
|
||||||
|
|
||||||
@@ -395,36 +406,40 @@ export function Dashboard({
|
|||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||||
if (sshManagerTab) {
|
if (sshManagerTab) {
|
||||||
updateTab(sshManagerTab.id, {
|
|
||||||
initialTab: "add_host",
|
|
||||||
hostConfig: undefined,
|
|
||||||
});
|
|
||||||
setCurrentTab(sshManagerTab.id);
|
setCurrentTab(sshManagerTab.id);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("host-manager:add-host"));
|
||||||
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
const id = addTab({
|
const id = addTab({
|
||||||
type: "ssh_manager",
|
type: "ssh_manager",
|
||||||
title: "Host Manager",
|
title: "Host Manager",
|
||||||
initialTab: "add_host",
|
initialTab: "hosts",
|
||||||
});
|
});
|
||||||
setCurrentTab(id);
|
setCurrentTab(id);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("host-manager:add-host"));
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddCredential = () => {
|
const handleAddCredential = () => {
|
||||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||||
if (sshManagerTab) {
|
if (sshManagerTab) {
|
||||||
updateTab(sshManagerTab.id, {
|
|
||||||
initialTab: "add_credential",
|
|
||||||
hostConfig: undefined,
|
|
||||||
});
|
|
||||||
setCurrentTab(sshManagerTab.id);
|
setCurrentTab(sshManagerTab.id);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("host-manager:add-credential"));
|
||||||
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
const id = addTab({
|
const id = addTab({
|
||||||
type: "ssh_manager",
|
type: "ssh_manager",
|
||||||
title: "Host Manager",
|
title: "Host Manager",
|
||||||
initialTab: "add_credential",
|
initialTab: "credentials",
|
||||||
});
|
});
|
||||||
setCurrentTab(id);
|
setCurrentTab(id);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("host-manager:add-credential"));
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -558,7 +573,7 @@ export function Dashboard({
|
|||||||
<div
|
<div
|
||||||
className="grid gap-4"
|
className="grid gap-4"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(600px, 1fr))",
|
gridTemplateColumns: "repeat(auto-fit, minmax(540px, 1fr))",
|
||||||
gridAutoRows: "minmax(300px, 1fr)",
|
gridAutoRows: "minmax(300px, 1fr)",
|
||||||
minHeight: "100%",
|
minHeight: "100%",
|
||||||
}}
|
}}
|
||||||
@@ -579,6 +594,7 @@ export function Dashboard({
|
|||||||
totalServers={totalServers}
|
totalServers={totalServers}
|
||||||
totalTunnels={totalTunnels}
|
totalTunnels={totalTunnels}
|
||||||
totalCredentials={totalCredentials}
|
totalCredentials={totalCredentials}
|
||||||
|
updateCheckDisabled={updateCheckDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (card.id === "recent_activity") {
|
} else if (card.id === "recent_activity") {
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ export function NetworkGraphCard({
|
|||||||
setElements([...nodes, ...validEdges]);
|
setElements([...nodes, ...validEdges]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load topology:", err);
|
console.error("Failed to load topology:", err);
|
||||||
setError(t("networkGraph.failedToLoadData"));
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||