mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
+7






![dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)


Luke Gustafson
emreumar
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ZacharyZcR
Russell Poovey
russell
Subedi Bibek
Claude Fable 5
Alexander Elsner
SankeerthNara
Stephan Groth
DivByZero
devdanetra
Aleksandr Fominykh
sash
lhojun
Yunus Emre Umar
ddbdd5c437
* chore(deps): bump node from 24-slim to 26-slim in /docker in the docker-major-updates group (#1021) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * chore(deps): bump node in /docker in the docker-major-updates group Bumps the docker-major-updates group in /docker with 1 update: node. Updates `node` from 24-slim to 26-slim --- updated-dependencies: - dependency-name: node dependency-version: 26-slim dependency-type: direct:production dependency-group: docker-major-updates ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump the dev-patch-updates group with 24 updates (#1023) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * chore(deps-dev): bump the dev-patch-updates group with 24 updates Bumps the dev-patch-updates group with 24 updates: | Package | From | To | | --- | --- | --- | | [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.1` | `2.5.2` | | [@codemirror/commands](https://github.com/codemirror/commands) | `6.10.3` | `6.10.4` | | [@codemirror/view](https://github.com/codemirror/view) | `6.43.1` | `6.43.5` | | [@radix-ui/react-accordion](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/accordion) | `1.2.14` | `1.2.15` | | [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.17` | `1.1.18` | | [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.5` | `1.3.6` | | [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.17` | `1.1.18` | | [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.18` | `2.1.19` | | [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.10` | `2.1.11` | | [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.17` | `1.1.18` | | [@radix-ui/react-progress](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/progress) | `1.1.10` | `1.1.11` | | [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.12` | `1.2.13` | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.1` | `2.3.2` | | [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.10` | `1.1.11` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.1` | `1.4.2` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.1` | `1.3.2` | | [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.15` | `1.1.16` | | [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.10` | `1.2.11` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.1` | `4.3.2` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.2` | `6.0.3` | | [i18next](https://github.com/i18next/i18next) | `26.3.1` | `26.3.4` | | [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.6.0` | `1.6.1` | | [sharp](https://github.com/lovell/sharp) | `0.35.2` | `0.35.3` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.1` | `4.3.2` | Updates `@biomejs/biome` from 2.5.1 to 2.5.2 - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.2/packages/@biomejs/biome) Updates `@codemirror/commands` from 6.10.3 to 6.10.4 - [Changelog](https://github.com/codemirror/commands/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/commands/commits) Updates `@codemirror/view` from 6.43.1 to 6.43.5 - [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/view/commits) Updates `@radix-ui/react-accordion` from 1.2.14 to 1.2.15 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/accordion/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/accordion) Updates `@radix-ui/react-alert-dialog` from 1.1.17 to 1.1.18 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog) Updates `@radix-ui/react-checkbox` from 1.3.5 to 1.3.6 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox) Updates `@radix-ui/react-dialog` from 1.1.17 to 1.1.18 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog) Updates `@radix-ui/react-dropdown-menu` from 2.1.18 to 2.1.19 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu) Updates `@radix-ui/react-label` from 2.1.10 to 2.1.11 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label) Updates `@radix-ui/react-popover` from 1.1.17 to 1.1.18 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover) Updates `@radix-ui/react-progress` from 1.1.10 to 1.1.11 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/progress/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/progress) Updates `@radix-ui/react-scroll-area` from 1.2.12 to 1.2.13 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area) Updates `@radix-ui/react-select` from 2.3.1 to 2.3.2 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-separator` from 1.1.10 to 1.1.11 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator) Updates `@radix-ui/react-slider` from 1.4.1 to 1.4.2 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider) Updates `@radix-ui/react-switch` from 1.3.1 to 1.3.2 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `@radix-ui/react-tabs` from 1.1.15 to 1.1.16 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs) Updates `@radix-ui/react-tooltip` from 1.2.10 to 1.2.11 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip) Updates `@tailwindcss/vite` from 4.3.1 to 4.3.2 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-vite) Updates `@vitejs/plugin-react` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react) Updates `i18next` from 26.3.1 to 26.3.4 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v26.3.1...v26.3.4) Updates `radix-ui` from 1.6.0 to 1.6.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui) Updates `sharp` from 0.35.2 to 0.35.3 - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](https://github.com/lovell/sharp/compare/v0.35.2...v0.35.3) Updates `tailwindcss` from 4.3.1 to 4.3.2 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.5.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@codemirror/commands" dependency-version: 6.10.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@codemirror/view" dependency-version: 6.43.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-accordion" dependency-version: 1.2.15 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-alert-dialog" dependency-version: 1.1.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-checkbox" dependency-version: 1.3.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-dialog" dependency-version: 1.1.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-dropdown-menu" dependency-version: 2.1.19 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-label" dependency-version: 2.1.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-popover" dependency-version: 1.1.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-progress" dependency-version: 1.1.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-scroll-area" dependency-version: 1.2.13 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-separator" dependency-version: 1.1.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-slider" dependency-version: 1.4.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-switch" dependency-version: 1.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-tabs" dependency-version: 1.1.16 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-tooltip" dependency-version: 1.2.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@vitejs/plugin-react" dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: i18next dependency-version: 26.3.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: radix-ui dependency-version: 1.6.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: sharp dependency-version: 0.35.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: tailwindcss dependency-version: 4.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the prod-patch-updates group with 2 updates (#1025) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * chore(deps): bump the prod-patch-updates group with 2 updates Bumps the prod-patch-updates group with 2 updates: [axios](https://github.com/axios/axios) and [nanoid](https://github.com/ai/nanoid). Updates `axios` from 1.18.0 to 1.18.1 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.18.0...v1.18.1) Updates `nanoid` from 5.1.15 to 5.1.16 - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/5.1.15...5.1.16) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates - dependency-name: nanoid dependency-version: 5.1.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the prod-minor-updates group with 3 updates (#1026) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * chore(deps): bump the prod-minor-updates group with 3 updates Bumps the prod-minor-updates group with 3 updates: [js-yaml](https://github.com/nodeca/js-yaml), [motion](https://github.com/motiondivision/motion) and [undici](https://github.com/nodejs/undici). Updates `js-yaml` from 5.0.0 to 5.2.1 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/5.0.0...5.2.1) Updates `motion` from 12.40.0 to 12.42.2 - [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md) - [Commits](https://github.com/motiondivision/motion/compare/v12.40.0...v12.42.2) Updates `undici` from 8.5.0 to 8.7.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v8.5.0...v8.7.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 5.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: motion dependency-version: 12.42.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: undici dependency-version: 8.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump electron from 42.4.1 to 43.0.0 in the major-updates group (#1027) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * chore(deps-dev): bump electron in the major-updates group Bumps the major-updates group with 1 update: [electron](https://github.com/electron/electron). Updates `electron` from 42.4.1 to 43.0.0 - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v42.4.1...v43.0.0) --- updated-dependencies: - dependency-name: electron dependency-version: 43.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: major-updates ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix MC syntax highlighting artifacts (#996) * Filter dashboard status hosts (#997) * Persist dashboard service link changes (#999) * Fix snippet text overflow (#1000) * Persist remote desktop credential auth (#1001) * Guard language switching failures (#1002) * Resolve tunnel source credentials (#1003) * Support Vault auth for monitors (#1004) * Fix Windows file delete command (#1005) * Fix release artifact checkout ref (#1006) * Fix command palette escape in fullscreen (#1008) * Fix alerts and audit log data normalization (#1010) * Fix macOS VNC protocol negotiation (#1012) * Fix port knocking before SSH connect (#1013) * Allow Escape to close link confirmation (#1014) * Prevent Electron modifier wheel zoom (#1016) * Fix credential auth optional password (#1009) * Retry transient terminal DNS lookups (#1011) * Retry transient terminal DNS lookups * Apply DNS retry to SSH entry points * Fix OIDC redirect forwarded port handling (#1007) * Preserve recent open tabs on startup (#1015) * Fix fish prompt OSC highlighting (#998) * Fix terminal font selection (#1018) * fix: font legibility (#1019) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * fix: font legibility Text was entirely unreadable in places for me. Especially with themes like Catppuccin. The muted-foreground text and the tags too similiar to the background. --------- Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: russell <git@0896c69e.com> * fix(file-manager): chunked uploads fail with 'Expected multipart/form-data request' (#1020) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * fix(file-manager): use postForm for chunked uploads so multipart content-type is sent The fileManagerApi axios instance defaults to Content-Type: application/json. Axios 1.x's default transformRequest converts a FormData body to JSON whenever the request content type is application/json, so every chunk POSTed to /ssh/uploadFileChunk arrived as a JSON body like {"chunk":{}} and the backend rejected it with 400 'Expected multipart/form-data request'. This breaks all uploads of files larger than the 1.5 GiB chunking threshold. The non-chunked path already uses postForm for /ssh/uploadFileStream; use it for the chunk path too so axios keeps the FormData intact and the browser sets the multipart boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat: implement OIDC back-channel logout support with session management (#1028) * feat: implement OIDC back-channel logout support with session management * Fix OIDC back-channel logout handling * Require logout token replay identifiers --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> * Add API key host enrollment endpoint (#1029) * Fix tmux detection for non-POSIX shells (#1030) * Fix OPKSSH js-yaml ESM import (#1031) * Fix Android Vietnamese IME input (#1032) * Fix Firefox RDP clipboard paste (#1033) * Fix Proxmox discovery over HTTPS (#1041) * Fix external editor actions in file preview (#1042) * Allow pinned hosts with name sorting (#1043) * Fix Firefox desktop OIDC callback (#1044) * feat(session): add recording and replay (#1049) * Fix status checks through jump hosts (#1045) * Add terminal font size shortcuts (#1047) * feat: add Open File Manager to tab right-click menu (#1051) Co-authored-by: SankeerthNara <sankeerthnara@gmail.com> * perf: frontend request cache, poll pause, and code-split shell (#1052) Host/status caching, shell code-split, SSH pool waits, host-metrics concurrency, background-tab idle, per-host status subscriptions, homepage poll quieting, and virtualized host sidebar + file manager lists. * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * feat: save quick connect sessions as hosts (#1055) * fix: restore sudo password autofill settings (#1056) * fix: preserve file editor position on save (#1057) * fix: sync cloud preference storage mode (#1058) * fix: render RDP sessions at native pixel density (#1059) * fix: restore database import in embedded desktop mode (#1060) * Update Auto-complete.tsx (#1061) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * chore: donation bar reporting wrong result * feat: add Open File Manager to tab right-click menu (#1046) * Revert "feat: add Open File Manager to tab right-click menu (#1046)" (#1050) This reverts commit0712fdd731. * Remove donation badge from README Removed donation badge from README. * Delete .github/workflows/donation-goal.yml * Update Auto-complete.tsx --------- Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> * feat(auth): opt-in OIDC DEK unlock for API-key requests (ALLOW_APIKEY_DATA_UNLOCK) (#1064) * chore: fix release workflow to merge docs branch * fix: svg donation generator push fail * fix: svg donation generator push fail * Update termix.rb * fix: svg donation generator push fail * chore: move donation badge to badges branch to avoid ruleset conflicts * chore: remove unneeded token from donation badge workflow * chore: debug donation badge commit step * fix: escape < character in donation SVG * fix: point donation badge to badges branch * chore: remove unused donation badge svg from main * Add Rack Genius logo to README Added Rack Genius logo to the README. * chore: improve donation goal svg generator to include stablecoins * chore: donation goal generator syntax error * chore: donation goal generator incorrect docs url usage * chore: donation bar reporting wrong result * feat: add Open File Manager to tab right-click menu (#1046) * Revert "feat: add Open File Manager to tab right-click menu (#1046)" (#1050) This reverts commit0712fdd731. * Remove donation badge from README Removed donation badge from README. * Delete .github/workflows/donation-goal.yml * feat(auth): opt-in OIDC DEK unlock for API-key requests API keys authenticate but cannot touch the encrypted credential/host store ('User data not unlocked') unless the user has a live interactive session, making them unusable for headless automation. For OIDC users the DEK is server-derivable (deriveOIDCSystemKey), so handleApiKeyAuth can unlock it without a password. Gated behind ALLOW_APIKEY_DATA_UNLOCK (default off) because enabling it widens the blast radius of a leaked API key. OIDC-only; password users are untouched. Refs #1063 --------- Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Sankeerth Nara <sankeerthnara@gmail.com> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> * chore: package lock sync * Add Proxmox guest auto sync (#1053) * draft: database layer refactor (#1054) * feat(sshid) - sshid.io equivalent for termix (#919) * feat(ssh-id): database schema, migrations and field encryption Adds ssh_identities, ssh_identity_keys and ssh_identity_ca tables (public keys stored plaintext for the unauthenticated resolver; CA private key registered for per-user field encryption), with UNIQUE(user_id), an index on ssh_identity_keys(identity_id), and idempotent CREATE TABLE migrations. * feat(ssh-id): backend API — resolver, key management, CA and certificates Mounts /sshid (nginx route added). Public text/plain authorized_keys resolver (+ exact /:algo filter, HTML viewer) and CA public-key endpoint; no-store + noindex headers on every resolver response including early 404s. Authenticated management: claim/rename/delete handle, add/import/generate/enable/delete keys, and a per-user CA (create/rotate/delete) with pure-Node OpenSSH certificate issuance. Audit logging on all mutations; UNIQUE races map to a precise 409. Unit tests for key parsing and certificate signing (ssh-keygen-validated). * feat(ssh-id): frontend panel, API client and i18n SSH ID panel wired into the app rail and AppShell: claim handle, resolver URL + curl one-liner, key list, generate, paste/import, CA enable/rotate/remove with server trust command, and per-key certificate issuance. API client re-exported through main-axios.ts; all strings i18n'd. * style(ssh-id): align panel and resolver page with Termix theme - Rebuild the SSH ID sidebar panel with the theme's square components (SectionCard / SettingRow / FakeSwitch) instead of rounded ad-hoc cards; use accent-brand and destructive tokens rather than raw red/green. - Fix panel scrolling: move overflow to a block scroll container so the cards keep their natural height instead of being clipped. - Restyle the public resolver HTML page (/sshid/u/:handle) to the Termix dark theme: square corners, #18181b/#303032 palette, #f59145 accent, uppercase section labels. - Tidy copy: 'Save To Credentials' label, drop the redundant generate intro, and correct the generate tooltip (the key is stored when saving to vault). * feat: rename to Termix ID, improve UI, backend inconsistencies, and general bug fixes --------- Co-authored-by: LukeGus <bugattiguy527@gmail.com> * ci(deps): bump actions/checkout from 6 to 7 in the github-actions group (#922) Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump the dev-patch-updates group with 11 updates (#923) Bumps the dev-patch-updates group with 11 updates: | Package | From | To | | --- | --- | --- | | [@codemirror/search](https://github.com/codemirror/search) | `6.7.0` | `6.7.1` | | [@codemirror/view](https://github.com/codemirror/view) | `6.43.0` | `6.43.1` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.0` | `4.3.1` | | [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `4.1.8` | `4.1.9` | | [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) | `4.1.8` | `4.1.9` | | [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` | | [lint-staged](https://github.com/lint-staged/lint-staged) | `17.0.7` | `17.0.8` | | [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` | | [sharp](https://github.com/lovell/sharp) | `0.35.1` | `0.35.2` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.1` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` | Updates `@codemirror/search` from 6.7.0 to 6.7.1 - [Changelog](https://github.com/codemirror/search/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/search/commits) Updates `@codemirror/view` from 6.43.0 to 6.43.1 - [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/view/commits) Updates `@tailwindcss/vite` from 4.3.0 to 4.3.1 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite) Updates `@vitest/coverage-v8` from 4.1.8 to 4.1.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/coverage-v8) Updates `@vitest/ui` from 4.1.8 to 4.1.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/ui) Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3 - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3) Updates `lint-staged` from 17.0.7 to 17.0.8 - [Release notes](https://github.com/lint-staged/lint-staged/releases) - [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md) - [Commits](https://github.com/lint-staged/lint-staged/compare/v17.0.7...v17.0.8) Updates `prettier` from 3.8.3 to 3.8.4 - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.8.4) Updates `sharp` from 0.35.1 to 0.35.2 - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](https://github.com/lovell/sharp/compare/v0.35.1...v0.35.2) Updates `tailwindcss` from 4.3.0 to 4.3.1 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss) Updates `vitest` from 4.1.8 to 4.1.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest) --- updated-dependencies: - dependency-name: "@codemirror/search" dependency-version: 6.7.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@codemirror/view" dependency-version: 6.43.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@vitest/ui" dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: eslint-plugin-react-refresh dependency-version: 0.5.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: lint-staged dependency-version: 17.0.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: prettier dependency-version: 3.8.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: sharp dependency-version: 0.35.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: tailwindcss dependency-version: 4.3.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: vitest dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump nanoid in the prod-patch-updates group (#925) Bumps the prod-patch-updates group with 1 update: [nanoid](https://github.com/ai/nanoid). Updates `nanoid` from 5.1.11 to 5.1.15 - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/5.1.11...5.1.15) --- updated-dependencies: - dependency-name: nanoid dependency-version: 5.1.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the major-updates group with 5 updates (#926) Bumps the major-updates group with 5 updates: | Package | From | To | | --- | --- | --- | | [js-yaml](https://github.com/nodeca/js-yaml) | `4.2.0` | `5.0.0` | | [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.4` | `10.0.1` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `26.0.0` | | [concurrently](https://github.com/open-cli-tools/concurrently) | `9.2.1` | `10.0.3` | | [eslint](https://github.com/eslint/eslint) | `9.39.4` | `10.5.0` | Updates `js-yaml` from 4.2.0 to 5.0.0 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...5.0.0) Updates `@eslint/js` from 9.39.4 to 10.0.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js) Updates `@types/node` from 25.9.2 to 26.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `concurrently` from 9.2.1 to 10.0.3 - [Release notes](https://github.com/open-cli-tools/concurrently/releases) - [Commits](https://github.com/open-cli-tools/concurrently/compare/v9.2.1...v10.0.3) Updates `eslint` from 9.39.4 to 10.5.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v9.39.4...v10.5.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: major-updates - dependency-name: "@eslint/js" dependency-version: 10.0.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: major-updates - dependency-name: "@types/node" dependency-version: 26.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: major-updates - dependency-name: concurrently dependency-version: 10.0.3 dependency-type: direct:development update-type: version-update:semver-major dependency-group: major-updates - dependency-name: eslint dependency-version: 10.5.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: major-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(ssh): add HashiCorp Vault SSH signer authentication * fix: small fixes to vault feature to align with Termix codebase * chore: add view docs links for vault/termix id * fix: file upload fails with 400 and missing schema migrations on upgrade (#929) Two bugs introduced in v2.4.1: 1. uploadFileStream uses fileManagerApi.post() which triggers axios's transformRequest to JSON-serialize the FormData because the instance default Content-Type is application/json. Change to postForm() which sets Content-Type: multipart/form-data so the browser XHR sends the correct multipart body with boundary. 2. Two schema items added to schema.ts were not included in migrateSchema() in db/index.ts, causing 500 errors on existing installations upgrading from v2.4.0: - user_preferences.status_color_scheme (no such column) - dashboard_service_links table (no such table) Fixes #928 Co-authored-by: sash <sash@fominykh.io> * fix: support PuTTY PPK ssh keys (#930) * fix: chunk large file manager uploads (#932) * fix: route dashboard hosts by protocol (#934) * fix: resolve tunnel endpoints reliably (#935) * Fix Electron OIDC browser auth failures (#936) * Allow RDP connections without stored credentials (#937) * Sync role credential shares for OIDC users (#938) * Fix terminal link dialog layering (#940) * Confirm large files before opening editor (#942) * Confirm closing active host connections (#943) * Preserve file path case in file manager UI (#941) * fix: preserve unicode guacamole tokens (#933) * Persist VNC authentication settings (#944) * Fix Guacamole websocket base path (#946) * Promote file manager terminals to tabs (#939) * Guard Guacamole disconnect during startup (#945) * chore: increment ver * feat: bitwarden ssh agent integration * feat: serial connections support * fix: various small bug fixes * feat: open all sessions in a folder and terminal custom theme color support * feat: cross host file manager clipboard and several small bug fixes * feat: tailscale/wireguard support and added a new status state for when backend is checking status * feat: grafana like server stats history, new alert system, ntfy/webhook support * feat: new grid and widget based homepage function * feat: new donate button in dashboard * fix: alert ui incorrectly using termix css and fixed issue with alert system not loading * chore: start database layer refactor * docs: plan database layer refactor * docs: audit database layer refactor phase zero * chore: add database runtime adapter skeleton * chore: add settings repository skeleton * chore: add user session repository skeleton * chore: add host credential repository skeleton * chore: add field encryption boundary * chore: migrate settings route slice * chore: migrate user settings routes * chore: migrate host metrics settings routes * chore: migrate acme settings route * chore: migrate terminal settings route * chore: migrate tailscale settings read * chore: migrate guacamole settings reads * chore: migrate session timeout settings reads * chore: migrate auth route settings reads * chore: migrate host metrics settings reads * chore: migrate startup settings reads * chore: migrate user settings cleanup * chore: migrate password reset settings * chore: migrate oidc legacy settings read * chore: migrate user route settings slice * chore: migrate oidc state settings * chore: migrate user login settings reads * chore: migrate user crypto settings * chore: consolidate startup settings defaults * chore: consolidate database settings import export * chore: migrate core session auth paths * chore: migrate remaining session auth paths * chore: migrate admin user routes * chore: migrate user route admin checks * chore: migrate user lifecycle routes * chore: migrate auth user lookups * chore: migrate oidc user routes * chore: migrate api key repository paths * docs: add database gray rollout guide * chore: migrate trusted device paths * chore: migrate user session route user lookups * chore: add database repository rollout guard * chore: expose repository rollout status * chore: warn on repository rollout misconfiguration * chore: migrate remaining user lookup helpers * chore: migrate ssh user lookups * chore: migrate user settings admin lookups * chore: migrate acme ssl user lookups * chore: migrate audit log admin checks * chore: migrate oidc account user updates * chore: migrate password reset user updates * chore: migrate user deletion core records * chore: migrate snippet audit user lookups * chore: migrate ldap user sync paths * chore: migrate totp user updates * chore: migrate rbac user checks * chore: migrate rbac role paths * chore: migrate permission role lookups * chore: migrate rbac access list reads * chore: migrate shared rbac reads * chore: migrate rbac access writes * chore: migrate permission host access * chore: migrate role host access lookup * chore: migrate snippet access lookup * chore: migrate shared credential access lookups * chore: migrate host access cleanup writes * chore: migrate host list access checks * chore: migrate host access cleanup routes * chore: migrate shared credential role lookups * chore: migrate user role cleanup * chore: migrate admin role sync * chore: migrate ldap role sync * chore: migrate user role assignment * chore: migrate sso provider access * chore: migrate audit log access * chore: migrate user preference access * chore: migrate open tab access * chore: migrate dismissed alert access * chore: migrate homepage layout access * chore: migrate network topology access * chore: migrate dashboard service link access * chore: migrate command history access * chore: migrate recent activity cleanup * chore: migrate ssh credential usage access * chore: migrate transfer recent access * chore: migrate file manager bookmark access * chore: migrate c2s tunnel preset access * chore: migrate homepage item access * chore: migrate session recording access * chore: migrate tmux session tag access * chore: migrate opkssh token access * chore: migrate vault token access * chore: migrate vault profile access * chore: migrate host metrics preference access * chore: migrate host health access * chore: migrate host metrics history access * chore: migrate alert persistence access * chore: route alert host lookup through repository * chore: migrate user data export reads * chore: route host metrics stats sync through repository * chore: migrate host folder persistence * chore: migrate host resolution reads * chore: route jump host resolution reads * chore: route docker console jump host reads * chore: route docker ssh resolution reads * chore: route proxmox discovery resolution reads * chore: route file manager activity host reads * chore: route host metrics resolution reads * chore: route ssh auth credential reads * chore: route tunnel endpoint credential reads * chore: route credential deployment resolution reads * chore: route command history host flag reads * chore: route snippet execution resolution reads * chore: route terminal host resolution reads * chore: route vault oidc host resolution reads * chore: route wake on lan host reads * chore: route internal host list reads * chore: route host key verification persistence * chore: route credential read paths * chore: route credential host usage reads * chore: route credential folder rename * chore: route host owner access checks * chore: route shared credential source reads * chore: route user host credential cleanup * chore: route credential delete reads * chore: route credential update reads * chore: route host credential reads * chore: route host read paths * chore: route host projection reads * chore: route host list reads * chore: route snippet read paths * chore: route snippet folder writes * chore: route snippet crud paths * chore: route snippet bulk import * chore: route rbac ownership reads * chore: route user count reads * chore: route cleanup snippets folders * chore: route shared credential persistence * chore: route dashboard activity * chore: route guacamole host reads * chore: route host bulk lookups * chore: remove unlock-only simple db ops * chore: route host autostart persistence * chore: route ldap provisioning through users * chore: route credential encrypted writes * chore: route host encrypted writes * chore: route bulk host encrypted writes * chore: route termix id credentials * chore: route termix id ca persistence * chore: route termix identity persistence * chore: route credential system migration * chore: isolate user encryption migration storage * chore: remove legacy simple db ops * chore: isolate legacy sqlite migration copy * chore: route database settings import export * chore: route database host credential export * chore: route database host credential import * chore: route database file-manager import export * chore: route database alert usage import export * chore: route database user checks * chore: isolate auth lazy migration storage * chore: route explicit database saves * chore: initialize database save boundary * chore: route migration snapshot saves * chore: isolate sqlite import constraints * chore: route import sqlite boundary * chore: route user encryption migration store * chore: centralize current repository runtime * chore: route more current repositories * chore: route activity repository runtimes * chore: route token repository runtimes * chore: route health repository runtimes * chore: route identity repository runtimes * chore: route rbac repository runtime * chore: centralize current sqlite runtime access * chore: route user deletion key cleanup * chore: route user deletion vault cleanup * chore: route user deletion homepage cleanup * chore: route user deletion health cleanup * chore: route user deletion alert cleanup * chore: route user deletion identity cleanup * chore: add database layer preupgrade backup * Fix database repository type errors * fix: complete post-merge compile fixes for database refactor Restore missing DatabaseSaveTrigger/getDb imports, session log format fallback, OIDC provider resolution, guacamole recording insert, and passwordFallbackOnly typing after merging current dev. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: DivByZero <mr.oplus@yahoo.fr> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: devdanetra <46488477+devdanetra@users.noreply.github.com> Co-authored-by: Aleksandr Fominykh <neoformalex@users.noreply.github.com> Co-authored-by: sash <sash@fominykh.io> * refactor(db): collapse repository rollout scaffolding into single factory Repositories are now the only data path. Replaces the 41 current-*-repository wrapper files, the DATABASE_LAYER_REPOSITORY_ROLLOUT flag/alias map and the unused database/runtime adapter with repositories/factory.ts, a plain DatabaseContext type and an in-memory TestSqliteDatabase test harness. * refactor(db): route remaining raw DB access through repositories proxmox, session-log, oidc-utils, webauthn and guacamole recording now use repositories (new WebauthnCredentialRepository; SsoProviderRepository listEnabled; HostRepository findDecryptedByIdAs/listProxmoxEnabled). Remaining raw access: db boot code, simple-db-ops and docker.ts, which are removed/restructured in later phases. * feat(crypto): add UserKeyManager with system-wrapped per-user DEKs New utils/user-keys.ts: one random 32-byte DEK per user, wrapped AES-256-GCM under an HKDF key derived from the system ENCRYPTION_KEY (per-user info string + AAD binding, versioned v3 wrap format stored in settings). Synchronous unwrap-on-demand with a 15-minute cache so the existing DataCrypto facade keeps its sync call sites. Not wired up yet. * feat(crypto): boot-time DEK migration to system-wrapped v3 format utils/crypto-migration/dek-migration.ts carries the legacy unwrap paths (PBKDF2 password KEK, OIDC/WebAuthn system keys, hardcoded-default fallback) and migrates every server-unwrappable DEK to the v3 wrap at startup. Password-wrapped DEKs migrate at next login or from a live session via adoptRecoveredDEK. Legacy rows are kept for now; cleanup flips on once the new path is authoritative. * refactor(crypto): make system-wrapped DEKs the authoritative key path DataCrypto and AuthManager now read keys through UserKeyManager: DEKs are always unwrappable server-side, so the in-memory unlock session, DEK-in-JWT wrapping, session-expiry data locks and ALLOW_APIKEY_DATA_UNLOCK are gone. utils/user-crypto.ts is deleted; boot migration now cleans legacy wraps. A one-release shim adopts DEKs from legacy dataKeyWrap tokens so active password users migrate without re-login. Password login migrates legacy password-wrapped DEKs via migratePasswordUserAtLogin. * refactor(crypto): remove pending share queue and credential sharing key With server-unwrappable DEKs both sides of a share are always available, so the needsReEncryption queue, CREDENTIAL_SHARING_KEY and the system_* shadow columns on ssh_credentials are gone. A one-time boot cleanup re-creates legacy pending share copies where possible (dropping unresolvable ones with a warning) and drops the legacy columns. * feat(auth): non-destructive password resets and admin reset endpoint Password resets no longer destroy user data: the DEK is system-wrapped, so forgot-password and admin resets are just a hash update plus session revoke. The wipe branch survives only for accounts that never logged in since the encryption upgrade and now requires explicit confirmDataWipe (surfaced as a 409 DATA_WIPE_REQUIRED; the reset UI asks for confirmation). Adds POST /users/admin/reset-password and removes the dead re-encryption paths. * refactor(ssh): consolidate four jump-host chain copies into one module terminal, host-metrics and docker now use ssh/jump-host-chain.ts (already shared by file-manager, tmux-monitor and docker-console); docker's inline copy also drops its raw SimpleDBOps host/credential lookups in favor of repositories. * refactor(ssh): single shared createConnectionLog helper file-manager-log.ts becomes ssh/connection-log.ts; the copies in docker.ts and host-metrics-helpers.ts are gone. * refactor(ssh): split docker module into layered directory ssh/docker/{index,routes,session-manager,container-routes,console}.ts: server boot and wiring in index, HTTP handlers in routes, SSH session registry and command execution in session-manager. Code motion only; port 30007/30009 and endpoints unchanged. Swagger now scans ssh subdirectories. * refactor(ssh): split tunnel module into layered directory ssh/tunnel/{index,routes,manager}.ts: server boot in index, HTTP handlers in routes, tunnel state and engine (connect/retry/autostart) in manager. Code motion only; port 30003 and endpoints unchanged. * refactor(backend): reorganize top-level layout - ssh/ renamed to hosts/ (it covers SSH, RDP, VNC, Telnet, Docker, metrics) - serial/serial.ts and guacamole/ moved inside hosts/ - dashboard.ts and homepage.ts moved to services/ - swagger.ts moved to utils/ with adjusted scan globs Import paths and the generate:openapi script updated; ports and endpoints unchanged. * refactor(tests): move backend tests into src/backend/tests mirror tree Backend *.test.ts files (and the test-support harness) no longer sit next to source files; they live under src/backend/tests/ mirroring the source layout. Imports rewritten accordingly; CLAUDE.md convention updated. * refactor(hosts): group host modules into per-feature directories file-manager/, metrics/ (incl. widgets, managers, alert-engine), terminal/, tmux/ and tunnel/ each own their files; docker/ gains container-runtime. Genuinely shared helpers (jump-host chain, host resolver, connection pool, opkssh, vault, serial) stay at hosts/ root. Pure file moves with import path updates; mirrored test paths follow. * refactor(backend): final cleanup pass - re-register WebAuthn passkey routes (registration was dropped in the #1054 merge, breaking passkey login) and document all six endpoints - delete utils/simple-db-ops.ts (last caller migrated to DataCrypto) - starter: use the typed serverReady export, collapse the four-way version lookup to env then package.json candidates - add OpenAPI JSDoc to c2s-tunnel-presets endpoints - strip block-divider comment banners * feat: remove legacy "data_unlocked" field * feat: refactor rbac/sharing to support new permissions and auth types * feat: refactor rbac/sharing to support new permissions and auth types * feat: add "id" to user profile hide list * chore: root cleanup * feat: add more donation references and a 30-day donation reminder * chore: update readme * feat: automate beta tests * feat: add links to milestones * fix: hoist github/google SSO defaults to module scope (#1065) * fix(ssh-tools): allow clipboard paste in key recording field (#1066) The broadcast key-recording input was marked readOnly, which makes browsers block paste entirely (no context-menu Paste, Ctrl+V does nothing). handleKeyDown also called preventDefault() unconditionally, swallowing the Ctrl+V shortcut before a paste event could even fire. Let Ctrl/Cmd+V pass through in handleKeyDown, drop readOnly, and add an onPaste handler that reads the clipboard text and broadcasts it to the selected terminals like any other captured keystroke. Signed-off-by: emreumar <emreumar@users.noreply.github.com> Co-authored-by: emreumar <emreumar@users.noreply.github.com> * chore: write release notes * chore: update release notes * chore: update readmes * chore: add crypto only reminder in en.json * fix: macOS and cask errors on release workflow * chore: sync Crowdin translations for 2.5.1 --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: emreumar <emreumar@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Russell Poovey <09.our_seekers@icloud.com> Co-authored-by: russell <git@0896c69e.com> Co-authored-by: Subedi Bibek <77529535+questbibek@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Alexander Elsner <101340634+Bensonheimer992@users.noreply.github.com> Co-authored-by: SankeerthNara <sankeerthnara@gmail.com> Co-authored-by: Stephan Groth <96803994+Kalvalax@users.noreply.github.com> Co-authored-by: DivByZero <mr.oplus@yahoo.fr> Co-authored-by: devdanetra <46488477+devdanetra@users.noreply.github.com> Co-authored-by: Aleksandr Fominykh <neoformalex@users.noreply.github.com> Co-authored-by: sash <sash@fominykh.io> Co-authored-by: lhojun <ldgs3324@gmail.com> Co-authored-by: Yunus Emre Umar <77045015+emre155@users.noreply.github.com> Co-authored-by: emreumar <emreumar@users.noreply.github.com>
2207 lines
56 KiB
TypeScript
2207 lines
56 KiB
TypeScript
import axios, { AxiosError, type AxiosInstance } from "axios";
|
|
import { toast } from "sonner";
|
|
import { getBasePath } from "@/lib/base-path";
|
|
import { isElectron } from "@/lib/electron";
|
|
import { clearTermixSessionStorage } from "@/shell/TabContext";
|
|
import type { SSHHost } from "@/types/index";
|
|
|
|
// ============================================================================
|
|
// RBAC TYPE DEFINITIONS
|
|
// ============================================================================
|
|
|
|
export interface Role {
|
|
id: number;
|
|
name: string;
|
|
displayName: string;
|
|
description: string | null;
|
|
isSystem: boolean;
|
|
permissions: string[] | string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface UserRole {
|
|
userId: string;
|
|
roleId: number;
|
|
roleName: string;
|
|
roleDisplayName: string;
|
|
grantedBy: string;
|
|
grantedByUsername: string;
|
|
grantedAt: string;
|
|
}
|
|
|
|
export interface AccessRecord {
|
|
id: number;
|
|
targetType: "user" | "role";
|
|
userId: string | null;
|
|
roleId: number | null;
|
|
username: string | null;
|
|
roleName: string | null;
|
|
roleDisplayName: string | null;
|
|
grantedBy: string;
|
|
grantedByUsername: string;
|
|
permissionLevel: "connect" | "view" | "edit" | "manage";
|
|
expiresAt: string | null;
|
|
createdAt: string;
|
|
}
|
|
import {
|
|
apiLogger,
|
|
authLogger,
|
|
sshLogger,
|
|
tunnelLogger,
|
|
fileLogger,
|
|
statsLogger,
|
|
systemLogger,
|
|
dashboardLogger,
|
|
type LogContext,
|
|
} from "@/lib/frontend-logger";
|
|
import { dbHealthMonitor } from "@/lib/db-health-monitor";
|
|
|
|
export type ServerStatus = {
|
|
status: "online" | "offline";
|
|
lastChecked: string;
|
|
};
|
|
|
|
export type SSHHostWithStatus = SSHHost & {
|
|
status: "online" | "offline" | "unknown";
|
|
};
|
|
|
|
interface CpuMetrics {
|
|
percent: number | null;
|
|
cores: number | null;
|
|
load: [number, number, number] | null;
|
|
}
|
|
|
|
interface MemoryMetrics {
|
|
percent: number | null;
|
|
usedGiB: number | null;
|
|
totalGiB: number | null;
|
|
}
|
|
|
|
interface DiskMetrics {
|
|
percent: number | null;
|
|
usedHuman: string | null;
|
|
totalHuman: string | null;
|
|
availableHuman?: string | null;
|
|
}
|
|
|
|
export interface NetworkInterface {
|
|
name: string;
|
|
ip: string;
|
|
state: string;
|
|
rx?: string | null;
|
|
tx?: string | null;
|
|
rxBytes?: string | null;
|
|
txBytes?: string | null;
|
|
}
|
|
|
|
export interface ProcessInfo {
|
|
pid: string;
|
|
user: string;
|
|
cpu: string;
|
|
mem: string;
|
|
command: string;
|
|
}
|
|
|
|
export interface LoginRecord {
|
|
user: string;
|
|
ip: string;
|
|
time: string;
|
|
status: "success" | "failed";
|
|
}
|
|
|
|
export interface ListeningPort {
|
|
protocol: "tcp" | "udp";
|
|
localAddress: string;
|
|
localPort: number;
|
|
state?: string;
|
|
pid?: number;
|
|
process?: string;
|
|
}
|
|
|
|
export interface FirewallRule {
|
|
chain: string;
|
|
target: string;
|
|
protocol: string;
|
|
source: string;
|
|
destination: string;
|
|
dport?: string;
|
|
sport?: string;
|
|
state?: string;
|
|
interface?: string;
|
|
extra?: string;
|
|
}
|
|
|
|
export interface FirewallChain {
|
|
name: string;
|
|
policy: string;
|
|
rules: FirewallRule[];
|
|
}
|
|
|
|
export type ServerMetrics = {
|
|
cpu: CpuMetrics;
|
|
memory: MemoryMetrics;
|
|
disk: DiskMetrics;
|
|
network?: { interfaces?: NetworkInterface[] };
|
|
uptime?: { seconds?: number | null; formatted?: string | null };
|
|
system?: {
|
|
hostname?: string | null;
|
|
os?: string | null;
|
|
kernel?: string | null;
|
|
arch?: string | null;
|
|
};
|
|
processes?: {
|
|
total?: number | null;
|
|
running?: number | null;
|
|
top?: ProcessInfo[];
|
|
};
|
|
login_stats?: {
|
|
recentLogins?: LoginRecord[];
|
|
failedLogins?: LoginRecord[];
|
|
totalLogins?: number;
|
|
uniqueIPs?: number;
|
|
};
|
|
ports?: {
|
|
source?: "ss" | "netstat" | "none";
|
|
ports?: ListeningPort[];
|
|
};
|
|
firewall?: {
|
|
type?: "iptables" | "nftables" | "none";
|
|
status?: "active" | "inactive" | "unknown";
|
|
chains?: FirewallChain[];
|
|
};
|
|
temperature?: {
|
|
source?: "sysfs" | "sensors" | "none";
|
|
highestCelsius?: number | null;
|
|
sensors?: Array<{
|
|
label: string;
|
|
celsius: number;
|
|
}>;
|
|
};
|
|
lastChecked: string;
|
|
};
|
|
|
|
export interface AuthResponse {
|
|
success?: boolean;
|
|
is_admin?: boolean;
|
|
username?: string;
|
|
userId?: string;
|
|
is_oidc?: boolean;
|
|
totp_enabled?: boolean;
|
|
requires_totp?: boolean;
|
|
temp_token?: string;
|
|
rememberMe?: boolean;
|
|
token?: string;
|
|
}
|
|
|
|
export interface UserInfo {
|
|
totp_enabled: boolean;
|
|
userId: string;
|
|
username: string;
|
|
is_admin: boolean;
|
|
is_oidc: boolean;
|
|
password_hash?: string;
|
|
data_unlocked?: boolean;
|
|
show_donation_modal?: boolean;
|
|
}
|
|
|
|
interface UserCount {
|
|
count: number;
|
|
}
|
|
|
|
interface OIDCAuthorize {
|
|
auth_url: string;
|
|
}
|
|
|
|
type ElectronApi = {
|
|
isElectron?: boolean;
|
|
getSetting?: (key: string) => Promise<string | null | undefined>;
|
|
setSetting?: (key: string, value: string) => Promise<void>;
|
|
};
|
|
|
|
type ElectronWindow = Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: ElectronApi;
|
|
ReactNativeWebView?: unknown;
|
|
};
|
|
|
|
// ============================================================================
|
|
// UTILITY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
export { isElectron };
|
|
|
|
function getLoggerForService(serviceName: string) {
|
|
if (serviceName.includes("SSH") || serviceName.includes("ssh")) {
|
|
return sshLogger;
|
|
} else if (serviceName.includes("TUNNEL") || serviceName.includes("tunnel")) {
|
|
return tunnelLogger;
|
|
} else if (serviceName.includes("FILE") || serviceName.includes("file")) {
|
|
return fileLogger;
|
|
} else if (serviceName.includes("STATS") || serviceName.includes("stats")) {
|
|
return statsLogger;
|
|
} else if (serviceName.includes("AUTH") || serviceName.includes("auth")) {
|
|
return authLogger;
|
|
} else if (
|
|
serviceName.includes("DASHBOARD") ||
|
|
serviceName.includes("dashboard")
|
|
) {
|
|
return dashboardLogger;
|
|
} else {
|
|
return apiLogger;
|
|
}
|
|
}
|
|
|
|
const electronSettingsCache = new Map<string, string>();
|
|
|
|
if (isElectron()) {
|
|
(async () => {
|
|
try {
|
|
const electronAPI = (window as ElectronWindow).electronAPI;
|
|
|
|
if (electronAPI?.getSetting) {
|
|
const settingsToLoad = ["rightClickCopyPaste"];
|
|
for (const key of settingsToLoad) {
|
|
const value = await electronAPI.getSetting(key);
|
|
if (value !== null && value !== undefined) {
|
|
// Only populate if not already set to prevent overwriting new values during login
|
|
if (!localStorage.getItem(key)) {
|
|
electronSettingsCache.set(key, value);
|
|
localStorage.setItem(key, value);
|
|
console.log(`[Electron] Loaded setting ${key} from main process`);
|
|
} else {
|
|
// Even if we don't overwrite localStorage, update the cache
|
|
electronSettingsCache.set(key, localStorage.getItem(key)!);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("[Electron] Failed to load settings cache:", error);
|
|
}
|
|
})();
|
|
}
|
|
|
|
export function setCookie(
|
|
name: string,
|
|
value: string,
|
|
days = 7,
|
|
): void | Promise<void> {
|
|
if (isElectron()) {
|
|
try {
|
|
if (name === "jwt") {
|
|
return;
|
|
}
|
|
|
|
const electronAPI = (window as ElectronWindow).electronAPI;
|
|
|
|
if (electronAPI?.setSetting) {
|
|
electronSettingsCache.set(name, value);
|
|
localStorage.setItem(name, value);
|
|
electronAPI.setSetting(name, value).catch((err: Error) => {
|
|
console.error(`[Electron] Failed to persist setting ${name}:`, err);
|
|
});
|
|
}
|
|
|
|
console.log(`[Electron] Set setting: ${name}`);
|
|
} catch (error) {
|
|
console.error(`[Electron] Failed to set setting: ${name}`, error);
|
|
}
|
|
} else {
|
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
|
}
|
|
}
|
|
|
|
export function getCookie(name: string): string | undefined {
|
|
if (isElectron()) {
|
|
try {
|
|
if (name === "jwt") {
|
|
return undefined;
|
|
}
|
|
|
|
if (electronSettingsCache.has(name)) {
|
|
return electronSettingsCache.get(name);
|
|
}
|
|
|
|
const token = localStorage.getItem(name) || undefined;
|
|
if (token) {
|
|
electronSettingsCache.set(name, token);
|
|
}
|
|
console.log(`[Electron] Get setting: ${name} = ${token}`);
|
|
return token;
|
|
} catch (error) {
|
|
console.error(`[Electron] Failed to get setting: ${name}`, error);
|
|
return undefined;
|
|
}
|
|
} else {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
const encodedToken =
|
|
parts.length === 2 ? parts.pop()?.split(";").shift() : undefined;
|
|
const token = encodedToken ? decodeURIComponent(encodedToken) : undefined;
|
|
return token;
|
|
}
|
|
}
|
|
|
|
let userWasAuthenticated = false;
|
|
let latestAuthSuccessAt = 0;
|
|
|
|
export function markUserAuthenticated(): void {
|
|
userWasAuthenticated = true;
|
|
latestAuthSuccessAt =
|
|
typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
}
|
|
|
|
export function isCurrentAuthInvalidationError(error: unknown): boolean {
|
|
const authError = error as {
|
|
__staleAuthInvalidation?: boolean;
|
|
};
|
|
|
|
if (authError.__staleAuthInvalidation) {
|
|
return false;
|
|
}
|
|
|
|
const axiosError = error as AxiosError;
|
|
const apiError = error as ApiError;
|
|
const responseData = axiosError.response?.data as
|
|
| Record<string, unknown>
|
|
| undefined;
|
|
const errorCode = responseData?.code || apiError.code;
|
|
const errorMessage = responseData?.error || apiError.message;
|
|
const status = axiosError.response?.status || apiError.status;
|
|
const isMissingAuthenticationToken =
|
|
errorMessage === "Missing authentication token";
|
|
|
|
return (
|
|
status === 401 &&
|
|
(errorCode === "SESSION_EXPIRED" ||
|
|
errorCode === "SESSION_NOT_FOUND" ||
|
|
(errorCode === "AUTH_REQUIRED" && userWasAuthenticated) ||
|
|
errorMessage === "Invalid token" ||
|
|
(errorMessage === "Authentication required" && userWasAuthenticated) ||
|
|
(isMissingAuthenticationToken && userWasAuthenticated))
|
|
);
|
|
}
|
|
|
|
function createApiInstance(
|
|
baseURL: string,
|
|
serviceName: string = "API",
|
|
): AxiosInstance {
|
|
const instance = axios.create({
|
|
baseURL,
|
|
headers: { "Content-Type": "application/json" },
|
|
timeout: 30000,
|
|
withCredentials: true,
|
|
});
|
|
|
|
instance.interceptors.request.use((config: AxiosRequestConfig) => {
|
|
const startTime = performance.now();
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
const configWithMetadata = config as AxiosRequestConfigExtended;
|
|
configWithMetadata.startTime = startTime;
|
|
configWithMetadata.requestId = requestId;
|
|
|
|
const method = config.method?.toUpperCase() || "UNKNOWN";
|
|
const url = config.url || "UNKNOWN";
|
|
const fullUrl = `${config.baseURL}${url}`;
|
|
|
|
const context: LogContext = {
|
|
requestId,
|
|
method,
|
|
url: fullUrl,
|
|
operation: "request_start",
|
|
};
|
|
|
|
const logger = getLoggerForService(serviceName);
|
|
|
|
const isDevMode = process.env.NODE_ENV === "development";
|
|
|
|
if (isDevMode) {
|
|
logger.requestStart(method, fullUrl, context);
|
|
}
|
|
|
|
if (isElectron()) {
|
|
if (config.headers.set) {
|
|
config.headers.set("X-Electron-App", "true");
|
|
} else {
|
|
config.headers["X-Electron-App"] = "true";
|
|
}
|
|
const jwt = localStorage.getItem("jwt");
|
|
if (jwt) {
|
|
if (config.headers.set) {
|
|
config.headers.set("Authorization", `Bearer ${jwt}`);
|
|
} else {
|
|
config.headers["Authorization"] = `Bearer ${jwt}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
typeof window !== "undefined" &&
|
|
(window as ElectronWindow).ReactNativeWebView
|
|
) {
|
|
let platform = "Unknown";
|
|
if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
if (navigator.userAgent.includes("Android")) {
|
|
platform = "Android";
|
|
} else if (
|
|
navigator.userAgent.includes("iPhone") ||
|
|
navigator.userAgent.includes("iPad") ||
|
|
navigator.userAgent.includes("iOS")
|
|
) {
|
|
platform = "iOS";
|
|
}
|
|
}
|
|
if (config.headers.set) {
|
|
config.headers.set("User-Agent", `Termix-Mobile/${platform}`);
|
|
} else {
|
|
config.headers["User-Agent"] = `Termix-Mobile/${platform}`;
|
|
}
|
|
}
|
|
|
|
return config;
|
|
});
|
|
|
|
instance.interceptors.response.use(
|
|
(response: AxiosResponse) => {
|
|
const endTime = performance.now();
|
|
const responseConfig = response.config as AxiosRequestConfigExtended;
|
|
const startTime = responseConfig.startTime;
|
|
const requestId = responseConfig.requestId;
|
|
const responseTime = Math.round(endTime - (startTime || endTime));
|
|
|
|
const method = response.config.method?.toUpperCase() || "UNKNOWN";
|
|
const url = response.config.url || "UNKNOWN";
|
|
const fullUrl = `${response.config.baseURL}${url}`;
|
|
|
|
const context: LogContext = {
|
|
requestId,
|
|
method,
|
|
url: fullUrl,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
responseTime,
|
|
operation: "request_success",
|
|
};
|
|
|
|
const logger = getLoggerForService(serviceName);
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
logger.requestSuccess(
|
|
method,
|
|
fullUrl,
|
|
response.status,
|
|
responseTime,
|
|
context,
|
|
);
|
|
}
|
|
|
|
if (responseTime > 3000) {
|
|
logger.warn(`🐌 Slow request: ${responseTime}ms`, context);
|
|
}
|
|
|
|
dbHealthMonitor.reportDatabaseSuccess();
|
|
|
|
return response;
|
|
},
|
|
(error: AxiosErrorExtended) => {
|
|
const endTime = performance.now();
|
|
const startTime = error.config?.startTime;
|
|
const requestId = error.config?.requestId;
|
|
const responseTime = startTime
|
|
? Math.round(endTime - startTime)
|
|
: undefined;
|
|
|
|
const method = error.config?.method?.toUpperCase() || "UNKNOWN";
|
|
const url = error.config?.url || "UNKNOWN";
|
|
const fullUrl = error.config ? `${error.config.baseURL}${url}` : url;
|
|
const status = error.response?.status;
|
|
const message =
|
|
(error.response?.data as { error?: string })?.error ||
|
|
(error as Error).message ||
|
|
"Unknown error";
|
|
const errorCode =
|
|
(error.response?.data as { code?: string })?.code || error.code;
|
|
|
|
const context: LogContext = {
|
|
requestId,
|
|
method,
|
|
url: fullUrl,
|
|
status,
|
|
responseTime,
|
|
errorCode,
|
|
errorMessage: message,
|
|
operation: "request_error",
|
|
};
|
|
|
|
const logger = getLoggerForService(serviceName);
|
|
// A caller can mark a request as a silent retry (see progressive /status
|
|
// retry) so we don't spam error logs / health events on each attempt.
|
|
const isSilentRetry = !!error.config?.__silentRetry;
|
|
|
|
if (process.env.NODE_ENV === "development" && !isSilentRetry) {
|
|
if (status === 401) {
|
|
logger.authError(method, fullUrl, context);
|
|
} else if (status === 0 || !status) {
|
|
logger.networkError(method, fullUrl, message, context);
|
|
} else {
|
|
logger.requestError(
|
|
method,
|
|
fullUrl,
|
|
status || 0,
|
|
message,
|
|
responseTime,
|
|
context,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (status === 401) {
|
|
const errorCode = (error.response?.data as Record<string, unknown>)
|
|
?.code;
|
|
const errorMessage = (error.response?.data as Record<string, unknown>)
|
|
?.error;
|
|
const isSessionExpired = errorCode === "SESSION_EXPIRED";
|
|
const isSessionNotFound = errorCode === "SESSION_NOT_FOUND";
|
|
const isMissingAuthenticationToken =
|
|
errorMessage === "Missing authentication token";
|
|
const isInvalidToken =
|
|
errorCode === "AUTH_REQUIRED" ||
|
|
errorMessage === "Invalid token" ||
|
|
errorMessage === "Authentication required" ||
|
|
(isMissingAuthenticationToken && userWasAuthenticated);
|
|
|
|
if (isSessionExpired || isSessionNotFound || isInvalidToken) {
|
|
const requestStartedAt =
|
|
typeof error.config?.startTime === "number"
|
|
? error.config.startTime
|
|
: 0;
|
|
const isStaleAuthInvalidation =
|
|
latestAuthSuccessAt > 0 &&
|
|
requestStartedAt > 0 &&
|
|
requestStartedAt < latestAuthSuccessAt;
|
|
|
|
if (isStaleAuthInvalidation) {
|
|
(
|
|
error as { __staleAuthInvalidation?: boolean }
|
|
).__staleAuthInvalidation = true;
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
if (isElectron()) {
|
|
const electronAPI = (
|
|
window as unknown as {
|
|
electronAPI?: { clearSessionCookies?: () => Promise<void> };
|
|
}
|
|
).electronAPI;
|
|
electronAPI?.clearSessionCookies?.().catch(() => {});
|
|
}
|
|
|
|
if (typeof window !== "undefined") {
|
|
document.cookie =
|
|
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
}
|
|
|
|
if (isSessionExpired && typeof window !== "undefined") {
|
|
console.warn("Session expired - please log in again");
|
|
toast.warning("Session expired. Please log in again.");
|
|
}
|
|
|
|
dbHealthMonitor.reportSessionExpired();
|
|
|
|
userWasAuthenticated = false;
|
|
}
|
|
} else if (!isSilentRetry) {
|
|
const wasAuthenticated = userWasAuthenticated;
|
|
dbHealthMonitor.reportDatabaseError(error, wasAuthenticated);
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
},
|
|
);
|
|
|
|
return instance;
|
|
}
|
|
|
|
// ============================================================================
|
|
// API INSTANCES
|
|
// ============================================================================
|
|
|
|
function isDev(): boolean {
|
|
if (isElectron()) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
process.env.NODE_ENV === "development" &&
|
|
(window.location.port === "3000" ||
|
|
window.location.port === "5173" ||
|
|
window.location.port === "" ||
|
|
window.location.hostname === "localhost" ||
|
|
window.location.hostname === "127.0.0.1")
|
|
);
|
|
}
|
|
|
|
const apiHost = import.meta.env.VITE_API_HOST || "localhost";
|
|
let configuredServerUrl: string | null = null;
|
|
let embeddedMode = false;
|
|
|
|
export interface ServerConfig {
|
|
serverUrl: string;
|
|
lastUpdated: string;
|
|
allowInvalidCertificate?: boolean;
|
|
}
|
|
|
|
interface AxiosRequestConfigExtended extends AxiosRequestConfig {
|
|
startTime?: number;
|
|
requestId?: string;
|
|
__silentRetry?: boolean;
|
|
}
|
|
|
|
interface AxiosErrorExtended extends AxiosError {
|
|
config?: AxiosRequestConfigExtended;
|
|
}
|
|
|
|
export async function getServerConfig(): Promise<ServerConfig | null> {
|
|
if (!isElectron()) return null;
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI?.invoke("get-server-config");
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Failed to get server config:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function saveServerConfig(config: ServerConfig): Promise<boolean> {
|
|
if (!isElectron()) return false;
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI?.invoke("save-server-config", config);
|
|
if (result?.success) {
|
|
configuredServerUrl = config.serverUrl;
|
|
(
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).configuredServerUrl = configuredServerUrl;
|
|
updateApiInstances();
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.error("Failed to save server config:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function getConfiguredServerUrl(): string | null {
|
|
return configuredServerUrl;
|
|
}
|
|
|
|
export async function testServerConnection(
|
|
serverUrl: string,
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
if (!isElectron())
|
|
return { success: false, error: "Not in Electron environment" };
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI?.invoke("test-server-connection", serverUrl);
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Failed to test server connection:", error);
|
|
return { success: false, error: "Connection test failed" };
|
|
}
|
|
}
|
|
|
|
export async function checkElectronUpdate(): Promise<{
|
|
success: boolean;
|
|
status?: "up_to_date" | "requires_update" | "beta";
|
|
localVersion?: string;
|
|
remoteVersion?: string;
|
|
latest_release?: {
|
|
tag_name: string;
|
|
name: string;
|
|
published_at: string;
|
|
html_url: string;
|
|
body: string;
|
|
};
|
|
cached?: boolean;
|
|
cache_age?: number;
|
|
error?: string;
|
|
}> {
|
|
if (!isElectron())
|
|
return { success: false, error: "Not in Electron environment" };
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI?.invoke("check-electron-update");
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Failed to check Electron update:", error);
|
|
return { success: false, error: "Update check failed" };
|
|
}
|
|
}
|
|
|
|
export async function getEmbeddedServerStatus(): Promise<{
|
|
running: boolean;
|
|
embedded: boolean;
|
|
dataDir: string | null;
|
|
} | null> {
|
|
if (!isElectron()) return null;
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: {
|
|
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
|
|
};
|
|
}
|
|
).electronAPI?.invoke("get-embedded-server-status");
|
|
return result as {
|
|
running: boolean;
|
|
embedded: boolean;
|
|
dataDir: string | null;
|
|
} | null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function isEmbeddedMode(): boolean {
|
|
return embeddedMode;
|
|
}
|
|
|
|
export function setEmbeddedMode(value: boolean): void {
|
|
embeddedMode = value;
|
|
if (value) {
|
|
configuredServerUrl = null;
|
|
initializeApiInstances();
|
|
}
|
|
}
|
|
|
|
function getApiUrl(path: string, defaultPort: number): string {
|
|
const devMode = isDev();
|
|
const electronMode = isElectron();
|
|
|
|
if (electronMode) {
|
|
if (embeddedMode && !configuredServerUrl) {
|
|
return `http://localhost:${defaultPort}${path}`;
|
|
}
|
|
if (configuredServerUrl) {
|
|
const baseUrl = configuredServerUrl.replace(/\/$/, "");
|
|
const url = `${baseUrl}${path}`;
|
|
return url;
|
|
}
|
|
console.warn("Electron mode but no server configured!");
|
|
return "http://no-server-configured";
|
|
} else if (devMode) {
|
|
const protocol = window.location.protocol === "https:" ? "https" : "http";
|
|
const sslPort = protocol === "https" ? 8443 : defaultPort;
|
|
const url = `${protocol}://${apiHost}:${sslPort}${path}`;
|
|
return url;
|
|
} else {
|
|
return getBasePath() + path;
|
|
}
|
|
}
|
|
|
|
function initializeApiInstances() {
|
|
// Host Management API (port 30001) - supports SSH, RDP, VNC, Telnet
|
|
hostApi = createApiInstance(getApiUrl("/host", 30001), "HOST");
|
|
sshHostApi = hostApi;
|
|
|
|
// Tunnel Management API (port 30003)
|
|
tunnelApi = createApiInstance(getApiUrl("/ssh", 30003), "TUNNEL");
|
|
|
|
// File Manager Operations API (port 30004)
|
|
fileManagerApi = createApiInstance(
|
|
getApiUrl("/ssh/file_manager", 30004),
|
|
"FILE_MANAGER",
|
|
);
|
|
|
|
// Server Statistics API (port 30005)
|
|
statsApi = createApiInstance(getApiUrl("", 30005), "STATS");
|
|
|
|
// Authentication API (port 30001)
|
|
authApi = createApiInstance(getApiUrl("", 30001), "AUTH");
|
|
|
|
// Dashboard API (port 30006)
|
|
dashboardApi = createApiInstance(getApiUrl("", 30006), "DASHBOARD");
|
|
|
|
// RBAC API (port 30001)
|
|
rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC");
|
|
|
|
// Docker Management API (port 30007)
|
|
dockerApi = createApiInstance(getApiUrl("/docker", 30007), "DOCKER");
|
|
|
|
// Tmux Monitor API (port 30010) --- tmux-monitor ---
|
|
tmuxMonitorApi = createApiInstance(
|
|
getApiUrl("/tmux_monitor", 30010),
|
|
"TMUX_MONITOR",
|
|
);
|
|
|
|
// Homepage API (port 30012)
|
|
homepageApi = createApiInstance(getApiUrl("/homepage", 30012), "HOMEPAGE");
|
|
}
|
|
|
|
// Host Management API (port 30001) - supports SSH, RDP, VNC, Telnet
|
|
export let hostApi: AxiosInstance;
|
|
// Backward compatibility
|
|
export let sshHostApi: AxiosInstance;
|
|
|
|
// Tunnel Management API (port 30003)
|
|
export let tunnelApi: AxiosInstance;
|
|
|
|
// File Manager Operations API (port 30004)
|
|
export let fileManagerApi: AxiosInstance;
|
|
|
|
// Server Statistics API (port 30005)
|
|
export let statsApi: AxiosInstance;
|
|
|
|
// Authentication API (port 30001)
|
|
export let authApi: AxiosInstance;
|
|
|
|
// Dashboard API (port 30006)
|
|
export let dashboardApi: AxiosInstance;
|
|
|
|
// RBAC API (port 30001)
|
|
export let rbacApi: AxiosInstance;
|
|
|
|
// Docker Management API (port 30007)
|
|
export let dockerApi: AxiosInstance;
|
|
|
|
// Tmux Monitor API (port 30010) --- tmux-monitor ---
|
|
export let tmuxMonitorApi: AxiosInstance;
|
|
|
|
// Homepage API (port 30012)
|
|
export let homepageApi: AxiosInstance;
|
|
|
|
// Pre-initialize with default values to avoid undefined errors during early mounting
|
|
initializeApiInstances();
|
|
|
|
let _resolveAppReady!: () => void;
|
|
export const appReadyPromise: Promise<void> = new Promise((resolve) => {
|
|
_resolveAppReady = resolve;
|
|
});
|
|
|
|
function initializeApp() {
|
|
if (isElectron()) {
|
|
Promise.all([getServerConfig(), getEmbeddedServerStatus()])
|
|
.then(([config, status]) => {
|
|
if (status?.embedded && status?.running && !config?.serverUrl) {
|
|
embeddedMode = true;
|
|
}
|
|
if (config?.serverUrl) {
|
|
configuredServerUrl = config.serverUrl;
|
|
(
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).configuredServerUrl = configuredServerUrl;
|
|
} else if (embeddedMode) {
|
|
// Embedded backend running, no remote server needed
|
|
} else {
|
|
console.warn("No server URL in config");
|
|
}
|
|
initializeApiInstances();
|
|
})
|
|
.catch((error) => {
|
|
console.error(
|
|
"Failed to load server config, initializing with default:",
|
|
error,
|
|
);
|
|
initializeApiInstances();
|
|
})
|
|
.finally(() => {
|
|
_resolveAppReady();
|
|
});
|
|
} else {
|
|
initializeApiInstances();
|
|
_resolveAppReady();
|
|
}
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initializeApp);
|
|
} else {
|
|
initializeApp();
|
|
}
|
|
|
|
function updateApiInstances() {
|
|
systemLogger.info("Updating API instances with new server configuration", {
|
|
operation: "api_instance_update",
|
|
configuredServerUrl,
|
|
});
|
|
|
|
initializeApiInstances();
|
|
|
|
(
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).configuredServerUrl = configuredServerUrl;
|
|
|
|
systemLogger.success("All API instances updated successfully", {
|
|
operation: "api_instance_update_complete",
|
|
configuredServerUrl,
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// ERROR HANDLING
|
|
// ============================================================================
|
|
|
|
class ApiError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public status?: number,
|
|
public code?: string,
|
|
) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
export function handleApiError(error: unknown, operation: string): never {
|
|
const context: LogContext = {
|
|
operation: "error_handling",
|
|
errorOperation: operation,
|
|
};
|
|
|
|
if (axios.isAxiosError(error)) {
|
|
const status = error.response?.status;
|
|
const message =
|
|
error.response?.data?.message ||
|
|
error.response?.data?.error ||
|
|
error.message;
|
|
const code = error.response?.data?.code || error.response?.data?.error;
|
|
const url = error.config?.url;
|
|
const method = error.config?.method?.toUpperCase();
|
|
|
|
const errorContext: LogContext = {
|
|
...context,
|
|
method,
|
|
url,
|
|
status,
|
|
errorCode: code,
|
|
errorMessage: message,
|
|
};
|
|
|
|
if (status === 401) {
|
|
authLogger.warn(
|
|
`Auth failed: ${method} ${url} - ${message}`,
|
|
errorContext,
|
|
);
|
|
|
|
const isLoginEndpoint = url?.includes("/users/login");
|
|
const errorMessage = isLoginEndpoint
|
|
? message
|
|
: "Authentication required. Please log in again.";
|
|
|
|
throw new ApiError(errorMessage, 401, code || "AUTH_REQUIRED");
|
|
} else if (status === 403) {
|
|
authLogger.warn(`Access denied: ${method} ${url}`, errorContext);
|
|
const apiError = new ApiError(
|
|
code === "TOTP_REQUIRED"
|
|
? message
|
|
: "Access denied. You do not have permission to perform this action.",
|
|
403,
|
|
code || "ACCESS_DENIED",
|
|
);
|
|
(apiError as ApiError & { response?: unknown }).response = error.response;
|
|
throw apiError;
|
|
} else if (status === 404) {
|
|
apiLogger.warn(`Not found: ${method} ${url}`, errorContext);
|
|
throw new ApiError(
|
|
"Resource not found. The requested item may have been deleted.",
|
|
404,
|
|
"NOT_FOUND",
|
|
);
|
|
} else if (status === 409) {
|
|
apiLogger.warn(`Conflict: ${method} ${url}`, errorContext);
|
|
throw new ApiError(
|
|
"Conflict. The resource already exists or is in use.",
|
|
409,
|
|
"CONFLICT",
|
|
);
|
|
} else if (status === 422) {
|
|
apiLogger.warn(
|
|
`Validation error: ${method} ${url} - ${message}`,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(
|
|
"Validation error. Please check your input and try again.",
|
|
422,
|
|
"VALIDATION_ERROR",
|
|
);
|
|
} else if (status && status >= 500) {
|
|
apiLogger.error(
|
|
`Server error: ${method} ${url} - ${message}`,
|
|
error,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(
|
|
"Server error occurred. Please try again later.",
|
|
status,
|
|
"SERVER_ERROR",
|
|
);
|
|
} else if (status === 0) {
|
|
if (url.includes("no-server-configured")) {
|
|
apiLogger.error(
|
|
`No server configured: ${method} ${url}`,
|
|
error,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(
|
|
"No server configured. Please configure a Termix server first.",
|
|
0,
|
|
"NO_SERVER_CONFIGURED",
|
|
);
|
|
}
|
|
apiLogger.error(
|
|
`Network error: ${method} ${url} - ${message}`,
|
|
error,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(
|
|
"Network error. Please check your connection and try again.",
|
|
0,
|
|
"NETWORK_ERROR",
|
|
);
|
|
} else {
|
|
apiLogger.error(
|
|
`Request failed: ${method} ${url} - ${message}`,
|
|
error,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(message || `Failed to ${operation}`, status, code);
|
|
}
|
|
}
|
|
|
|
if (error instanceof ApiError) {
|
|
throw error;
|
|
}
|
|
|
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
apiLogger.error(
|
|
`Unexpected error during ${operation}: ${errorMessage}`,
|
|
error,
|
|
context,
|
|
);
|
|
throw new ApiError(
|
|
`Unexpected error during ${operation}: ${errorMessage}`,
|
|
undefined,
|
|
"UNKNOWN_ERROR",
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
// HOST-TO-HOST TRANSFER
|
|
// ============================================================================
|
|
|
|
export type TransferMethodPreference = "auto" | "tar" | "item_sftp";
|
|
|
|
export interface TransferScanSummary {
|
|
fileCount: number;
|
|
totalBytes: number;
|
|
largestFileBytes: number;
|
|
incompressibleRatio: number;
|
|
}
|
|
|
|
export interface TransferMethodPreview {
|
|
methodPreference: TransferMethodPreference;
|
|
resolvedMethod: "tar" | "item_sftp";
|
|
reasonKey: string;
|
|
sourcePlatform: "unix" | "windows";
|
|
destPlatform: "unix" | "windows";
|
|
sourceHasTar: boolean;
|
|
destHasTar: boolean;
|
|
summary: TransferScanSummary;
|
|
}
|
|
|
|
export async function getTransferMethodPreview(
|
|
sourceSessionId: string,
|
|
sourcePaths: string[],
|
|
destSessionId: string,
|
|
destPath: string,
|
|
methodPreference?: TransferMethodPreference,
|
|
): Promise<TransferMethodPreview> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/transferMethodPreview", {
|
|
sourceSessionId,
|
|
sourcePaths,
|
|
destSessionId,
|
|
destPath,
|
|
methodPreference: methodPreference ?? "auto",
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "preview transfer method");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export interface TransferHopMetrics {
|
|
id: string;
|
|
mbPerSec?: number;
|
|
}
|
|
|
|
export interface TransferTimings {
|
|
prepareDestMs?: number;
|
|
compressMs?: number;
|
|
transferMs?: number;
|
|
extractMs?: number;
|
|
sourceDeleteMs?: number;
|
|
totalMs?: number;
|
|
transferBytes?: number;
|
|
endToEndMbPerSec?: number;
|
|
hops?: TransferHopMetrics[];
|
|
}
|
|
|
|
export function getTransferProgressPercent(
|
|
status: TransferProgressResponse,
|
|
): number | undefined {
|
|
if (
|
|
status.bytesTransferred !== undefined &&
|
|
status.totalBytes !== undefined &&
|
|
status.totalBytes > 0
|
|
) {
|
|
return Math.min(
|
|
100,
|
|
Math.round((status.bytesTransferred / status.totalBytes) * 100),
|
|
);
|
|
}
|
|
if (
|
|
status.itemsCompleted !== undefined &&
|
|
status.totalItems !== undefined &&
|
|
status.totalItems > 0
|
|
) {
|
|
return Math.min(
|
|
100,
|
|
Math.round((status.itemsCompleted / status.totalItems) * 100),
|
|
);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function formatTransferMbPerSec(
|
|
mbPerSec?: number,
|
|
_bytes?: number,
|
|
_ms?: number,
|
|
): string {
|
|
if (mbPerSec === undefined || mbPerSec <= 0) return "";
|
|
if (mbPerSec < 1) return `${(mbPerSec * 1024).toFixed(0)} KB/s`;
|
|
return `${mbPerSec.toFixed(1)} MB/s`;
|
|
}
|
|
|
|
export function formatDurationMs(ms?: number): string {
|
|
if (ms === undefined) return "";
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
const minutes = Math.floor(ms / 60000);
|
|
const seconds = Math.round((ms % 60000) / 1000);
|
|
return `${minutes}m ${seconds}s`;
|
|
}
|
|
|
|
interface TransferProgressTracker {
|
|
update(status: TransferProgressResponse): {
|
|
rate: number | undefined;
|
|
stalled: boolean;
|
|
};
|
|
}
|
|
|
|
export function createTransferProgressTracker(): TransferProgressTracker {
|
|
let lastBytes: number | undefined;
|
|
let lastTime: number | undefined;
|
|
let lastRate: number | undefined;
|
|
let lastActivityTime: number | undefined;
|
|
const STALL_THRESHOLD_MS = 5000;
|
|
|
|
return {
|
|
update(status) {
|
|
const now = Date.now();
|
|
const bytes = status.bytesTransferred;
|
|
|
|
if (
|
|
bytes !== undefined &&
|
|
lastBytes !== undefined &&
|
|
lastTime !== undefined
|
|
) {
|
|
const deltaBytes = bytes - lastBytes;
|
|
const deltaMs = now - lastTime;
|
|
if (deltaMs > 0 && deltaBytes >= 0) {
|
|
lastRate = (deltaBytes / deltaMs / 1024 / 1024) * 1000;
|
|
if (deltaBytes > 0) lastActivityTime = now;
|
|
}
|
|
} else if (bytes !== undefined) {
|
|
lastActivityTime = now;
|
|
}
|
|
|
|
lastBytes = bytes;
|
|
lastTime = now;
|
|
|
|
const stalled =
|
|
lastActivityTime !== undefined &&
|
|
now - lastActivityTime > STALL_THRESHOLD_MS &&
|
|
status.status === "running" &&
|
|
status.phase === "transferring";
|
|
|
|
return { rate: lastRate, stalled };
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface TransferProgressResponse {
|
|
transferId: string;
|
|
status: "running" | "success" | "partial" | "error" | "cancelled";
|
|
phase: "compressing" | "transferring" | "extracting" | "reconnecting";
|
|
bytesTransferred?: number;
|
|
totalBytes?: number;
|
|
itemsCompleted?: number;
|
|
totalItems?: number;
|
|
failedPaths?: string[];
|
|
message?: string;
|
|
method?: "stream" | "tar" | "item_sftp";
|
|
sourcePaths?: string[];
|
|
destPath?: string;
|
|
sourceSessionId?: string;
|
|
destSessionId?: string;
|
|
startedAt?: number;
|
|
timings?: TransferTimings;
|
|
sourceDeleted?: boolean;
|
|
moveRequested?: boolean;
|
|
partialDestRemaining?: boolean;
|
|
cleanupCompleted?: boolean;
|
|
retryable?: boolean;
|
|
parallelSegmentCount?: number;
|
|
}
|
|
|
|
export async function transferToHost(
|
|
sourceSessionId: string,
|
|
sourcePaths: string[],
|
|
destSessionId: string,
|
|
destPath: string,
|
|
move?: boolean,
|
|
methodPreference?: TransferMethodPreference,
|
|
parallelSegmentCount = 2,
|
|
): Promise<{ transferId: string }> {
|
|
try {
|
|
fileLogger.info("Starting host transfer", {
|
|
operation: "host_transfer",
|
|
sourceSessionId,
|
|
destSessionId,
|
|
sourcePaths,
|
|
destPath,
|
|
move,
|
|
methodPreference,
|
|
parallelSegmentCount,
|
|
});
|
|
|
|
const response = await fileManagerApi.post("/ssh/transferToHost", {
|
|
sourceSessionId,
|
|
sourcePaths,
|
|
destSessionId,
|
|
destPath,
|
|
move,
|
|
methodPreference: methodPreference ?? "auto",
|
|
parallelSegmentCount,
|
|
});
|
|
|
|
return response.data;
|
|
} catch (error) {
|
|
fileLogger.error("Failed to start host transfer", error, {
|
|
operation: "host_transfer",
|
|
sourceSessionId,
|
|
destSessionId,
|
|
sourcePaths,
|
|
});
|
|
handleApiError(error, "transfer to host");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getTransferStatus(
|
|
transferId: string,
|
|
): Promise<TransferProgressResponse> {
|
|
try {
|
|
const response = await fileManagerApi.get(
|
|
`/ssh/transferStatus/${transferId}`,
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "get transfer status");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function listActiveTransfers(): Promise<{
|
|
transfers: TransferProgressResponse[];
|
|
}> {
|
|
try {
|
|
const response = await fileManagerApi.get("/ssh/activeTransfers");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "list active transfers");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function cancelTransferToHost(transferId: string): Promise<void> {
|
|
try {
|
|
await fileManagerApi.post(`/ssh/transferCancel/${transferId}`);
|
|
} catch (error) {
|
|
fileLogger.warn("Transfer cancel request failed (non-fatal)", {
|
|
operation: "host_transfer",
|
|
transferId,
|
|
error,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function cleanupCancelledTransfer(
|
|
transferId: string,
|
|
): Promise<{ removedPaths: string[]; failedPaths: string[] }> {
|
|
try {
|
|
const response = await fileManagerApi.post(
|
|
`/ssh/transferCleanup/${transferId}`,
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "clean up cancelled transfer");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function retryTransferToHost(
|
|
transferId: string,
|
|
): Promise<{ ok: boolean; transferId: string }> {
|
|
try {
|
|
const response = await fileManagerApi.post(
|
|
`/ssh/transferRetry/${transferId}`,
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "retry transfer");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function pollTransferUntilComplete(
|
|
transferId: string,
|
|
onProgress?: (status: TransferProgressResponse) => void,
|
|
intervalMs = 500,
|
|
): Promise<TransferProgressResponse> {
|
|
return new Promise((resolve, reject) => {
|
|
const poll = async () => {
|
|
try {
|
|
const status = await getTransferStatus(transferId);
|
|
onProgress?.(status);
|
|
|
|
if (
|
|
status.status === "success" ||
|
|
status.status === "partial" ||
|
|
status.status === "error" ||
|
|
status.status === "cancelled"
|
|
) {
|
|
resolve(status);
|
|
return;
|
|
}
|
|
|
|
setTimeout(poll, intervalMs);
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
};
|
|
|
|
void poll();
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// FILE MANAGER DATA
|
|
// ============================================================================
|
|
|
|
export interface TransferDestination {
|
|
id: number;
|
|
userId: string;
|
|
sourceHostId: number;
|
|
destHostId: number;
|
|
destPath: string;
|
|
destPathLabel?: string;
|
|
lastUsed?: string;
|
|
}
|
|
|
|
export async function getTransferRecent(
|
|
sourceHostId: number,
|
|
): Promise<TransferDestination[]> {
|
|
try {
|
|
const response = await authApi.get("/host/transfer/recent", {
|
|
params: { sourceHostId },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "get transfer recent destinations");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function addTransferRecent(
|
|
sourceHostId: number,
|
|
destHostId: number,
|
|
destPath: string,
|
|
destPathLabel?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/host/transfer/recent", {
|
|
sourceHostId,
|
|
destHostId,
|
|
destPath,
|
|
destPathLabel,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "add transfer recent destination");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export {
|
|
getSSHHosts,
|
|
createSSHHost,
|
|
updateSSHHost,
|
|
wakeOnLan,
|
|
bulkImportSSHHosts,
|
|
importSSHConfigHosts,
|
|
discoverProxmoxGuests,
|
|
syncProxmoxGuests,
|
|
bulkUpdateSSHHosts,
|
|
deleteSSHHost,
|
|
getSSHHostById,
|
|
exportSSHHostWithCredentials,
|
|
exportAllSSHHosts,
|
|
enableAutoStart,
|
|
disableAutoStart,
|
|
getAutoStartStatus,
|
|
testProxyConnection,
|
|
} from "@/api/ssh-host-management-api";
|
|
|
|
export {
|
|
getTunnelStatuses,
|
|
subscribeTunnelStatuses,
|
|
getTunnelStatusByName,
|
|
connectTunnel,
|
|
disconnectTunnel,
|
|
cancelTunnel,
|
|
getC2STunnelPresets,
|
|
createC2STunnelPreset,
|
|
updateC2STunnelPreset,
|
|
deleteC2STunnelPreset,
|
|
} from "@/api/tunnel-api";
|
|
|
|
export {
|
|
getFileManagerRecent,
|
|
addFileManagerRecent,
|
|
removeFileManagerRecent,
|
|
getFileManagerPinned,
|
|
addFileManagerPinned,
|
|
removeFileManagerPinned,
|
|
getFileManagerShortcuts,
|
|
addFileManagerShortcut,
|
|
removeFileManagerShortcut,
|
|
} from "@/api/file-manager-metadata-api";
|
|
|
|
export {
|
|
connectSSH,
|
|
disconnectSSH,
|
|
verifySSHTOTP,
|
|
verifySSHWarpgate,
|
|
quickConnect,
|
|
getSSHStatus,
|
|
keepSSHAlive,
|
|
listSSHFiles,
|
|
identifySSHSymlink,
|
|
resolveSSHPath,
|
|
readSSHFile,
|
|
writeSSHFile,
|
|
uploadSSHFile,
|
|
downloadSSHFile,
|
|
downloadSSHFileStream,
|
|
createSSHFile,
|
|
createSSHFolder,
|
|
deleteSSHItem,
|
|
setSudoPassword,
|
|
copySSHItem,
|
|
renameSSHItem,
|
|
moveSSHItem,
|
|
changeSSHPermissions,
|
|
extractSSHArchive,
|
|
compressSSHFiles,
|
|
ensureSSHSessionForHost,
|
|
browseSSHDirectory,
|
|
type HostConnectionState,
|
|
type EnsureSSHSessionResult,
|
|
type BrowseSSHDirectoryResult,
|
|
} from "@/api/ssh-file-operations-api";
|
|
|
|
export {
|
|
getRecentFiles,
|
|
addRecentFile,
|
|
removeRecentFile,
|
|
getPinnedFiles,
|
|
addPinnedFile,
|
|
removePinnedFile,
|
|
getFolderShortcuts,
|
|
addFolderShortcut,
|
|
removeFolderShortcut,
|
|
} from "@/api/file-manager-data-api";
|
|
|
|
export {
|
|
getAllServerStatuses,
|
|
getServerStatusById,
|
|
getServerMetricsById,
|
|
startMetricsPolling,
|
|
stopMetricsPolling,
|
|
sendMetricsHeartbeat,
|
|
registerMetricsViewer,
|
|
unregisterMetricsViewer,
|
|
submitMetricsTOTP,
|
|
refreshServerPolling,
|
|
notifyHostCreatedOrUpdated,
|
|
} from "@/api/host-metrics-status-api";
|
|
|
|
export {
|
|
getHostMetricsLayout,
|
|
saveHostMetricsLayout,
|
|
getHostPlatform,
|
|
managerGet,
|
|
managerGetSub,
|
|
managerPost,
|
|
type PlatformInfo,
|
|
} from "@/api/host-metrics-api";
|
|
|
|
export {
|
|
getGlobalMonitoringSettings,
|
|
updateGlobalMonitoringSettings,
|
|
getLogLevel,
|
|
updateLogLevel,
|
|
getSessionTimeout,
|
|
updateSessionTimeout,
|
|
getGuacamoleSettings,
|
|
updateGuacamoleSettings,
|
|
} from "@/api/settings-api";
|
|
|
|
// ============================================================================
|
|
// AUTHENTICATION
|
|
// ============================================================================
|
|
|
|
export async function registerUser(
|
|
username: string,
|
|
password: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/create", {
|
|
username,
|
|
password,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "register user");
|
|
}
|
|
}
|
|
|
|
export async function adminCreateUser(
|
|
username: string,
|
|
password: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/admin-create", {
|
|
username,
|
|
password,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "admin create user");
|
|
}
|
|
}
|
|
|
|
export async function loginUser(
|
|
username: string,
|
|
password: string,
|
|
rememberMe: boolean = false,
|
|
): Promise<AuthResponse> {
|
|
try {
|
|
const response = await authApi.post("/users/login", {
|
|
username,
|
|
password,
|
|
rememberMe,
|
|
});
|
|
|
|
const isInIframe =
|
|
typeof window !== "undefined" && window.self !== window.top;
|
|
|
|
if (isInIframe && isElectron() && response.data.success) {
|
|
try {
|
|
window.parent.postMessage(
|
|
{
|
|
type: "AUTH_SUCCESS",
|
|
source: "login_api",
|
|
platform: "desktop",
|
|
timestamp: Date.now(),
|
|
},
|
|
window.location.origin,
|
|
);
|
|
} catch (e) {
|
|
console.error("[main-axios] Error posting message to parent:", e);
|
|
}
|
|
}
|
|
|
|
if (response.data.token) {
|
|
localStorage.setItem("jwt", response.data.token);
|
|
}
|
|
|
|
if (response.data.success && !response.data.requires_totp) {
|
|
markUserAuthenticated();
|
|
}
|
|
|
|
return {
|
|
success: response.data.success,
|
|
is_admin: response.data.is_admin,
|
|
username: response.data.username,
|
|
requires_totp: response.data.requires_totp,
|
|
temp_token: response.data.temp_token,
|
|
rememberMe: response.data.rememberMe,
|
|
is_oidc: response.data.is_oidc,
|
|
totp_enabled: response.data.totp_enabled,
|
|
token: response.data.token,
|
|
};
|
|
} catch (error) {
|
|
throw handleApiError(error, "login user");
|
|
}
|
|
}
|
|
|
|
export async function logoutUser(): Promise<{
|
|
success: boolean;
|
|
message: string;
|
|
}> {
|
|
try {
|
|
const response = await authApi.post("/users/logout");
|
|
|
|
clearTermixSessionStorage();
|
|
|
|
if (isElectron()) {
|
|
const electronAPI = (
|
|
window as unknown as {
|
|
electronAPI?: { clearSessionCookies?: () => Promise<void> };
|
|
}
|
|
).electronAPI;
|
|
electronAPI?.clearSessionCookies?.().catch(() => {});
|
|
} else {
|
|
const isSecure = window.location.protocol === "https:";
|
|
const cookieString = isSecure
|
|
? "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; Secure; SameSite=Lax"
|
|
: "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax";
|
|
document.cookie = cookieString;
|
|
}
|
|
|
|
return response.data;
|
|
} catch (error) {
|
|
clearTermixSessionStorage();
|
|
|
|
if (isElectron()) {
|
|
const electronAPI = (
|
|
window as unknown as {
|
|
electronAPI?: { clearSessionCookies?: () => Promise<void> };
|
|
}
|
|
).electronAPI;
|
|
electronAPI?.clearSessionCookies?.().catch(() => {});
|
|
} else {
|
|
const isSecure = window.location.protocol === "https:";
|
|
const cookieString = isSecure
|
|
? "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; Secure; SameSite=Lax"
|
|
: "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax";
|
|
document.cookie = cookieString;
|
|
}
|
|
handleApiError(error, "logout user");
|
|
}
|
|
}
|
|
|
|
export async function getUserInfo(): Promise<UserInfo> {
|
|
try {
|
|
const response = await authApi.get("/users/me");
|
|
markUserAuthenticated();
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch user info");
|
|
}
|
|
}
|
|
|
|
export async function dismissDonationModal(): Promise<void> {
|
|
try {
|
|
await authApi.post("/users/me/dismiss-donation-modal");
|
|
} catch (error) {
|
|
handleApiError(error, "dismiss donation modal");
|
|
}
|
|
}
|
|
|
|
export async function getCurrentToken(): Promise<string | null> {
|
|
try {
|
|
const response = await authApi.get("/users/me/token");
|
|
return response.data?.token ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function unlockUserData(
|
|
password: string,
|
|
): Promise<{ success: boolean; message: string }> {
|
|
try {
|
|
const response = await authApi.post("/users/unlock-data", { password });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "unlock user data");
|
|
}
|
|
}
|
|
|
|
export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> {
|
|
try {
|
|
const response = await authApi.get("/users/registration-allowed");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "check registration status");
|
|
}
|
|
}
|
|
|
|
export async function getPasswordLoginAllowed(): Promise<{ allowed: boolean }> {
|
|
try {
|
|
const response = await authApi.get("/users/password-login-allowed");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "check password login status");
|
|
}
|
|
}
|
|
|
|
export async function getOIDCConfig(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/users/oidc-config");
|
|
return response.data;
|
|
} catch (error: unknown) {
|
|
console.warn(
|
|
"Failed to fetch OIDC config:",
|
|
error.response?.data?.error || error.message,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getAdminOIDCConfig(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/users/oidc-config/admin");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch admin OIDC config");
|
|
}
|
|
}
|
|
|
|
export async function getSetupRequired(): Promise<{ setup_required: boolean }> {
|
|
try {
|
|
const response = await authApi.get("/users/setup-required");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "check setup status");
|
|
}
|
|
}
|
|
|
|
export async function getUserCount(): Promise<UserCount> {
|
|
try {
|
|
const response = await authApi.get("/users/count");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch user count");
|
|
}
|
|
}
|
|
|
|
export async function initiatePasswordReset(
|
|
username: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/initiate-reset", { username });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "initiate password reset");
|
|
}
|
|
}
|
|
|
|
export async function verifyPasswordResetCode(
|
|
username: string,
|
|
resetCode: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/verify-reset-code", {
|
|
username,
|
|
resetCode,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "verify reset code");
|
|
}
|
|
}
|
|
|
|
export async function completePasswordReset(
|
|
username: string,
|
|
tempToken: string,
|
|
newPassword: string,
|
|
confirmDataWipe = false,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/complete-reset", {
|
|
username,
|
|
tempToken,
|
|
newPassword,
|
|
confirmDataWipe,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "complete password reset");
|
|
}
|
|
}
|
|
|
|
export async function changePassword(oldPassword: string, newPassword: string) {
|
|
try {
|
|
const response = await authApi.post("/users/change-password", {
|
|
oldPassword,
|
|
newPassword,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "change password");
|
|
}
|
|
}
|
|
|
|
export async function getOIDCAuthorizeUrl(
|
|
rememberMe = false,
|
|
desktopCallbackPort?: number,
|
|
providerId?: number,
|
|
appCallbackUrl?: string,
|
|
): Promise<OIDCAuthorize> {
|
|
try {
|
|
const response = await authApi.get("/users/oidc/authorize", {
|
|
params: { rememberMe, desktopCallbackPort, providerId, appCallbackUrl },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "get OIDC authorize URL");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
export {
|
|
getUserList,
|
|
getSessions,
|
|
revokeSession,
|
|
revokeAllUserSessions,
|
|
createApiKey,
|
|
getApiKeys,
|
|
deleteApiKey,
|
|
makeUserAdmin,
|
|
removeAdminStatus,
|
|
deleteUser,
|
|
deleteAccount,
|
|
updateRegistrationAllowed,
|
|
getOidcAutoProvision,
|
|
updateOidcAutoProvision,
|
|
getOidcSilentLoginDefault,
|
|
updateOidcSilentLoginDefault,
|
|
updatePasswordLoginAllowed,
|
|
getPasswordResetAllowed,
|
|
updatePasswordResetAllowed,
|
|
updateOIDCConfig,
|
|
disableOIDCConfig,
|
|
getCommandHistoryEnabled,
|
|
updateCommandHistoryEnabled,
|
|
adminResetUserPassword,
|
|
adminDisableUserTotp,
|
|
adminExportUserData,
|
|
type ApiKey,
|
|
type CreatedApiKey,
|
|
} from "@/api/user-management-api";
|
|
|
|
// ADMIN USER DATA MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export {
|
|
adminGetUserHosts,
|
|
adminCreateUserHost,
|
|
adminUpdateUserHost,
|
|
adminDeleteUserHost,
|
|
adminGetHostPassword,
|
|
adminGetUserCredentials,
|
|
adminGetUserCredentialDetails,
|
|
adminCreateUserCredential,
|
|
adminUpdateUserCredential,
|
|
adminDeleteUserCredential,
|
|
adminGetUserSnippets,
|
|
adminCreateUserSnippet,
|
|
adminUpdateUserSnippet,
|
|
adminDeleteUserSnippet,
|
|
} from "@/api/admin-user-data-api";
|
|
|
|
export {
|
|
setupTOTP,
|
|
enableTOTP,
|
|
disableTOTP,
|
|
verifyTOTPLogin,
|
|
generateBackupCodes,
|
|
getUserAlerts,
|
|
dismissAlert,
|
|
getReleasesRSS,
|
|
getVersionInfo,
|
|
getDatabaseHealth,
|
|
} from "@/api/system-status-api";
|
|
|
|
// SSH CREDENTIALS MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export {
|
|
getCredentials,
|
|
getCredentialDetails,
|
|
createCredential,
|
|
updateCredential,
|
|
deleteCredential,
|
|
getCredentialHosts,
|
|
getCredentialFolders,
|
|
getSSHHostWithCredentials,
|
|
getHostPassword,
|
|
applyCredentialToHost,
|
|
removeCredentialFromHost,
|
|
migrateHostToCredential,
|
|
getFoldersWithStats,
|
|
renameFolder,
|
|
getSSHFolders,
|
|
updateFolderMetadata,
|
|
deleteAllHostsInFolder,
|
|
renameCredentialFolder,
|
|
detectKeyType,
|
|
detectPublicKeyType,
|
|
validateKeyPair,
|
|
generatePublicKeyFromPrivate,
|
|
generateKeyPair,
|
|
deployCredentialToHost,
|
|
} from "@/api/credentials-api";
|
|
|
|
export {
|
|
getVaultProfiles,
|
|
createVaultProfile,
|
|
updateVaultProfile,
|
|
deleteVaultProfile,
|
|
type VaultProfilePayload,
|
|
} from "@/api/vault-profiles-api";
|
|
|
|
// ============================================================================
|
|
// SNIPPETS API
|
|
// ============================================================================
|
|
|
|
export type {
|
|
NetworkTopologyNode,
|
|
NetworkTopologyEdge,
|
|
NetworkTopologyData,
|
|
} from "@/api/snippets-api";
|
|
export {
|
|
getSnippets,
|
|
createSnippet,
|
|
updateSnippet,
|
|
deleteSnippet,
|
|
executeSnippet,
|
|
getNetworkTopology,
|
|
saveNetworkTopology,
|
|
getSnippetFolders,
|
|
createSnippetFolder,
|
|
updateSnippetFolderMetadata,
|
|
renameSnippetFolder,
|
|
deleteSnippetFolder,
|
|
reorderSnippets,
|
|
} from "@/api/snippets-api";
|
|
|
|
// ============================================================================
|
|
export type {
|
|
UptimeInfo,
|
|
RecentActivityItem,
|
|
ServiceLink,
|
|
} from "@/api/dashboard-api";
|
|
export {
|
|
getUptime,
|
|
getRecentActivity,
|
|
logActivity,
|
|
resetRecentActivity,
|
|
getServiceLinks,
|
|
createServiceLink,
|
|
deleteServiceLink,
|
|
updateServiceLink,
|
|
} from "@/api/dashboard-api";
|
|
|
|
// ============================================================================
|
|
export {
|
|
saveCommandToHistory,
|
|
getCommandHistory,
|
|
deleteCommandFromHistory,
|
|
clearCommandHistory,
|
|
} from "@/api/command-history-api";
|
|
|
|
export {
|
|
linkOIDCToPasswordAccount,
|
|
unlinkOIDCFromPasswordAccount,
|
|
} from "@/api/oidc-account-api";
|
|
|
|
export type {
|
|
GuacamoleTokenRequest,
|
|
GuacamoleTokenResponse,
|
|
} from "@/api/guacamole-api";
|
|
export {
|
|
getGuacamoleDpi,
|
|
getGuacamoleToken,
|
|
getGuacamoleTokenFromHost,
|
|
getGuacdStatus,
|
|
} from "@/api/guacamole-api";
|
|
|
|
// ============================================================================
|
|
// RBAC MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export {
|
|
getRoles,
|
|
createRole,
|
|
updateRole,
|
|
deleteRole,
|
|
getUserRoles,
|
|
assignRoleToUser,
|
|
removeRoleFromUser,
|
|
shareHost,
|
|
updateHostAccess,
|
|
getHostAccess,
|
|
revokeHostAccess,
|
|
getPermissionsCatalog,
|
|
getSharedHosts,
|
|
shareSnippet,
|
|
getSnippetAccess,
|
|
revokeSnippetAccess,
|
|
getSharedSnippets,
|
|
} from "@/api/rbac-api";
|
|
export type {
|
|
SharePermissionLevel,
|
|
ShareTarget,
|
|
PermissionCatalogEntry,
|
|
} from "@/api/rbac-api";
|
|
|
|
// ============================================================================
|
|
// DOCKER MANAGEMENT API
|
|
// ============================================================================
|
|
|
|
export {
|
|
connectDockerSession,
|
|
verifyDockerTOTP,
|
|
verifyDockerWarpgate,
|
|
disconnectDockerSession,
|
|
keepaliveDockerSession,
|
|
getDockerSessionStatus,
|
|
validateDockerAvailability,
|
|
listDockerContainers,
|
|
getDockerContainerDetails,
|
|
startDockerContainer,
|
|
stopDockerContainer,
|
|
restartDockerContainer,
|
|
pauseDockerContainer,
|
|
unpauseDockerContainer,
|
|
removeDockerContainer,
|
|
getContainerLogs,
|
|
downloadContainerLogs,
|
|
getContainerStats,
|
|
} from "@/api/docker-api";
|
|
|
|
export {
|
|
getOpenTabs,
|
|
syncOpenTabs,
|
|
deleteOpenTab,
|
|
patchOpenTab,
|
|
addOpenTab,
|
|
getActiveSessions,
|
|
getUserPreferences,
|
|
saveUserPreferences,
|
|
type OpenTabRecord,
|
|
type OpenTabSyncPayload,
|
|
type OpenTabUpsertPayload,
|
|
type ActiveSessionInfo,
|
|
type UserPreferences,
|
|
} from "@/api/open-tabs-api";
|
|
|
|
// ============================================================================
|
|
// TERMIX ID API
|
|
// ============================================================================
|
|
|
|
export {
|
|
getMyTermixId,
|
|
checkTermixIdHandle,
|
|
createTermixId,
|
|
updateTermixId,
|
|
deleteTermixId,
|
|
addTermixIdKey,
|
|
generateTermixIdKey,
|
|
setTermixIdKeyEnabled,
|
|
deleteTermixIdKey,
|
|
getMyCa,
|
|
createCa,
|
|
rotateCa,
|
|
deleteCa,
|
|
issueCertificate,
|
|
getLinkedCredentialIds,
|
|
type TermixIdentity,
|
|
type TermixIdentityKey,
|
|
type TermixIdMe,
|
|
type GeneratedKey,
|
|
type TermixIdCa,
|
|
type IssuedCertificate,
|
|
} from "@/api/termix-id-api";
|