From 7d702fd878600689ebf64d03493df140cc93c0da Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Tue, 13 Oct 2026 15:01:00 +0200 Subject: [PATCH 1/4] feat: added new webssh support --- eslint-suppressions.json | 5 - package-lock.json | 10 +- package.json | 2 +- .../controller/units/UnitsTable.vue | 11 +- src/i18n/en.json | 7 +- src/lib/config.ts | 27 +-- src/views/controller/UnitManagerView.vue | 16 -- src/views/controller/UnitTerminalView.vue | 182 +++++++++++------- vite.config.ts | 64 ++++-- vitest.config.ts | 20 +- 10 files changed, 194 insertions(+), 150 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index bb95dbe29..846c4495e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -779,11 +779,6 @@ "count": 2 } }, - "src/views/controller/UnitTerminalView.vue": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "src/views/standalone/firewall/PortForward.vue": { "@typescript-eslint/no-explicit-any": { "count": 4 diff --git a/package-lock.json b/package-lock.json index 802089fc5..1008e0885 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@vueuse/core": "^14.0.0", "@vueuse/integrations": "^14.0.0", "@vueuse/router": "^14.2.1", - "@xterm/addon-attach": "^0.11.0", + "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.4.0", "axios": "^1.11.0", "chart.js": "^4.4.0", @@ -4663,10 +4663,10 @@ "vue": "^3.5.0" } }, - "node_modules/@xterm/addon-attach": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-attach/-/addon-attach-0.11.0.tgz", - "integrity": "sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==", + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" diff --git a/package.json b/package.json index 82fd80ed1..7f2161b09 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@vueuse/core": "^14.0.0", "@vueuse/integrations": "^14.0.0", "@vueuse/router": "^14.2.1", - "@xterm/addon-attach": "^0.11.0", + "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.4.0", "axios": "^1.11.0", "chart.js": "^4.4.0", diff --git a/src/components/controller/units/UnitsTable.vue b/src/components/controller/units/UnitsTable.vue index b808b05be..4954a392d 100644 --- a/src/components/controller/units/UnitsTable.vue +++ b/src/components/controller/units/UnitsTable.vue @@ -67,7 +67,6 @@ library.add(faCalendarXmark) const emit = defineEmits<{ reloadData: [] - openSshModal: [unit: Unit] scheduleUpdate: [unit: Unit] abortUpdate: [unit: Unit] upgradeUnitPackages: [unit: Unit] @@ -151,6 +150,14 @@ async function openUnit(unit: Unit, versionCheck = true) { } } +function openTerminal(unit: Unit) { + const routeData = router.resolve({ + name: 'ControllerUnitTerminal', + query: { unitId: unit.id, unitName: unit.info?.unit_name || unit.id } + }) + window.open(routeData.href, '_blank') +} + function getKebabMenuItems(unit: Unit) { const menuItems: Array = [] if (unit.registered) { @@ -172,7 +179,7 @@ function getKebabMenuItems(unit: Unit) { id: 'openSsh', label: t('controller.units.open_ssh_terminal'), icon: faTerminal, - action: () => emit('openSshModal', unit), + action: () => openTerminal(unit), disabled: !unit.connected } ) diff --git a/src/i18n/en.json b/src/i18n/en.json index 4dd00c8ab..a26331c42 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -3242,7 +3242,12 @@ "connection_status_closed": "Closed", "connection_closed": "Connection closed", "connection_closed_description": "Connection to the unit terminal was closed. Please close this window and connect again, if needed.", - "exit_terminal_description": "To exit the unit terminal, simply close this window." + "exit_terminal_description": "To exit the unit terminal, simply close this window.", + "connect": "Connect", + "wrong_passphrase": "Wrong passphrase", + "cannot_open_terminal": "Cannot open terminal", + "connection_failed": "Connection to the unit terminal failed.", + "ssh_key_hint": "Generate an SSH key pair in Account settings and send the public key to the unit." }, "account_settings": { "title": "Account settings", diff --git a/src/lib/config.ts b/src/lib/config.ts index f93277303..b6fe27a63 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -31,21 +31,19 @@ export const getStandaloneApiEndpoint = () => { } /** - * API endpoint of the controller itself + * API endpoint of the controller itself, proxied by the dev server in development */ export const getControllerApiEndpoint = () => { - if (import.meta.env.DEV) { - // controller development environment - - const apiScheme = import.meta.env.VITE_API_SCHEME - const controllerApiHost = import.meta.env.VITE_CONTROLLER_API_HOST - return `${apiScheme}://${controllerApiHost}/api` - } - - // controller production environment return `${window.location.origin}/api` } +/** + * Websocket URL of the SSH terminal of a unit + */ +export const getControllerUnitTerminalUrl = (unitId: string) => { + return `${getControllerApiEndpoint().replace(/^http/, 'ws')}/units/${unitId}/terminal` +} + /** * API endpoint of a specific unit, through the controller's proxy */ @@ -53,15 +51,6 @@ export const getUnitManagementApiEndpoint = (unitId?: string) => { const unitsStore = useUnitsStore() // in case of "Open unit" unitsStore.unitId is used const currentUnitId = unitId || unitsStore.unitId - - if (import.meta.env.DEV) { - // controller development environment - - const apiScheme = import.meta.env.VITE_API_SCHEME - const controllerApiHost = import.meta.env.VITE_CONTROLLER_API_HOST - return `${apiScheme}://${controllerApiHost}/${currentUnitId}/api` - } - return `${window.location.origin}/${currentUnitId}/api` } diff --git a/src/views/controller/UnitManagerView.vue b/src/views/controller/UnitManagerView.vue index 5b059494c..a252b7aed 100644 --- a/src/views/controller/UnitManagerView.vue +++ b/src/views/controller/UnitManagerView.vue @@ -21,7 +21,6 @@ import { import { useI18n } from 'vue-i18n' import UnitsTable from '@/components/controller/units/UnitsTable.vue' import AddUnitModal from '@/components/controller/units/AddUnitModal.vue' -import OpenSshModal from '@/components/controller/units/OpenSshModal.vue' import SendOrRevokeSshKeyDrawer from '@/components/controller/units/SendOrRevokeSshKeyDrawer.vue' import { useDefaultsStore } from '@/stores/controller/defaults' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' @@ -44,9 +43,7 @@ const textFilter = ref('') const isShownAddUnitModal = ref(false) const getUnitsIntervalId = ref(0) const isUnitsSkeletonEnabled = ref(true) -const isShownOpenSshModal = ref(false) const isShownSendOrRevokeSshKeyDrawer = ref(false) -const currentUnit = ref() const sendOrRevokeAction = ref('send') const unitToUpdate = ref() const unitToAbortUpdate = ref() @@ -125,11 +122,6 @@ async function loadData() { isUnitsSkeletonEnabled.value = false } -function showOpenSshModal(unit: Unit) { - currentUnit.value = unit - isShownOpenSshModal.value = true -} - function showSendSshKeyDrawer() { sendOrRevokeAction.value = 'send' isShownSendOrRevokeSshKeyDrawer.value = true @@ -317,7 +309,6 @@ function getBulkActionsKebabMenuItems() { v-else :filtered-units="filteredUnits" @reload-data="loadData" - @open-ssh-modal="showOpenSshModal" @schedule-update="unitToUpdate = $event" @abort-update="unitToAbortUpdate = $event" @upgrade-unit-packages="unitToUpgradePackages = $event" @@ -332,13 +323,6 @@ function getBulkActionsKebabMenuItems() { @reload-data="loadData" /> - - - - - diff --git a/src/i18n/en.json b/src/i18n/en.json index a26331c42..972c0e97f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -362,16 +362,13 @@ "cannot_add_unit": "Cannot add unit", "max_units_reached": "Maximum number of units reached", "cannot_upload_backup": "Cannot upload backup", - "cannot_retrieve_xsrf_token": "Cannot retrieve XSRF token", "cannot_retrieve_ssh_keys": "Cannot retrieve account SSH keys", - "cannot_retrieve_websocket_id": "Cannot retrieve websocket ID", "cannot_retrieve_defaults": "Cannot retrieve defaults", "cannot_change_password": "Cannot change password", "cannot_delete_ssh_key": "Cannot delete SSH key", "cannot_generate_ssh_key_pair": "Cannot generate SSH key pair", "cannot_retrieve_unit_ssh_port": "Cannot retrieve unit SSH port", "cannot_open_unit": "Cannot open unit", - "cannot_retrieve_unit_ssh_keys": "Cannot retrieve unit SSH keys", "cannot_remove_unit": "Cannot remove unit", "cannot_scan_network": "Cannot scan network", "cannot_retrieve_vpn_networks": "Cannot retrieve VPN networks", @@ -3125,20 +3122,12 @@ "open_metrics": "Open metrics", "open_logs": "Open logs", "open_ssh_terminal": "Open SSH terminal", - "open_terminal": "Open terminal", "remove_unit": "Remove unit", "copy_join_code": "Copy join code", "add_unit_step_1": "Copy the join code of the unit", "add_unit_step_2": "Access {product} UI on the unit you want to add and go to 'System > Controller' page", "add_unit_step_3": "Paste Controller join code in the form and click 'Connect the unit'", "add_unit_step_4": "The unit will appear in 'Unit manager' page", - "unit_username": "Unit username", - "unit_password": "Unit password", - "popup_permission_title": "Your browser might need the permission to open popups", - "popup_permission_description": "To open the SSH terminal, you might need to allow popups for this site.", - "auth_error": "Authentication error", - "auth_error_credentials": "Incorrect username or password", - "auth_error_passphrase": "Incorrect username or passphrase", "open_unit_tooltip": "To open a unit, you might need to allow popups for this site.", "open_actions_menu": "Open actions menu", "actions": "Actions", diff --git a/src/lib/controller/webssh.ts b/src/lib/controller/webssh.ts deleted file mode 100644 index f44bd8f3e..000000000 --- a/src/lib/controller/webssh.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2024 Nethesis S.r.l. -// SPDX-License-Identifier: GPL-3.0-or-later - -import axios from 'axios' -import { useDefaultsStore } from '@/stores/controller/defaults' - -export interface SshConnectionPayload { - hostname: string - port: number - username: string - password: string - passphrase: string - privatekey: string - totp: string - term: string - _xsrf: string -} - -export async function getXsrfToken() { - const defaultsStore = useDefaultsStore() - - const res = await axios.get(`https://${defaultsStore.fqdn}${defaultsStore.websshPath}`, { - withCredentials: true - }) - - // retrieve the XSRF token from the response body, it is a hidden input field, e.g.: - // - const parser = new DOMParser() - const htmlDoc = parser.parseFromString(res.data, 'text/html') - const xsrfToken = (htmlDoc.getElementsByName('_xsrf')[0] as HTMLInputElement)?.value - return xsrfToken -} - -export async function getWebsocketId(payload: SshConnectionPayload) { - const defaultsStore = useDefaultsStore() - - const formData = new FormData() - formData.append('hostname', payload.hostname) - formData.append('port', payload.port.toString()) - formData.append('username', payload.username) - formData.append('password', payload.password) - formData.append('passphrase', payload.passphrase) - formData.append('privatekey', payload.privatekey) - formData.append('totp', payload.totp) - formData.append('term', payload.term) - formData.append('_xsrf', payload._xsrf) - - const res = await axios.post( - `https://${defaultsStore.fqdn}${defaultsStore.websshPath}`, - formData, - { - headers: { - 'Content-Type': 'multipart/form-data' - }, - withCredentials: true - } - ) - - // If API call fails returns the reason inside status field - if (res.data.status) { - // let's manually raise a 401 error - throw { response: { status: 401 } } - } - return res.data.id -} diff --git a/src/stores/controller/defaults.ts b/src/stores/controller/defaults.ts index d3462afa5..6cc4de332 100644 --- a/src/stores/controller/defaults.ts +++ b/src/stores/controller/defaults.ts @@ -11,8 +11,6 @@ export const useDefaultsStore = defineStore('defaults', () => { const controllerLoginStore = useControllerLoginStore() const fqdn = ref('') const grafanaPath = ref('') - const prometheusPath = ref('') - const websshPath = ref('') const validSubscription = ref(false) const defaultsLoaded = ref(false) @@ -26,8 +24,6 @@ export const useDefaultsStore = defineStore('defaults', () => { if (res.data.data) { fqdn.value = res.data.data.fqdn grafanaPath.value = res.data.data.grafana_path - prometheusPath.value = res.data.data.prometheus_path - websshPath.value = res.data.data.webssh_path validSubscription.value = res.data.data.valid_subscription defaultsLoaded.value = true } @@ -36,8 +32,6 @@ export const useDefaultsStore = defineStore('defaults', () => { return { fqdn, grafanaPath, - prometheusPath, - websshPath, validSubscription, defaultsLoaded, getDefaults From 70445624079e557d826f31e8678f98dc84843a94 Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Tue, 13 Oct 2026 15:15:33 +0200 Subject: [PATCH 3/4] fix(deps): update xterm to v6 --- package-lock.json | 26 +++++++++++++------------- package.json | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1008e0885..11c2c0c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,8 +29,8 @@ "@vueuse/core": "^14.0.0", "@vueuse/integrations": "^14.0.0", "@vueuse/router": "^14.2.1", - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.4.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "axios": "^1.11.0", "chart.js": "^4.4.0", "chartjs-adapter-date-fns": "^3.0.0", @@ -4664,19 +4664,19 @@ } }, "node_modules/@xterm/addon-fit": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", - "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - } + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] }, "node_modules/acorn": { "version": "8.18.0", diff --git a/package.json b/package.json index 7f2161b09..1b6215670 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "@vueuse/core": "^14.0.0", "@vueuse/integrations": "^14.0.0", "@vueuse/router": "^14.2.1", - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.4.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "axios": "^1.11.0", "chart.js": "^4.4.0", "chartjs-adapter-date-fns": "^3.0.0", From 696d648acb674584bc5e496e7ff59642ea0f782e Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Tue, 13 Oct 2026 15:28:40 +0200 Subject: [PATCH 4/4] adjusted interface flickering --- .../controller/units/UnitsTable.vue | 2 +- src/i18n/en.json | 3 + src/views/controller/UnitTerminalView.vue | 239 ++++++++++++------ 3 files changed, 171 insertions(+), 73 deletions(-) diff --git a/src/components/controller/units/UnitsTable.vue b/src/components/controller/units/UnitsTable.vue index 4954a392d..5e56754b1 100644 --- a/src/components/controller/units/UnitsTable.vue +++ b/src/components/controller/units/UnitsTable.vue @@ -153,7 +153,7 @@ async function openUnit(unit: Unit, versionCheck = true) { function openTerminal(unit: Unit) { const routeData = router.resolve({ name: 'ControllerUnitTerminal', - query: { unitId: unit.id, unitName: unit.info?.unit_name || unit.id } + query: { unitId: unit.id } }) window.open(routeData.href, '_blank') } diff --git a/src/i18n/en.json b/src/i18n/en.json index 972c0e97f..df526c7c3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -362,6 +362,7 @@ "cannot_add_unit": "Cannot add unit", "max_units_reached": "Maximum number of units reached", "cannot_upload_backup": "Cannot upload backup", + "cannot_retrieve_unit": "Cannot retrieve unit", "cannot_retrieve_ssh_keys": "Cannot retrieve account SSH keys", "cannot_retrieve_defaults": "Cannot retrieve defaults", "cannot_change_password": "Cannot change password", @@ -3236,6 +3237,8 @@ "wrong_passphrase": "Wrong passphrase", "cannot_open_terminal": "Cannot open terminal", "connection_failed": "Connection to the unit terminal failed.", + "unit_not_connected": "The unit is not connected to the controller.", + "ssh_disabled": "SSH is disabled on the unit.", "ssh_key_hint": "Generate an SSH key pair in Account settings and send the public key to the unit." }, "account_settings": { diff --git a/src/views/controller/UnitTerminalView.vue b/src/views/controller/UnitTerminalView.vue index 589b13290..fb7b4ab9e 100644 --- a/src/views/controller/UnitTerminalView.vue +++ b/src/views/controller/UnitTerminalView.vue @@ -4,9 +4,11 @@ -->