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>
No files matched your search
@@ -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 |
@@ -1 +1,29 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" zoomAndPan="magnify" viewBox="0 0 375 374.999991" height="500" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g/><clipPath id="1761bbaa89"><path d="M 0.101562 0 L 375 0 L 375 375 L 0.101562 375 Z M 0.101562 0 " clip-rule="nonzero"/></clipPath><clipPath id="3db8e9a507"><path d="M 57.101562 0 L 318 0 C 333.117188 0 347.617188 6.003906 358.304688 16.695312 C 368.996094 27.382812 375 41.882812 375 57 L 375 318 C 375 333.117188 368.996094 347.617188 358.304688 358.304688 C 347.617188 368.996094 333.117188 375 318 375 L 57.101562 375 C 41.984375 375 27.484375 368.996094 16.796875 358.304688 C 6.105469 347.617188 0.101562 333.117188 0.101562 318 L 0.101562 57 C 0.101562 41.882812 6.105469 27.382812 16.796875 16.695312 C 27.484375 6.003906 41.984375 0 57.101562 0 Z M 57.101562 0 " clip-rule="nonzero"/></clipPath><clipPath id="fb4435a1fb"><path d="M 0.101562 0 L 375 0 L 375 375 L 0.101562 375 Z M 0.101562 0 " clip-rule="nonzero"/></clipPath><clipPath id="573455ae23"><path d="M 57.101562 0 L 318 0 C 333.117188 0 347.617188 6.003906 358.304688 16.695312 C 368.996094 27.382812 375 41.882812 375 57 L 375 318 C 375 333.117188 368.996094 347.617188 358.304688 358.304688 C 347.617188 368.996094 333.117188 375 318 375 L 57.101562 375 C 41.984375 375 27.484375 368.996094 16.796875 358.304688 C 6.105469 347.617188 0.101562 333.117188 0.101562 318 L 0.101562 57 C 0.101562 41.882812 6.105469 27.382812 16.796875 16.695312 C 27.484375 6.003906 41.984375 0 57.101562 0 Z M 57.101562 0 " clip-rule="nonzero"/></clipPath><clipPath id="66023f0a94"><rect x="0" width="375" y="0" height="375"/></clipPath><clipPath id="096103623d"><path d="M 0.101562 0.00390625 L 375 0.00390625 L 375 375 L 0.101562 375 Z M 0.101562 0.00390625 " clip-rule="nonzero"/></clipPath><clipPath id="38e0707f78"><path d="M 57.101562 0.00390625 L 317.996094 0.00390625 C 333.113281 0.00390625 347.613281 6.007812 358.300781 16.695312 C 368.992188 27.386719 374.996094 41.882812 374.996094 57 L 374.996094 318 C 374.996094 333.117188 368.992188 347.613281 358.300781 358.304688 C 347.613281 368.992188 333.113281 374.996094 317.996094 374.996094 L 57.101562 374.996094 C 41.984375 374.996094 27.484375 368.992188 16.796875 358.304688 C 6.105469 347.613281 0.101562 333.117188 0.101562 318 L 0.101562 57 C 0.101562 41.882812 6.105469 27.386719 16.796875 16.695312 C 27.484375 6.007812 41.984375 0.00390625 57.101562 0.00390625 Z M 57.101562 0.00390625 " clip-rule="nonzero"/></clipPath><clipPath id="d42a783983"><rect x="0" width="131" y="0" height="224"/></clipPath><clipPath id="a8ddcf3d5e"><rect x="0" width="115" y="0" height="222"/></clipPath></defs><g clip-path="url(#1761bbaa89)"><g clip-path="url(#3db8e9a507)"><g transform="matrix(1, 0, 0, 1, 0, 0)"><g clip-path="url(#66023f0a94)"><g clip-path="url(#fb4435a1fb)"><g clip-path="url(#573455ae23)"><path fill="#18181b" d="M 0.101562 0 L 375 0 L 375 374.898438 L 0.101562 374.898438 Z M 0.101562 0 " fill-opacity="1" fill-rule="nonzero"/></g></g></g></g></g></g><g clip-path="url(#096103623d)"><g clip-path="url(#38e0707f78)"><path stroke-linecap="butt" transform="matrix(0.75, 0, 0, 0.75, 0.101056, 0.00221852)" fill="none" stroke-linejoin="miter" d="M 76.000681 0.0022503 L 423.860079 0.0022503 C 444.01633 0.0022503 463.349665 8.007459 477.599666 22.25746 C 491.854875 36.512669 499.860084 55.840796 499.860084 75.997047 L 499.860084 423.99707 C 499.860084 444.153321 491.854875 463.481447 477.599666 477.736657 C 463.349665 491.986658 444.01633 499.991866 423.860079 499.991866 L 76.000681 499.991866 C 55.84443 499.991866 36.511095 491.986658 22.261094 477.736657 C 8.005885 463.481447 0.000675892 444.153321 0.000675892 423.99707 L 0.000675892 75.997047 C 0.000675892 55.840796 8.005885 36.512669 22.261094 22.25746 C 36.511095 8.007459 55.84443 0.0022503 76.000681 0.0022503 Z M 76.000681 0.0022503 " stroke="#2e2e30" stroke-width="28" stroke-opacity="1" stroke-miterlimit="4"/></g></g><path stroke-linecap="round" transform="matrix(0.75, -0.000000000000000093, 0.000000000000000093, 0.75, 51.908154, 43.885444)" fill="none" stroke-linejoin="miter" d="M 27.997463 28.001701 L 340.638109 28.001701 " stroke="#e6e6e6" stroke-width="56" stroke-opacity="1" stroke-miterlimit="4"/><path stroke-linecap="round" transform="matrix(0, 0.75, -0.75, 0, 208.892597, 64.889403)" fill="none" stroke-linejoin="miter" d="M 25.001631 24.997423 L 330.152693 24.997423 " stroke="#e6e6e6" stroke-width="50" stroke-opacity="1" stroke-miterlimit="4"/><g transform="matrix(1, 0, 0, 1, 69, 101)"><g clip-path="url(#d42a783983)"><g fill="#14191f" fill-opacity="1"><g transform="translate(6.947029, 160.304978)"><g><path d="M 4.691406 -62.90625 L 68.734375 -87.59375 L 73.878906 -74.238281 L 68.667969 -60.910156 L -4.472656 -89.511719 L -4.472656 -145.9375 L 122 -94.609375 L 122 -54.226562 L -4.472656 -2.894531 L -4.472656 -59.371094 Z M 14.984375 -36.199219 L 9.839844 -49.550781 L 24.Line truncated
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
|
||||||
|
<path d="M0 0 C2.73400004 -0.002639 5.46799221 -0.00826382 8.20198786 -0.01373065 C14.12976744 -0.02399057 20.05751439 -0.02688145 25.98530197 -0.02481842 C34.80090688 -0.02175792 43.61647591 -0.03011562 52.43207382 -0.04056767 C69.09746094 -0.05987554 85.76283572 -0.06553472 102.42823271 -0.06668179 C115.30380751 -0.0676022 128.17937749 -0.07172576 141.05495071 -0.07808304 C144.76625176 -0.07987824 148.47755281 -0.08165787 152.18885386 -0.08343279 C153.11160075 -0.08387489 154.03434764 -0.08431699 154.98505655 -0.08477248 C180.92924305 -0.09707542 206.87342788 -0.10497418 232.81761742 -0.10408592 C233.79511593 -0.10405476 234.77261444 -0.10402361 235.77973412 -0.10399151 C240.73366138 -0.1038253 245.68758864 -0.10362722 250.6415159 -0.10342209 C251.6253128 -0.1033822 252.60910971 -0.10334231 253.62271865 -0.10330121 C255.61219571 -0.10322034 257.60167278 -0.10313674 259.59114985 -0.10305043 C290.52844692 -0.10182554 321.46570011 -0.11682888 352.40298462 -0.14419556 C387.19411554 -0.17495891 421.98521764 -0.19212546 456.77636302 -0.19026911 C460.49080761 -0.19011986 464.20525221 -0.19002244 467.91969681 -0.18996048 C469.29149007 -0.18992552 469.29149007 -0.18992552 470.69099632 -0.18988985 C485.3682461 -0.18982846 500.04545761 -0.20256613 514.72269603 -0.21982501 C529.49125025 -0.23683126 544.25974885 -0.23871645 559.02830748 -0.22560637 C567.78591009 -0.2182501 576.54334301 -0.2217749 585.30092741 -0.24149105 C591.16987765 -0.25357988 597.03870633 -0.25080234 602.90765122 -0.23655005 C606.23912509 -0.22886096 609.57001068 -0.22929279 612.90150099 -0.24346475 C669.26549491 -0.4671585 719.76924989 19.84001892 760.15640259 59.26742554 C772.42844891 71.50006259 782.52075649 85.25415658 791.15640259 100.26742554 C791.50912231 100.87183472 791.86184204 101.4762439 792.22525024 102.09896851 C811.88962254 136.11043906 817.64194919 173.04055363 817.54408264 211.76298523 C817.54660703 214.48240629 817.55362534 217.2018119 817.56040245 219.92122525 C817.57267741 225.79546941 817.57402194 231.66963359 817.56785202 237.54388618 C817.5587013 246.27976136 817.56684044 255.01556352 817.57814326 263.75143313 C817.59774407 279.36124531 817.60017028 294.97102417 817.59494806 310.58084573 C817.59037982 324.24363644 817.59100383 337.90641403 817.59678841 351.56920433 C817.59754529 353.40633989 817.59830127 355.24347545 817.59905635 357.08061102 C817.60020162 359.84946233 817.60135151 362.61831363 817.60251559 365.38716494 C817.61321044 391.1073786 817.61795627 416.82758792 817.61004543 442.54780293 C817.60959967 444.00013607 817.60959967 444.00013607 817.60914491 445.48180929 C817.60669631 453.34299581 817.60408455 461.20418229 817.60146077 469.06536876 C817.5913601 499.72185315 817.59971449 530.37828276 817.62295532 561.03475952 C817.64906362 595.52892829 817.6618664 630.02306408 817.65476757 664.51724309 C817.65405158 668.19956187 817.65337718 671.88188065 817.65272713 675.56419945 C817.65255732 676.47078703 817.65238751 677.37737461 817.65221255 678.31143455 C817.64991573 691.95846782 817.65785694 705.60547034 817.6710186 719.25249672 C817.685971 734.78338018 817.68671805 750.31419187 817.66894769 765.84507386 C817.65942821 774.52050334 817.66072981 783.19576513 817.67818238 791.8711832 C817.68875186 797.68511687 817.68446055 803.49892416 817.66870234 809.31284558 C817.66015583 812.61276916 817.65970357 815.91210367 817.67297166 819.21204952 C817.88436667 875.86359058 797.79480503 926.66414093 758.15640259 967.26742554 C745.93892953 979.52425902 732.17979891 989.68607442 717.15640259 998.26742554 C716.4925354 998.65253296 715.82866821 999.03764038 715.14468384 999.43441772 C695.23288262 1010.86828901 673.71271691 1018.1589933 651.15640259 1022.26742554 C650.28498416 1022.44004335 649.41356574 1022.61266116 648.51574069 1022.79050982 C635.90814607 1024.96518969 623.18430579 1024.55138623 610.43305969 1024.53485107 C607.6999227 1024.53748912 604.9667936 1024.54311399 602.233661 1024.54858172 C596.30466868 1024.55884731 590.375709 1024.56173152 584.44670868 1024.55966949 C575.62928442 1024.55661047 566.81189608 1024.5649635 557.99447882 1024.57541875 C541.32568596 1024.59473226 524.65690543 1024.60038602 507.9881027 1024.60153286 C495.10964357 1024.60245312 482.23118926 1024.60657588 469.3527317 1024.61293411 C465.64116775 1024.61472931 461.92960378 1024.61650893 458.21803981 1024.61828387 C457.29523567 1024.61872596 456.37243152 1024.61916806 455.42166361 1024.61962355 C429.47354925 1024.6319276 403.52543655 1024.63982514 377.57731915 1024.638937 C376.59955462 1024.63890584 375.62179009 1024.63887468 374.61439634 1024.63884258 C369.65907825 1024.63867637 364.70376016 1024.63847829 359.74844208 1024.63827316 C358.76436283 1024.63823327 357.78028358 1024.63819338 356.76638375 1024.63815228 C354.77633502 1024.63807141 352.78628628 1024.63798781 350.79623754 1024.6379015 C319.85239029 1024.63667671 288.9085869 1024.6516778 257.9647522 1024.67904663 C223.16886751 1024.70981007 188.37301164 1024.7269765 153.5771125 1024.72512019 CLine truncated
|
||||||
|
<path d="M0 0 C1.40423182 0.00051035 1.40423182 0.00051035 2.83683196 0.00103101 C3.82595598 -0.0025196 4.81508001 -0.00607022 5.83417749 -0.00972843 C9.19305859 -0.02050902 12.55190967 -0.0240525 15.91080561 -0.02766219 C18.32564927 -0.0338193 20.74049189 -0.04039487 23.15533337 -0.04735598 C29.12496578 -0.0633399 35.09459078 -0.07451946 41.06423627 -0.08363459 C46.02947164 -0.09137043 50.99469774 -0.10144125 55.95992556 -0.11302939 C57.03288597 -0.1155315 57.03288597 -0.1155315 58.12752234 -0.11808417 C59.58627278 -0.12149296 61.04502321 -0.12490448 62.50377363 -0.12831869 C82.24770504 -0.17404715 101.99165615 -0.20448455 121.73561946 -0.23248091 C122.54409117 -0.2336359 123.35256289 -0.23479089 124.18553374 -0.23598088 C156.18819906 -0.28168636 188.19087249 -0.32132586 220.19355964 -0.34800582 C228.12660644 -0.35462936 236.05965303 -0.3615028 243.99269953 -0.36846724 C245.30758824 -0.36961202 245.30758824 -0.36961202 246.64904036 -0.37077993 C275.04965913 -0.39570526 303.45013113 -0.44916217 331.85067361 -0.51848551 C361.0453603 -0.58961736 390.23996808 -0.63467261 419.4347424 -0.64598659 C423.56094367 -0.64759145 427.68714476 -0.64947904 431.81334582 -0.65153113 C432.62541303 -0.65192401 433.43748025 -0.6523169 434.27415556 -0.65272169 C447.33054215 -0.65977305 460.38672234 -0.69395932 473.44302996 -0.73819214 C486.57766984 -0.78206641 499.71208526 -0.79644882 512.8467916 -0.78109888 C520.64442365 -0.77285756 528.4412709 -0.78672339 536.23877507 -0.83281821 C542.07168701 -0.86481521 547.90404453 -0.85466238 553.73700174 -0.8285531 C556.08920992 -0.82449162 558.44148962 -0.83384796 560.79357401 -0.8583189 C579.09200025 -1.03720424 592.84786818 2.26405378 606.57546321 15.14642534 C611.04162409 19.56596659 611.04162409 19.56596659 612.57546321 22.14642534 C612.57546321 23.46642534 612.57546321 24.78642534 612.57546321 26.14642534 C613.23546321 26.14642534 613.89546321 26.14642534 614.57546321 26.14642534 C619.06125557 33.80380806 619.31056144 41.10884428 619.26296321 49.83392534 C619.26159358 50.50217332 619.26022395 51.17042131 619.25881281 51.85891923 C619.18815119 62.55403817 618.32559764 70.52408381 611.57546321 79.14642534 C610.89226008 80.02943315 610.20905696 80.91244096 609.50515071 81.82220659 C604.63307267 87.62979771 599.46895511 91.97760101 592.57546321 95.20892534 C591.05952571 95.93789018 591.05952571 95.93789018 589.51296321 96.68158159 C576.2689793 101.93707205 563.08325643 101.55732802 549.06838313 101.48700151 C546.66384639 101.48608132 544.25930885 101.486485 541.85477248 101.48809728 C535.44511442 101.48877152 529.03562612 101.47144822 522.62600794 101.45026597 C516.60024343 101.43311673 510.57451914 101.43237312 504.54872981 101.43023881 C486.953452 101.41452862 469.35820543 101.3726449 451.76296321 101.33392534 C412.43108821 101.27205034 373.09921321 101.21017534 332.57546321 101.14642534 C332.57546321 234.13642534 332.57546321 367.12642534 332.57546321 504.14642534 C311.12546321 504.14642534 289.67546321 504.14642534 267.57546321 504.14642534 C267.90546321 537.47642534 268.23546321 570.80642534 268.57546321 605.14642534 C300.25546321 605.64142534 300.25546321 605.64142534 332.57546321 606.14642534 C332.67858821 613.61267534 332.78171321 621.07892534 332.88796321 628.77142534 C332.95624323 632.26083696 332.95624323 632.26083696 333.02590266 635.82074174 C333.20342124 651.39816119 331.30646416 663.44206298 321.57546321 676.14642534 C320.89483821 677.11580034 320.21421321 678.08517534 319.51296321 679.08392534 C317.34689201 681.38974306 315.57653653 682.22943071 312.57546321 683.14642534 C312.57546321 683.80642534 312.57546321 684.46642534 312.57546321 685.14642534 C311.91546321 685.14642534 311.25546321 685.14642534 310.57546321 685.14642534 C310.57546321 685.80642534 310.57546321 686.46642534 310.57546321 687.14642534 C309.20261164 687.67042924 309.20261164 687.67042924 307.80202571 688.20501909 C306.61351008 688.66005815 305.42499446 689.11509721 304.20046321 689.58392534 C303.01710383 690.03638627 301.83374446 690.48884721 300.61452571 690.95501909 C297.71510128 692.03914445 297.71510128 692.03914445 295.57546321 693.14642534 C285.24933309 693.80324905 276.05684124 693.82446208 266.57546321 689.14642534 C266.57546321 688.48642534 266.57546321 687.82642534 266.57546321 687.14642534 C265.51005305 687.04201127 265.51005305 687.04201127 264.42311946 686.93548784 C257.70869754 685.07497587 251.36435061 678.07379091 247.88796321 672.14642534 C246.57546321 669.14642534 246.57546321 669.14642534 246.57546321 667.14642534 C245.91546321 667.14642534 245.25546321 667.14642534 244.57546321 667.14642534 C241.81043885 659.58869209 241.42919517 652.37018368 241.45520869 644.4210988 C241.45392705 642.97880046 241.45392705 642.97880046 241.45261952 641.50736478 C241.45097643 638.295317 241.45646692 635.0833218 241.46193781 631.87127885 C241.46221996 629.56485695 241.46206141 627.25843496 241.46149531 624.95201311 C241.46120842 618.69642091 241.46708767 612.44084789 241.47406927 606.18526015 C241.48Line truncated
|
||||||
|
<path d="M0 0 C1.65 0 3.3 0 5 0 C5 0.66 5 1.32 5 2 C5.9590625 2.1546875 5.9590625 2.1546875 6.9375 2.3125 C12.53190183 3.56838613 17.91521936 5.33654348 23 8 C23 8.66 23 9.32 23 10 C23.85851562 10.12246094 24.71703125 10.24492187 25.6015625 10.37109375 C29.00686898 11.00127116 32.10342101 11.93562279 35.375 13.0625 C36.43460937 13.42472656 37.49421875 13.78695313 38.5859375 14.16015625 C39.38257813 14.43730469 40.17921875 14.71445312 41 15 C41 15.66 41 16.32 41 17 C41.61488281 17.03738281 42.22976563 17.07476562 42.86328125 17.11328125 C43.67152344 17.17902344 44.47976563 17.24476563 45.3125 17.3125 C46.11300781 17.37050781 46.91351563 17.42851562 47.73828125 17.48828125 C50.43593563 18.09863138 51.10042243 19.06165554 53 21 C55.49018012 21.99988926 55.49018012 21.99988926 58.1875 22.6875 C59.08855469 22.93886719 59.98960937 23.19023437 60.91796875 23.44921875 C61.60503906 23.63097656 62.29210937 23.81273437 63 24 C63 24.66 63 25.32 63 26 C64.65 26 66.3 26 68 26 C68 26.66 68 27.32 68 28 C69.216875 28.12375 70.43375 28.2475 71.6875 28.375 C75.06479059 28.78891306 77.89756695 29.54573451 81 31 C81 31.66 81 32.32 81 33 C81.928125 33.0825 82.85625 33.165 83.8125 33.25 C89.12534498 33.98242925 94.02778798 36.06686151 99 38 C99 38.99 99 39.98 99 41 C101.97 41.33 104.94 41.66 108 42 C108 42.66 108 43.32 108 44 C109.216875 44.103125 110.43375 44.20625 111.6875 44.3125 C115.50184935 44.63574995 117.7522667 46.02730273 121 48 C123.14968745 48.8169898 125.31856158 49.58451998 127.5 50.3125 C133.53502516 52.40422076 139.22759376 54.89590242 145 57.640625 C148.92721265 59.42014323 152.8789933 60.74274372 157 62 C157 62.66 157 63.32 157 64 C158.010625 64.0825 159.02125 64.165 160.0625 64.25 C163.53239519 64.7397864 165.87249712 66.03039413 168.8515625 67.890625 C172.65571858 69.85495287 176.78979197 70.8552536 180.88671875 72.05078125 C184.4123549 73.12572427 187.6626102 74.44772568 191 76 C191 76.66 191 77.32 191 78 C191.71542969 77.95875 192.43085938 77.9175 193.16796875 77.875 C196.30819586 78.01360313 198.43211348 78.77103509 201.3125 80 C202.19550781 80.37125 203.07851563 80.7425 203.98828125 81.125 C204.65214844 81.41375 205.31601562 81.7025 206 82 C206 82.66 206 83.32 206 84 C208.64 84.33 211.28 84.66 214 85 C214 85.66 214 86.32 214 87 C215.2375 87.226875 216.475 87.45375 217.75 87.6875 C223.81734976 89.02193294 229.325946 91.5275072 235 94 C236.66414626 94.67293346 238.32999182 95.3417486 240 96 C240 96.66 240 97.32 240 98 C241.32 98.33 242.64 98.66 244 99 C244 99.66 244 100.32 244 101 C245.5159375 100.9071875 245.5159375 100.9071875 247.0625 100.8125 C251.39947898 101.01902281 254.07744088 102.21701858 258 104 C259.98821313 104.39764263 261.98732362 104.75455166 264 105 C264 105.66 264 106.32 264 107 C264.639375 107.0825 265.27875 107.165 265.9375 107.25 C268.50012926 107.87758268 270.44458009 108.59426495 272.8125 109.6875 C279.70969554 112.86630951 286.87764831 115.37827545 294 118 C294 118.66 294 119.32 294 120 C294.886875 120.12375 295.77375 120.2475 296.6875 120.375 C303.69767442 121.69767442 303.69767442 121.69767442 306 124 C306.09529612 127.01350238 306.12551904 130.00404106 306.11352539 133.01782227 C306.11367142 133.96406464 306.11381744 134.91030701 306.1139679 135.88522339 C306.11326836 139.02897392 306.10547409 142.17266601 306.09765625 145.31640625 C306.09579221 147.48974068 306.09436824 149.66307552 306.09336853 151.83641052 C306.08954221 157.56901195 306.07971097 163.30158823 306.06866455 169.03417969 C306.05845287 174.87890279 306.05387143 180.72362993 306.04882812 186.56835938 C306.0380888 198.045584 306.02101102 209.52278991 306 221 C303.09399739 222.25664978 301.20395416 223 298 223 C298 223.66 298 224.32 298 225 C295.03 225.66 292.06 226.32 289 227 C289 227.66 289 228.32 289 229 C285.26028463 230.44339892 281.62167556 231.83262581 277.75 232.875 C273.2496418 234.22510746 269.05993179 236.07094172 264.76953125 237.9765625 C262 239 262 239 258 239 C258 239.66 258 240.32 258 241 C257.1646875 241.27263672 257.1646875 241.27263672 256.3125 241.55078125 C248.58964059 244.09319952 248.58964059 244.09319952 241 247 C240.67 247.66 240.34 248.32 240 249 C238.11105677 249.74208484 236.21777695 250.47493307 234.3046875 251.15234375 C232.02624136 251.99034852 229.8353074 252.93460251 227.625 253.9375 C224.32911372 255.41693595 221.02373953 256.86357447 217.6875 258.25 C216.88699219 258.58515625 216.08648437 258.9203125 215.26171875 259.265625 C213 260 213 260 209 260 C209 260.66 209 261.32 209 262 C207.88625 262.4125 206.7725 262.825 205.625 263.25 C202.39899591 264.45832662 199.19572477 265.71379498 196 267 C193.01202799 268.20247654 190.01974967 269.37977028 187 270.5 C178.56870095 273.62992779 170.2626406 277.10032586 161.9375 280.5 C161.00873047 280.87769531 160.07996094 281.25539063 159.12304688 281.64453125 C154.04116178 283.72008011 148.99851079 285.85665669 143.99926758 288.12451172 C140.71811867 289.56134161 137.36265982 290.76702473 134 292 C133.113125 292.391875 132.22625 292.78375 131.3125 2Line truncated
|
||||||
|
<path d="M0 0 C1.32 0 2.64 0 4 0 C4 0.66 4 1.32 4 2 C4.59425781 2.17015625 5.18851562 2.3403125 5.80078125 2.515625 C14.03088043 4.92193705 22.0506053 7.78884415 30 11 C30 11.66 30 12.32 30 13 C30.763125 13.0825 31.52625 13.165 32.3125 13.25 C41.03712852 15.02450072 49.04834601 18.83619356 57.0546875 22.6328125 C61.74551929 24.81025431 65.8589058 26.28263802 71 27 C71 27.66 71 28.32 71 29 C71.5775 29.144375 72.155 29.28875 72.75 29.4375 C75.875 30.21875 75.875 30.21875 79 31 C79 31.66 79 32.32 79 33 C79.845625 33.0825 80.69125 33.165 81.5625 33.25 C84.94816006 33.98868947 87.1679512 35.23316228 90.08984375 37.03125 C92.20090329 38.1018896 94.05027523 38.51702804 96.375 38.9375 C100.78658503 39.76888529 104.00591761 41.5746091 107.8359375 43.8515625 C110.36891446 45.19577771 112.89143377 45.70830903 115.68359375 46.29296875 C118.75695192 47.23104266 121.40817621 48.6884795 124.25 50.1875 C130.92469726 53.70757728 137.81399628 56.18579238 144.98339844 58.5144043 C148.81489863 59.78075405 152.3200305 61.06832861 155.7890625 63.1484375 C158.76224988 64.86290321 160.59810208 65.53188262 163.9375 65.9375 C168.43366109 66.69933062 171.342726 68.21955324 175.07421875 70.76953125 C178.8471118 73.18020328 183.08817864 74.39052675 187.328125 75.7421875 C188.49601562 76.11730469 188.49601562 76.11730469 189.6875 76.5 C190.39261719 76.7165625 191.09773437 76.933125 191.82421875 77.15625 C194 78 194 78 196.37109375 79.43359375 C202.28631102 82.95811399 209.20600207 85.36493689 215.875 87.0625 C219 88 219 88 222.3125 89.9375 C226.84415671 92.6470648 226.84415671 92.6470648 236 95 C236 112.16 236 129.32 236 147 C228.13477975 151.49441157 228.13477975 151.49441157 224.3125 152.1875 C223.58675781 152.32542969 222.86101562 152.46335937 222.11328125 152.60546875 C220.67602299 152.87379238 219.23564618 153.12581399 217.79296875 153.36328125 C217.20128906 153.57339844 216.60960937 153.78351563 216 154 C215.67 154.99 215.34 155.98 215 157 C212.41796875 158.09765625 212.41796875 158.09765625 209.1875 159.0625 C205.59909404 160.15082288 202.32570343 161.27582837 199 163 C197.38773015 163.47024537 195.77452619 163.93757854 194.15625 164.38671875 C185.85630305 166.74739207 178.06683002 169.72029401 170.33984375 173.5378418 C167.30328348 175.0185775 164.37352911 176.23370456 161.11328125 177.0859375 C159.57220703 177.53839844 159.57220703 177.53839844 158 178 C157.67 178.66 157.34 179.32 157 180 C154.52984421 181.04037849 152.08512956 181.98065187 149.5625 182.875 C148.08819029 183.41049192 146.61423048 183.94694816 145.140625 184.484375 C144.42455078 184.74412109 143.70847656 185.00386719 142.97070312 185.27148438 C140.55887129 186.16307335 138.18543043 187.13496942 135.8125 188.125 C131.00122041 190.08696003 126.06950487 191.5404007 121.078125 192.97265625 C117.91500582 193.94520247 117.91500582 193.94520247 114.796875 195.62109375 C112 197 112 197 109 197 C108.67 197.99 108.34 198.98 108 200 C106.21484375 200.625 106.21484375 200.625 103.9375 201 C99.34708416 201.88135984 95.30113259 203.15665746 91 205 C90.67 205.66 90.34 206.32 90 207 C88.12988281 207.9296875 88.12988281 207.9296875 85.703125 208.875 C84.8265625 209.22046875 83.95 209.5659375 83.046875 209.921875 C82.12390625 210.27765625 81.2009375 210.6334375 80.25 211 C79.3425 211.35578125 78.435 211.7115625 77.5 212.078125 C75.79712731 212.74533243 74.09313765 213.40970098 72.38769531 214.0703125 C70.7198096 214.71973058 69.05809239 215.38511668 67.40136719 216.0625 C65 217 65 217 61.75 217.875 C57.96339034 219.0109829 54.57204208 220.49171411 51.015625 222.1953125 C49 223 49 223 46 223 C46 223.66 46 224.32 46 225 C25.78125 232.90625 25.78125 232.90625 19 234 C19 234.66 19 235.32 19 236 C17.15047952 236.82302143 15.29511436 237.63291366 13.4375 238.4375 C12.40496094 238.88996094 11.37242187 239.34242188 10.30859375 239.80859375 C6.84845419 241.0545708 3.63685493 241.5300587 0 242 C-0.06943556 234.21156603 -0.12277821 226.42320423 -0.15543652 218.63453865 C-0.17110901 215.0181296 -0.19237553 211.40193192 -0.22631836 207.78564453 C-0.25885631 204.29730012 -0.27684108 200.80915932 -0.28463173 197.3206749 C-0.29289269 195.33856231 -0.31632087 193.35653191 -0.33988953 191.37454224 C-0.34011612 190.17344818 -0.34034271 188.97235413 -0.34057617 187.73486328 C-0.34723801 186.67341095 -0.35389984 185.61195862 -0.36076355 184.51834106 C-0.24171158 183.68728851 -0.12265961 182.85623596 0 182 C3 180 3 180 6 180 C6 179.34 6 178.68 6 178 C7.32451172 177.65388672 7.32451172 177.65388672 8.67578125 177.30078125 C15.48290775 175.45093914 21.89314175 173.34586589 28.2734375 170.296875 C35.35610061 166.92804383 42.45740949 164.13469543 50 162 C52.31944799 160.97928402 54.60816608 159.91361012 56.88671875 158.8046875 C59 158 59 158 63 158 C63 157.34 63 156.68 63 156 C63.60328125 155.86722656 64.2065625 155.73445312 64.828125 155.59765625 C66.02695312 155.33275391 66.02695312 155.33275391 67.25 155.0625 C68.03890625 154.88847656 68.8278125 154.71445312 69.640625 154.53515625 C70.80851562 154.270Line truncated
|
||||||
|
<path d="M0 0 C12.01719911 0.01365142 24.03440239 0.01874096 36.05160737 0.02450979 C54.80253918 0.03372043 73.55344276 0.05369526 92.30437303 0.07202661 C92.30437303 34.06202661 92.30437303 68.05202661 92.30437303 103.07202661 C51.07884714 103.11232964 51.07884714 103.11232964 33.31999803 103.12085474 C21.23047528 103.12670618 9.14095977 103.13347291 -2.94855666 103.14746606 C-74.27785064 103.22842372 -74.27785064 103.22842372 -103.14972854 102.84497583 C-103.88451289 102.83521793 -104.61929724 102.82546003 -105.37634778 102.81540644 C-119.81699678 102.61855942 -134.2560097 102.33467791 -148.69562697 102.07202661 C-148.69562697 68.74202661 -148.69562697 35.41202661 -148.69562697 1.07202661 C-114.45674119 0.44923393 -114.45674119 0.44923393 -103.14972854 0.29907739 C-102.35742103 0.28855474 -101.56511353 0.27803209 -100.7487967 0.26719058 C-67.16855577 -0.16690708 -33.58227694 -0.03887021 0 0 Z " fill="#14191F" transform="translate(641.6956269741058,696.9279733896255)"/>
|
||||||
|
<path d="M0 0 C56.43 0 112.86 0 171 0 C171 10.56 171 21.12 171 32 C114.57 32 58.14 32 0 32 C0 21.44 0 10.88 0 0 Z " fill="#727272" transform="translate(528,733)"/>
|
||||||
|
<path d="M0 0 C79.86 0 159.72 0 242 0 C242 0.33 242 0.66 242 1 C162.8 1 83.6 1 2 1 C2 133.99 2 266.98 2 404 C-19.45 404 -40.9 404 -63 404 C-62.67 437.33 -62.34 470.66 -62 505 C-30.32 505.495 -30.32 505.495 2 506 C2.04125 513.6725 2.0825 521.345 2.125 529.25 C2.14320801 531.66731445 2.16141602 534.08462891 2.18017578 536.57519531 C2.18601743 538.49283628 2.19111377 540.41047973 2.1953125 542.328125 C2.20554443 543.32247559 2.21577637 544.31682617 2.22631836 545.34130859 C2.22668091 546.77192627 2.22668091 546.77192627 2.22705078 548.23144531 C2.231492 549.05751282 2.23593323 549.88358032 2.24050903 550.73468018 C2 553 2 553 0 557 C0.33 540.5 0.66 524 1 507 C-20.45 507 -41.9 507 -64 507 C-64 472.35 -64 437.7 -64 402 C-42.88 402 -21.76 402 0 402 C0 269.34 0 136.68 0 0 Z " fill="#A0A0A1" transform="translate(556,294)"/>
|
||||||
|
<path d="M0 0 C57.42 0 114.84 0 174 0 C174 11.55 174 23.1 174 35 C151.46087147 35.02254502 128.92175202 35.040978 106.38261509 35.05181217 C95.91807719 35.05697564 85.45354677 35.06401929 74.98901367 35.07543945 C65.87160565 35.08538476 56.75420304 35.09185306 47.63678974 35.09408849 C42.80606124 35.09539586 37.97534825 35.09848336 33.14462471 35.10573006 C28.60326313 35.11248785 24.06192353 35.11461746 19.5205574 35.11310768 C17.84875431 35.11338412 16.17695052 35.11539066 14.50515175 35.11920547 C12.23274147 35.12413898 9.96041554 35.12299958 7.68800354 35.12025452 C6.41316384 35.12117631 5.13832415 35.1220981 3.82485294 35.12304783 C1 35 1 35 0 34 C0 22.78 0 11.56 0 0 Z M2 1 C2 11.56 2 22.12 2 33 C58.43 33 114.86 33 173 33 C173 22.44 173 11.88 173 1 C116.57 1 60.14 1 2 1 Z " fill="#3D4044" transform="translate(526,732)"/>
|
||||||
|
<path d="M0 0 C80.19 0 160.38 0 243 0 C243.33 55.77 243.66 111.54 244 169 C245.98 169.99 247.96 170.98 250 172 C250 172.33 250 172.66 250 173 C248.35 173 246.7 173 245 173 C245 172.34 245 171.68 245 171 C244.01 170.67 243.02 170.34 242 170 C242 114.23 242 58.46 242 1 C162.14 1 82.28 1 0 1 C0 0.67 0 0.34 0 0 Z " fill="#4B4B4B" transform="translate(224,295)"/>
|
||||||
|
<path d="M0 0 C1.485 0.99 1.485 0.99 3 2 C3.25616216 5.05290084 3.31849805 7.89702233 3.2487793 10.94775391 C3.23946381 11.87463516 3.23014832 12.80151642 3.22055054 13.75648499 C3.1860853 16.83020963 3.13020737 19.90316065 3.07421875 22.9765625 C3.04619629 25.10361061 3.01950476 27.23067664 2.99409485 29.35775757 C2.9235311 34.96399488 2.83482971 40.56980591 2.74243164 46.17572021 C2.63537976 52.89774556 2.55071052 59.62008508 2.46231842 66.34237671 C2.32541953 76.56192943 2.15997827 86.78066503 2 97 C21.8 97 41.6 97 62 97 C62 97.33 62 97.66 62 98 C42.2 98 22.4 98 2 98 C2 98.66 2 99.32 2 100 C1.34 100 0.68 100 0 100 C0 67 0 34 0 0 Z " fill="#0A0E15" transform="translate(494,699)"/>
|
||||||
|
<path d="M0 0 C0.33 0 0.66 0 1 0 C1.33 55.77 1.66 111.54 2 169 C3.98 169.99 5.96 170.98 8 172 C8 172.33 8 172.66 8 173 C6.35 173 4.7 173 3 173 C3 172.34 3 171.68 3 171 C2.01 170.67 1.02 170.34 0 170 C0 113.9 0 57.8 0 0 Z " fill="#717171" transform="translate(466,295)"/>
|
||||||
|
<path d="M0 0 C0.66 0.33 1.32 0.66 2 1 C1.67 20.14 1.34 39.28 1 59 C3.64 58.01 6.28 57.02 9 56 C10.32 56.33 11.64 56.66 13 57 C11.23141309 57.67260351 9.46003984 58.33788395 7.6875 59 C6.70136719 59.37125 5.71523438 59.7425 4.69921875 60.125 C2 61 2 61 -1 61 C-1.02312792 53.19424805 -1.04091738 45.38850405 -1.05181217 37.58272648 C-1.05704002 33.9585404 -1.06413587 30.3343777 -1.07543945 26.71020508 C-1.08627184 23.21540025 -1.09227758 19.72061796 -1.09487724 16.22579765 C-1.09763551 14.23760611 -1.10544688 12.24942369 -1.11329651 10.26124573 C-1.11337204 9.06007614 -1.11344757 7.85890656 -1.11352539 6.62133789 C-1.115746 5.55766495 -1.11796661 4.493992 -1.12025452 3.39808655 C-1 1 -1 1 0 0 Z " fill="#049652" transform="translate(261,599)"/>
|
||||||
|
<path d="M0 0 C1.32 0 2.64 0 4 0 C4 0.66 4 1.32 4 2 C4.66 2.33 5.32 2.66 6 3 C5.01 3.33 4.02 3.66 3 4 C2.67 23.14 2.34 42.28 2 62 C1.34 62 0.68 62 0 62 C0 41.54 0 21.08 0 0 Z " fill="#049853" transform="translate(260,418)"/>
|
||||||
|
<path d="M0 0 C-1.65 0.33 -3.3 0.66 -5 1 C-5 1.66 -5 2.32 -5 3 C-6.65 3 -8.3 3 -10 3 C-10 3.66 -10 4.32 -10 5 C-10.9178125 5.32613281 -11.835625 5.65226563 -12.78125 5.98828125 C-14.62203125 6.64376953 -14.62203125 6.64376953 -16.5 7.3125 C-17.1384082 7.5396167 -17.77681641 7.7667334 -18.43457031 8.00073242 C-21.88861021 9.23312895 -25.32784181 10.49433536 -28.75 11.8125 C-32 13 -32 13 -34 13 C-34 12.34 -34 11.68 -34 11 C-32.58603098 10.49242138 -31.16868271 9.99425075 -29.75 9.5 C-28.96109375 9.2215625 -28.1721875 8.943125 -27.359375 8.65625 C-25 8 -25 8 -21 8 C-21 7.34 -21 6.68 -21 6 C-17.49223216 4.39644899 -14.85645121 3.77963136 -11 4 C-11 3.34 -11 2.68 -11 2 C-9.54494642 1.47007308 -8.08581873 0.9513211 -6.625 0.4375 C-5.81289063 0.14746094 -5.00078125 -0.14257812 -4.1640625 -0.44140625 C-2 -1 -2 -1 0 0 Z " fill="#25272A" transform="translate(309,544)"/>
|
||||||
|
<path d="M0 0 C0 0.66 0 1.32 0 2 C-0.89460938 2.31453125 -1.78921875 2.6290625 -2.7109375 2.953125 C-8.35592004 4.95113588 -13.96926436 6.94975521 -19.5 9.25 C-22.98649532 10.67681657 -26.22664243 11.60280447 -30 12 C-30 11.34 -30 10.68 -30 10 C-28.60974609 9.58427734 -28.60974609 9.58427734 -27.19140625 9.16015625 C-25.99386719 8.79792969 -24.79632813 8.43570313 -23.5625 8.0625 C-21.77005859 7.52302734 -21.77005859 7.52302734 -19.94140625 6.97265625 C-17.14132455 6.29110858 -17.14132455 6.29110858 -16 5 C-14.33382885 4.95936168 -12.66611905 4.957279 -11 5 C-11 4.34 -11 3.68 -11 3 C-9.54293274 2.49632243 -8.08429904 1.99717444 -6.625 1.5 C-5.81289062 1.2215625 -5.00078125 0.943125 -4.1640625 0.65625 C-2 0 -2 0 0 0 Z " fill="#25272A" transform="translate(257,563)"/>
|
||||||
|
<path d="M0 0 C0.66 0.33 1.32 0.66 2 1 C-2.99893918 3.7266941 -7.31292741 5.43542946 -12.89453125 6.4921875 C-15.28068151 6.98141752 -15.28068151 6.98141752 -18.3125 8.6875 C-21.33484121 10.1635271 -21.94813661 9.96956854 -25 9 C-17.71998049 6.15325357 -10.44504388 3.38499241 -3 1 C-2.01 0.67 -1.02 0.34 0 0 Z " fill="#087F48" transform="translate(394,606)"/>
|
||||||
|
<path d="M0 0 C0.66 0.33 1.32 0.66 2 1 C1.360625 1.28875 0.72125 1.5775 0.0625 1.875 C-2.15206079 2.82823451 -2.15206079 2.82823451 -3 5 C-8.04706509 7.17545909 -12.5487185 8.43215818 -18 9 C-16.44464191 5.88928381 -13.88562714 5.66683615 -10.6875 4.5 C-6.10637354 2.84526208 -6.10637354 2.84526208 -1.70703125 0.78125 C-1.14371094 0.5234375 -0.58039063 0.265625 0 0 Z " fill="#0B5C3A" transform="translate(448,583)"/>
|
||||||
|
<path d="M0 0 C0 0.66 0 1.32 0 2 C-1.65 2 -3.3 2 -5 2 C-5 2.66 -5 3.32 -5 4 C-6.4117783 4.53394179 -7.82970244 5.05165104 -9.25 5.5625 C-10.03890625 5.85253906 -10.8278125 6.14257812 -11.640625 6.44140625 C-14.19103452 7.04522837 -15.55322229 6.8473932 -18 6 C-12.33627962 2.96586408 -6.59531296 0 0 0 Z " fill="#087946" transform="translate(380,553)"/>
|
||||||
|
<path d="M0 0 C1.98 0 3.96 0 6 0 C4.13497412 2.62799102 2.69589209 3.86365751 -0.4921875 4.48828125 C-2.9964272 4.72593573 -5.48653979 4.9040664 -8 5 C-5.08235 3.0549 -3.37462715 2.54723683 0 2 C0 1.34 0 0.68 0 0 Z " fill="#07834A" transform="translate(388,548)"/>
|
||||||
|
<path d="M0 0 C2.0625 0.4375 2.0625 0.4375 4 1 C1.35261084 2.4606285 0.10551666 3 -3 3 C-3 3.66 -3 4.32 -3 5 C-6.4019487 7.09350689 -9.05272412 7.17942163 -13 7 C-9.84906684 4.24293349 -6.60395471 2.99320144 -2.70703125 1.6484375 C-0.99486039 1.13150184 -0.99486039 1.13150184 0 0 Z " fill="#0B623C" transform="translate(319,635)"/>
|
||||||
|
<path d="M0 0 C-2.58411731 1.72274487 -4.54902656 2.68895522 -7.4375 3.6875 C-8.19933594 3.95949219 -8.96117187 4.23148438 -9.74609375 4.51171875 C-12.21829721 5.04729142 -13.62983284 4.80445309 -16 4 C-14.08959948 3.1319092 -12.17139263 2.28098217 -10.25 1.4375 C-9.18265625 0.96183594 -8.1153125 0.48617187 -7.015625 -0.00390625 C-4.01630163 -0.9946154 -2.86578759 -1.16161779 0 0 Z " fill="#24262A" transform="translate(295,685)"/>
|
||||||
|
<path d="M0 0 C2.31 0.33 4.62 0.66 7 1 C7 1.66 7 2.32 7 3 C8.32 3 9.64 3 11 3 C11 3.66 11 4.32 11 5 C11.99 5.33 12.98 5.66 14 6 C9.50540476 5.29771949 5.09389959 3.99912387 1 2 C0.67 1.34 0.34 0.68 0 0 Z " fill="#0B5E3B" transform="translate(387,469)"/>
|
||||||
|
<path d="M0 0 C-2.52480693 2.52480693 -4.13823034 2.55738633 -7.625 3.125 C-8.62789063 3.29257812 -9.63078125 3.46015625 -10.6640625 3.6328125 C-11.43492187 3.75398437 -12.20578125 3.87515625 -13 4 C-11.72807637 1.45615274 -10.80051531 1.30631844 -8.1875 0.3125 C-7.50042969 0.04050781 -6.81335938 -0.23148438 -6.10546875 -0.51171875 C-3.70623504 -1.06812731 -2.3105636 -0.77364938 0 0 Z " fill="#0A673F" transform="translate(363,620)"/>
|
||||||
|
<path d="M0 0 C-1.24176695 0.70226141 -2.49385599 1.38629109 -3.75 2.0625 C-4.79414063 2.63677734 -4.79414063 2.63677734 -5.859375 3.22265625 C-8.36140628 4.1312406 -9.5332355 3.88336837 -12 3 C-9.84267191 0.84267191 -8.88177557 0.51705175 -6 -0.1875 C-5.29875 -0.36667969 -4.5975 -0.54585937 -3.875 -0.73046875 C-2 -1 -2 -1 0 0 Z " fill="#0D4E34" transform="translate(396,607)"/>
|
||||||
|
<path d="M0 0 C9.71428571 1.71428571 9.71428571 1.71428571 13 5 C11.35 5 9.7 5 8 5 C8 4.34 8 3.68 8 3 C7.236875 2.87625 6.47375 2.7525 5.6875 2.625 C3 2 3 2 0 0 Z " fill="#0B623D" transform="translate(338,510)"/>
|
||||||
|
<path d="M0 0 C-0.33 0.99 -0.66 1.98 -1 3 C-4.28976808 4.09658936 -5.71303767 3.79953138 -9 3 C-4.5 0 -4.5 0 0 0 Z " fill="#BEC0C1" transform="translate(523,592)"/>
|
||||||
|
</svg>
|
||||||
|
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",
|
||||||
{
|
userId,
|
||||||
operation: "credential_update_success",
|
credentialId: parseInt(id),
|
||||||
userId,
|
});
|
||||||
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",
|
||||||
{
|
userId,
|
||||||
operation: "credential_delete_success",
|
credentialId: parseInt(id),
|
||||||
userId,
|
});
|
||||||
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}`,
|
userId,
|
||||||
{
|
snippetId: parseInt(id),
|
||||||
operation: "snippet_update_success",
|
});
|
||||||
userId,
|
|
||||||
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}`,
|
userId,
|
||||||
{
|
snippetId: parseInt(id),
|
||||||
operation: "snippet_delete_success",
|
});
|
||||||
userId,
|
|
||||||
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 row = db.$client
|
const envConfig = getOIDCConfigFromEnv();
|
||||||
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
let config;
|
||||||
.get();
|
|
||||||
if (!row) {
|
|
||||||
return res.status(404).json({ error: "OIDC not configured" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = JSON.parse((row as Record<string, unknown>).value as string);
|
if (envConfig) {
|
||||||
|
config = envConfig;
|
||||||
|
} else {
|
||||||
|
const row = db.$client
|
||||||
|
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
||||||
|
.get();
|
||||||
|
if (!row) {
|
||||||
|
return res.status(404).json({ error: "OIDC not configured" });
|
||||||
|
}
|
||||||
|
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 configRow = db.$client
|
const envConfig = getOIDCConfigFromEnv();
|
||||||
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
let config;
|
||||||
.get();
|
|
||||||
if (!configRow) {
|
|
||||||
return res.status(500).json({ error: "OIDC not configured" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = JSON.parse(
|
if (envConfig) {
|
||||||
(configRow as Record<string, unknown>).value as string,
|
config = envConfig;
|
||||||
);
|
} else {
|
||||||
|
const configRow = db.$client
|
||||||
|
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
||||||
|
.get();
|
||||||
|
if (!configRow) {
|
||||||
|
return res.status(500).json({ error: "OIDC not configured" });
|
||||||
|
}
|
||||||
|
config = JSON.parse(
|
||||||
|
(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",
|
operation: "docker_exec",
|
||||||
err,
|
sessionId,
|
||||||
{
|
containerId,
|
||||||
operation: "docker_exec",
|
});
|
||||||
sessionId,
|
|
||||||
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",
|
||||||
{
|
sessionId,
|
||||||
operation: "docker_connect_auth",
|
});
|
||||||
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",
|
||||||
{
|
sessionId,
|
||||||
operation: "docker_connect",
|
hostId,
|
||||||
sessionId,
|
});
|
||||||
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",
|
||||||
{
|
sessionId,
|
||||||
operation: "docker_warpgate_auth",
|
});
|
||||||
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.emit("database-connection-lost", {
|
this.confirmedUnhealthy = true;
|
||||||
error: errorMessage || "Backend server unreachable",
|
this.emit("database-connection-lost", {
|
||||||
code: errorCode,
|
error: errorMessage || "Backend server unreachable",
|
||||||
timestamp: Date.now(),
|
code: errorCode,
|
||||||
});
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||