mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
release-2.5.0 (#994)
* 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: fix ci checks (#966) * Fix dashboard service link creation (#950) * Fix jump host SOCKS5 proxy selection (#951) * Fix jump host SOCKS5 proxy selection * fix: type jump host socks proxy config * Fix tmux detection path handling (#949) * Support GUACD_URL environment config (#952) * Retry autostart tunnel host fetches (#953) * Retry autostart tunnel host fetches * chore: format tunnel route * Fix PUID html ownership in Docker entrypoint (#954) * feat: allow custom tunnel endpoints (#977) * fix: skip metrics start for non-ssh hosts (#976) * Fix Proxmox import auth fallback (#956) * Fix Proxmox import auth fallback * chore: format proxmox import auth * Fix SSH heading syntax highlighting (#955) * Fix SSH heading syntax highlighting * chore: format terminal highlighter * Initialize auth before fullscreen terminal routes (#957) * Add WebAuthn passkey authentication (#959) * chore: add Biome tooling (#965) * chore: add biome tooling * chore: support tailwind syntax in biome * Fix VNC required argument handshake (#968) * Prioritize host results in command palette search (#969) * Prevent sidebar host hover layout shift (#970) * Add terminal font zoom with mouse wheel (#971) * Add host temperature metrics card (#972) * Make file downloads reliable in desktop app (#973) * Add app rail hover expansion setting (#974) * fix: use correct translation key for nav.close (#964) * fix(tunnel): skip endpoint credential validation for direct tunnels (#963) * fix: SSH port connection bug (#975) * fix: chunked upload for files >=1.5GB to bypass browser ArrayBuffer limit (#948) * feat: support SSH agent auth across SSH features (#960) * feat: add Podman container runtime support (#958) * chore: update readme and release notes * fix: stabilize Windows app icon (#978) * Add safe host sharing export (#979) * Add external editor support for file manager (#985) * Rework SSH credential password handling (#984) * Support password fallback for SSH key credentials * Complete SSH credential password fallback * Fix runtime base path for auth callbacks (#982) * Fix TUI terminal output highlighting (#983) Co-authored-by: LukeGus <bugattiguy527@gmail.com> * Add app fullscreen mode (#993) * Fix host metrics startup polling (#986) * chore: update release notes * fix: line chart text overlap * chore: update RELEASE_NOTES.md * chore: add donation goal to readme * chore: update readme * fix: hide full-screen button in electron app * fix: failed unit tests * chore: lint, format, and bump version to 2.5.0 * chore: remove timeout from crowdin translate * chore: sync Crowdin translations for 2.5.0 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: DivByZero <mr.oplus@yahoo.fr> 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> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
This commit is contained in:
co-authored by
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
sash
LukeGus
DivByZero
devdanetra
Aleksandr Fominykh
ZacharyZcR
parent
fd27a366d0
commit
9de904dd4c
+290
-36
@@ -33,9 +33,11 @@ import {
|
||||
connectTunnel,
|
||||
disconnectTunnel,
|
||||
getUserInfo,
|
||||
getVaultProfiles,
|
||||
getHostPassword,
|
||||
} from "@/main-axios";
|
||||
import { getTailscaleDevices, getHostDefaults } from "@/api/settings-api";
|
||||
import type { Host } from "@/types/ui-types";
|
||||
import type { Host, VaultProfile } from "@/types/ui-types";
|
||||
import type { SSHHost, TunnelStatus } from "@/types";
|
||||
import { useTabsSafe } from "@/shell/TabContext";
|
||||
import {
|
||||
@@ -61,6 +63,8 @@ import {
|
||||
HostEditorVncTab,
|
||||
} from "./HostEditorGuacamoleTabs";
|
||||
import { HostStatsTab } from "./HostEditorStatsTab";
|
||||
import { VaultProfileManager } from "./VaultProfileManager";
|
||||
import { findHostByTunnelEndpoint } from "@/features/tunnel/tunnel-endpoints";
|
||||
|
||||
export function HostEditor({
|
||||
host,
|
||||
@@ -112,6 +116,14 @@ export function HostEditor({
|
||||
const [tailscaleLoading, setTailscaleLoading] = useState(false);
|
||||
const [connectingTunnel, setConnectingTunnel] = useState<number | null>(null);
|
||||
const [isOidcUser, setIsOidcUser] = useState(false);
|
||||
const [vaultProfiles, setVaultProfiles] = useState<VaultProfile[]>([]);
|
||||
const [showVaultManager, setShowVaultManager] = useState(false);
|
||||
|
||||
const reloadVaultProfiles = () => {
|
||||
getVaultProfiles()
|
||||
.then((res) => setVaultProfiles(res as unknown as VaultProfile[]))
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getUserInfo()
|
||||
@@ -123,6 +135,7 @@ export function HostEditor({
|
||||
getSnippets()
|
||||
.then((res) => setSnippets(mapSnippetResponse(res)))
|
||||
.catch(() => {});
|
||||
reloadVaultProfiles();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -132,6 +145,22 @@ export function HostEditor({
|
||||
.catch(() => {});
|
||||
}, [host]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!host?.id || form.vncAuthType !== "direct" || form.vncPassword) return;
|
||||
|
||||
let cancelled = false;
|
||||
getHostPassword(Number(host.id), "vncPassword").then((password) => {
|
||||
if (cancelled || !password) return;
|
||||
setForm((prev) =>
|
||||
prev.vncPassword ? prev : { ...prev, vncPassword: password },
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [form.vncAuthType, form.vncPassword, host?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== "tunnels") return;
|
||||
const unsub = subscribeTunnelStatuses((s) => setTunnelStatuses(s));
|
||||
@@ -252,9 +281,11 @@ export function HostEditor({
|
||||
"password",
|
||||
"key",
|
||||
"credential",
|
||||
"vault",
|
||||
"none",
|
||||
"opkssh",
|
||||
"tailscale",
|
||||
"agent",
|
||||
].map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
@@ -363,7 +394,7 @@ export function HostEditor({
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".pem,.key,.txt"
|
||||
accept=".pem,.key,.ppk,.txt"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -413,6 +444,17 @@ export function HostEditor({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.password")} ({t("common.optional")})
|
||||
</label>
|
||||
<PasswordInput
|
||||
className="h-8 text-xs pr-8"
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={(e) => setField("password", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.keyTypeLabel")}
|
||||
@@ -441,6 +483,58 @@ export function HostEditor({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{authMethod === "vault" && (
|
||||
<div className="flex flex-col gap-1.5 col-span-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.vaultProfile")}
|
||||
</label>
|
||||
<select
|
||||
value={form.vaultProfileId}
|
||||
onChange={(e) =>
|
||||
setField("vaultProfileId", e.target.value)
|
||||
}
|
||||
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="">
|
||||
{t("hosts.selectAVaultProfile")}
|
||||
</option>
|
||||
{vaultProfiles.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.shared ? `${p.name} (shared)` : p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{t("hosts.vaultProfileHint")}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<a
|
||||
href="https://docs.termix.site/features/authentication/vault"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[10px] text-accent-brand hover:underline"
|
||||
>
|
||||
{t("hosts.docsLink")}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="text-[10px] text-accent-brand hover:text-accent-brand/80"
|
||||
onClick={() => setShowVaultManager((v) => !v)}
|
||||
>
|
||||
{t("hosts.vaultManageProfiles")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{authMethod === "vault" && showVaultManager && (
|
||||
<VaultProfileManager
|
||||
profiles={vaultProfiles}
|
||||
onChanged={reloadVaultProfiles}
|
||||
onClose={() => setShowVaultManager(false)}
|
||||
/>
|
||||
)}
|
||||
{authMethod === "credential" && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
@@ -604,6 +698,31 @@ export function HostEditor({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{authMethod === "agent" && (
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.agentLabel")}
|
||||
</span>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{t("hosts.agentDesc")}
|
||||
</p>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.agentSocketPathLabel")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder={t("hosts.agentSocketPathPlaceholder")}
|
||||
value={form.agentSocketPath}
|
||||
onChange={(e) =>
|
||||
setField("agentSocketPath", e.target.value)
|
||||
}
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{t("hosts.agentSocketPathHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SettingRow
|
||||
label={t("hosts.forceKeyboardInteractiveLabel")}
|
||||
description={t("hosts.forceKeyboardInteractiveShortDesc")}
|
||||
@@ -625,28 +744,6 @@ export function HostEditor({
|
||||
onChange={(v) => setField("allowLegacyAlgorithms", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.sudoPasswordAutoFillLabel")}
|
||||
description={t("hosts.sudoPasswordAutoFillDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.sudoPasswordAutoFill}
|
||||
onChange={(v) => setField("sudoPasswordAutoFill", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
{form.sudoPasswordAutoFill && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.sudoPasswordLabel")}
|
||||
</label>
|
||||
<PasswordInput
|
||||
className="h-8 text-xs pr-8"
|
||||
placeholder="••••••••"
|
||||
value={form.sudoPassword}
|
||||
onChange={(e) => setField("sudoPassword", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
</>
|
||||
@@ -671,6 +768,7 @@ export function HostEditor({
|
||||
cursorBlink={form.cursorBlink}
|
||||
letterSpacing={form.letterSpacing}
|
||||
lineHeight={form.lineHeight}
|
||||
customThemeColors={form.customThemeColors ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -681,8 +779,14 @@ export function HostEditor({
|
||||
<select
|
||||
value={form.theme}
|
||||
onChange={(e) => {
|
||||
setField("theme", e.target.value);
|
||||
setPreviewTerminalTheme(e.target.value);
|
||||
const newTheme = e.target.value;
|
||||
setField("theme", newTheme);
|
||||
setPreviewTerminalTheme(newTheme);
|
||||
if (newTheme === "custom" && !form.customThemeColors) {
|
||||
setField("customThemeColors", {
|
||||
...TERMINAL_THEMES.termixDark.colors,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
@@ -823,6 +927,121 @@ export function HostEditor({
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{form.theme === "custom" && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.customThemeColors")}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
title={t("hosts.customThemeResetTooltip")}
|
||||
onClick={() =>
|
||||
setField("customThemeColors", {
|
||||
...TERMINAL_THEMES.termixDark.colors,
|
||||
})
|
||||
}
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{t("hosts.customThemeResetTooltip")}
|
||||
</button>
|
||||
</div>
|
||||
{(
|
||||
[
|
||||
["background", "customThemeBackground"],
|
||||
["foreground", "customThemeForeground"],
|
||||
["cursor", "customThemeCursor"],
|
||||
["cursorAccent", "customThemeCursorAccent"],
|
||||
["selectionBackground", "customThemeSelection"],
|
||||
] as const
|
||||
).map(([key, labelKey]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
>
|
||||
<label className="text-xs text-muted-foreground min-w-0 flex-1">
|
||||
{t(`hosts.${labelKey}`)}
|
||||
</label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="color"
|
||||
value={
|
||||
form.customThemeColors?.[key] ??
|
||||
TERMINAL_THEMES.termixDark.colors[key]
|
||||
}
|
||||
onChange={(e) =>
|
||||
setField("customThemeColors", {
|
||||
...(form.customThemeColors ??
|
||||
TERMINAL_THEMES.termixDark.colors),
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-7 w-10 cursor-pointer border border-border bg-background p-0.5"
|
||||
/>
|
||||
<span className="text-[10px] font-mono text-muted-foreground w-16 tabular-nums">
|
||||
{form.customThemeColors?.[key] ??
|
||||
TERMINAL_THEMES.termixDark.colors[key]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mt-1">
|
||||
{t("hosts.customThemeAnsiColors")}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
{(
|
||||
[
|
||||
["black", "customThemeBlack"],
|
||||
["brightBlack", "customThemeBrightBlack"],
|
||||
["red", "customThemeRed"],
|
||||
["brightRed", "customThemeBrightRed"],
|
||||
["green", "customThemeGreen"],
|
||||
["brightGreen", "customThemeBrightGreen"],
|
||||
["yellow", "customThemeYellow"],
|
||||
["brightYellow", "customThemeBrightYellow"],
|
||||
["blue", "customThemeBlue"],
|
||||
["brightBlue", "customThemeBrightBlue"],
|
||||
["magenta", "customThemeMagenta"],
|
||||
["brightMagenta", "customThemeBrightMagenta"],
|
||||
["cyan", "customThemeCyan"],
|
||||
["brightCyan", "customThemeBrightCyan"],
|
||||
["white", "customThemeWhite"],
|
||||
["brightWhite", "customThemeBrightWhite"],
|
||||
] as const
|
||||
).map(([key, labelKey]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
>
|
||||
<label className="text-xs text-muted-foreground min-w-0 flex-1 truncate">
|
||||
{t(`hosts.${labelKey}`)}
|
||||
</label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="color"
|
||||
value={
|
||||
form.customThemeColors?.[key] ??
|
||||
TERMINAL_THEMES.termixDark.colors[key]
|
||||
}
|
||||
onChange={(e) =>
|
||||
setField("customThemeColors", {
|
||||
...(form.customThemeColors ??
|
||||
TERMINAL_THEMES.termixDark.colors),
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-7 w-10 cursor-pointer border border-border bg-background p-0.5"
|
||||
/>
|
||||
<span className="text-[10px] font-mono text-muted-foreground w-16 tabular-nums">
|
||||
{form.customThemeColors?.[key] ??
|
||||
TERMINAL_THEMES.termixDark.colors[key]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SettingRow
|
||||
label={t("hosts.cursorBlinking")}
|
||||
description={t("hosts.cursorBlinkingDesc")}
|
||||
@@ -1364,7 +1583,20 @@ export function HostEditor({
|
||||
</p>
|
||||
)}
|
||||
{form.serverTunnels.map((tun, i) => {
|
||||
const tunnelName = `${host?.id ?? "new"}-${i}-${tun.sourcePort}`;
|
||||
const endpointValue = (tun.endpointHost ?? "").trim();
|
||||
const selectedEndpointHost = findHostByTunnelEndpoint(
|
||||
hosts,
|
||||
endpointValue,
|
||||
);
|
||||
const directEndpoint =
|
||||
!endpointValue ||
|
||||
endpointValue === "127.0.0.1" ||
|
||||
endpointValue === "localhost";
|
||||
const endpointInputId = `server-tunnel-endpoint-${host?.id ?? "new"}-${i}`;
|
||||
const hostLabel =
|
||||
host?.name ||
|
||||
(host ? `${host.username}@${host.ip}` : "new");
|
||||
const tunnelName = `${host?.id ?? "new"}::${i}::${hostLabel}::${tun.sourcePort}::${endpointValue}::${tun.endpointPort}`;
|
||||
const tunnelStatus = tunnelStatuses[tunnelName]?.status as
|
||||
| string
|
||||
| undefined;
|
||||
@@ -1422,12 +1654,32 @@ export function HostEditor({
|
||||
sourceCredentialId: form.credentialId
|
||||
? Number(form.credentialId)
|
||||
: undefined,
|
||||
endpointIP: host.ip,
|
||||
endpointIP: directEndpoint
|
||||
? host.ip
|
||||
: (selectedEndpointHost?.ip ??
|
||||
endpointValue),
|
||||
endpointSSHPort:
|
||||
host.sshPort ?? host.port,
|
||||
endpointHost: tun.endpointHost ?? "",
|
||||
endpointUsername: form.username,
|
||||
endpointAuthMethod: form.authType,
|
||||
directEndpoint || selectedEndpointHost
|
||||
? directEndpoint
|
||||
? (host.sshPort ?? host.port)
|
||||
: (selectedEndpointHost?.sshPort ??
|
||||
selectedEndpointHost?.port ??
|
||||
22)
|
||||
: 22,
|
||||
endpointHost: endpointValue,
|
||||
endpointUsername: directEndpoint
|
||||
? form.username
|
||||
: (selectedEndpointHost?.username ??
|
||||
""),
|
||||
endpointAuthMethod:
|
||||
selectedEndpointHost?.authType ??
|
||||
"none",
|
||||
endpointCredentialId:
|
||||
selectedEndpointHost?.credentialId
|
||||
? Number(
|
||||
selectedEndpointHost.credentialId,
|
||||
)
|
||||
: undefined,
|
||||
sourcePort: tun.sourcePort,
|
||||
endpointPort: tun.endpointPort ?? 0,
|
||||
bindHost: tun.bindHost ?? "127.0.0.1",
|
||||
@@ -1506,8 +1758,10 @@ export function HostEditor({
|
||||
<label className="text-[10px] font-bold text-muted-foreground">
|
||||
{t("hosts.endpointHost")}
|
||||
</label>
|
||||
<select
|
||||
<Input
|
||||
className="h-7 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||
list={endpointInputId}
|
||||
placeholder={t("hosts.endpointHostPlaceholder")}
|
||||
value={tun.endpointHost ?? ""}
|
||||
onChange={(e) => {
|
||||
const updated = [...form.serverTunnels];
|
||||
@@ -1517,8 +1771,8 @@ export function HostEditor({
|
||||
};
|
||||
setField("serverTunnels", updated);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("hosts.sameHost")}</option>
|
||||
/>
|
||||
<datalist id={endpointInputId}>
|
||||
{hosts
|
||||
.filter((h) => h.enableSsh)
|
||||
.map((h) => (
|
||||
@@ -1526,7 +1780,7 @@ export function HostEditor({
|
||||
{h.name || h.ip} ({h.ip})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</datalist>
|
||||
</div>
|
||||
)}
|
||||
{tun.mode !== "dynamic" && (
|
||||
|
||||
Reference in New Issue
Block a user