import { useState, useRef, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { copyToClipboard } from "@/lib/clipboard"; import { getUserInfo, getApiKeys, createApiKey, deleteApiKey, changePassword, deleteAccount, logoutUser, setupTOTP, enableTOTP, disableTOTP, getVersionInfo, getUserRoles, saveUserPreferences, getUserPreferences, } from "@/main-axios"; import { deleteWebAuthnCredential, listWebAuthnCredentials, registerWebAuthnCredential, type WebAuthnCredentialSummary, type WebAuthnUserVerification, } from "@/api/webauthn-api"; import type { UserRole } from "@/main-axios"; import type React from "react"; import { isElectron } from "@/lib/electron"; import { C2STunnelPresetManager } from "@/user/C2STunnelPresetManager"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/dialog"; import { Activity, AlertCircle, CheckCircle2, ChevronDown, Clock, Copy, Eye, EyeOff, Hammer, KeyRound, LayoutPanelLeft, Network, Palette, Play, Plug, Plus, RotateCcw, ScrollText, Server, Shield, ShieldCheck, Trash2, Type, Usb, User, X, Zap, } from "lucide-react"; import { SettingRow, FakeSwitch } from "@/components/section-card"; import { ACCENT_PRESET_COLORS, applyAccentColor, applyFontSize, FONT_SIZES, } from "@/lib/theme"; import type { ApiKey } from "@/main-axios"; import { useTheme } from "@/components/theme-provider"; import type { FontSizeId, ThemeId } from "@/types/ui-types"; import { toast } from "sonner"; import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n"; type UserProfileSection = | "account" | "appearance" | "security" | "api-keys" | "c2s-tunnels"; const THEMES: { id: ThemeId; preview: string }[] = [ { id: "system", preview: "auto" }, { id: "light", preview: "#ffffff" }, { id: "dark", preview: "#1a1c22" }, { id: "dracula", preview: "#282a36" }, { id: "catppuccin", preview: "#1e1e2e" }, { id: "nord", preview: "#2e3440" }, { id: "solarized", preview: "#002b36" }, { id: "tokyo-night", preview: "#1a1b26" }, { id: "one-dark", preview: "#282c34" }, { id: "gruvbox", preview: "#282828" }, ]; const LANGUAGES = [ { code: "en", label: "English" }, { code: "af", label: "Afrikaans" }, { code: "ar", label: "العربية" }, { code: "bn", label: "বাংলা" }, { code: "bg", label: "Български" }, { code: "ca", label: "Català" }, { code: "zh-CN", label: "中文 (简体)" }, { code: "zh-TW", label: "中文 (繁體)" }, { code: "cs", label: "Čeština" }, { code: "da", label: "Dansk" }, { code: "nl", label: "Nederlands" }, { code: "fi", label: "Suomi" }, { code: "fr", label: "Français" }, { code: "de", label: "Deutsch" }, { code: "el", label: "Ελληνικά" }, { code: "he", label: "עברית" }, { code: "hi", label: "हिन्दी" }, { code: "hu", label: "Magyar" }, { code: "id", label: "Indonesia" }, { code: "it", label: "Italiano" }, { code: "ja", label: "日本語" }, { code: "ko", label: "한국어" }, { code: "no", label: "Norsk" }, { code: "pl", label: "Polski" }, { code: "pt-PT", label: "Português (PT)" }, { code: "pt-BR", label: "Português (BR)" }, { code: "ro", label: "Română" }, { code: "ru", label: "Русский" }, { code: "sr", label: "Српски" }, { code: "es-ES", label: "Español" }, { code: "sv-SE", label: "Svenska" }, { code: "th", label: "ไทย" }, { code: "tr", label: "Türkçe" }, { code: "uk", label: "Українська" }, { code: "vi", label: "Tiếng Việt" }, ]; function AccordionSection({ id, label, icon, open, onToggle, children, }: { id: string; label: string; icon: React.ReactNode; open: boolean; onToggle: () => void; children: React.ReactNode; }) { return (
{open && (
{children}
)}
); } type ApiErrorLike = { response?: { data?: { error?: string; }; }; }; function apiErrorMessage(error: unknown, fallback: string) { return (error as ApiErrorLike).response?.data?.error || fallback; } type CreatedProfileApiKey = { id: string; name: string; token?: string; tokenPrefix?: string; createdAt?: string; expiresAt?: string | null; }; function NewApiKeyDialog({ open, onOpenChange, onAdd, userId, }: { open: boolean; onOpenChange: (v: boolean) => void; onAdd: (key: CreatedProfileApiKey) => void; userId: string; }) { const { t } = useTranslation(); const [name, setName] = useState(""); const [expiry, setExpiry] = useState(""); const handleCreate = async () => { if (!name.trim()) { toast.error(t("newUi.sidebar.userProfile.apiKeyNameRequired")); return; } try { const created = await createApiKey( name.trim(), userId, expiry ? new Date(expiry).toISOString() : undefined, ); onAdd(created); onOpenChange(false); setName(""); setExpiry(""); toast.success(t("newUi.sidebar.userProfile.apiKeyCreated", { name })); } catch { toast.error(t("newUi.sidebar.userProfile.apiKeyCreateFailed")); } }; return (
{t("newUi.sidebar.userProfile.createApiKeyTitle")} {t("newUi.sidebar.userProfile.createApiKeyDescription")}
setName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCreate()} className="rounded-none bg-muted/50 border-border text-sm h-9" />
setExpiry(e.target.value)} className="rounded-none bg-muted/50 border-border text-sm h-9" />
); } function PasswordChangeSection({ showPassword, setShowPassword, onLogout, }: { showPassword: boolean; setShowPassword: (v: boolean | ((prev: boolean) => boolean)) => void; onLogout?: () => void; }) { const { t } = useTranslation(); const [currentPw, setCurrentPw] = useState(""); const [newPw, setNewPw] = useState(""); const [confirmPw, setConfirmPw] = useState(""); async function handleUpdate() { if (!currentPw || !newPw) { toast.error(t("newUi.sidebar.userProfile.passwordFieldsRequired")); return; } if (newPw !== confirmPw) { toast.error(t("newUi.sidebar.userProfile.passwordMismatch")); return; } if (newPw.length < 6) { toast.error(t("newUi.sidebar.userProfile.passwordTooShort")); return; } try { await changePassword(currentPw, newPw); toast.success(t("newUi.sidebar.userProfile.passwordUpdated")); setCurrentPw(""); setNewPw(""); setConfirmPw(""); onLogout?.(); } catch (e: unknown) { toast.error( apiErrorMessage(e, t("newUi.sidebar.userProfile.passwordUpdateFailed")), ); } } return (
{t("newUi.sidebar.userProfile.changePassword")}
setCurrentPw(e.target.value)} className="pr-9 text-sm" />
setNewPw(e.target.value)} className="text-sm" />
setConfirmPw(e.target.value)} className="text-sm" />
); } export function UserProfilePanel({ username, onLogout, onChangeServer, userPrefs, onPrefsChange, }: { username?: string; onLogout?: () => void; onChangeServer?: () => void; userPrefs?: { reopenTabsOnLogin: boolean; storageMode?: string | null; commandAutocomplete?: boolean | null; commandPaletteEnabled?: boolean | null; showHostTags?: boolean | null; hostTrayOnClick?: boolean | null; compactHostView?: boolean | null; pinAppRail?: boolean | null; expandAppRailOnHover?: boolean | null; foldersCollapsed?: boolean | null; confirmSnippetExecution?: boolean | null; disableUpdateCheck?: boolean | null; confirmTabClose?: boolean | null; hiddenRailTabs?: string | null; statusColorScheme?: string | null; }; onPrefsChange?: (prefs: { reopenTabsOnLogin?: boolean; storageMode?: "local" | "cloud"; }) => void; }) { const { t } = useTranslation(); const themeLabel: Record = { system: t("newUi.sidebar.userProfile.themeSystem"), light: t("newUi.sidebar.userProfile.themeLight"), dark: t("newUi.sidebar.userProfile.themeDark"), dracula: t("newUi.sidebar.userProfile.themeDracula"), catppuccin: t("newUi.sidebar.userProfile.themeCatppuccin"), nord: t("newUi.sidebar.userProfile.themeNord"), solarized: t("newUi.sidebar.userProfile.themeSolarized"), "tokyo-night": t("newUi.sidebar.userProfile.themeTokyoNight"), "one-dark": t("newUi.sidebar.userProfile.themeOneDark"), gruvbox: t("newUi.sidebar.userProfile.themeGruvbox"), }; const [openSection, setOpenSection] = useState( "account", ); // User info const [userId, setUserId] = useState(""); const [userRole, setUserRole] = useState(""); const [authMethod, setAuthMethod] = useState(""); const [version, setVersion] = useState(""); const [versionStatus, setVersionStatus] = useState< "up_to_date" | "requires_update" | "beta" >("up_to_date"); const [isOidc, setIsOidc] = useState(false); const [isDualAuth, setIsDualAuth] = useState(false); // TOTP const [totpEnabled, setTotpEnabled] = useState(false); const [totpStep, setTotpStep] = useState< "idle" | "setup" | "verify" | "backup" >("idle"); const [totpQrCode, setTotpQrCode] = useState(""); const [totpSecret, setTotpSecret] = useState(""); const [totpCode, setTotpCode] = useState(""); const [totpBackupCodes, setTotpBackupCodes] = useState([]); const [totpLoading, setTotpLoading] = useState(false); const [showDisableTotp, setShowDisableTotp] = useState(false); const [disableTotpInput, setDisableTotpInput] = useState(""); const [passkeys, setPasskeys] = useState([]); const [passkeyLoading, setPasskeyLoading] = useState(false); const [passkeyName, setPasskeyName] = useState(""); const [passkeyUserVerification, setPasskeyUserVerification] = useState("preferred"); // Delete account const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deletePassword, setDeletePassword] = useState(""); const [deleteLoading, setDeleteLoading] = useState(false); // UI state const [showPassword, setShowPassword] = useState(false); const [newKeyOpen, setNewKeyOpen] = useState(false); const colorInputRef = useRef(null); const { theme, setTheme } = useTheme(); const localSnapshot = useRef>({}); // Appearance state — initialized from localStorage const [accentColor, setAccentColor] = useState( () => localStorage.getItem("termix-accent") ?? "#f59145", ); const [customColorInput, setCustomColorInput] = useState( () => localStorage.getItem("termix-accent") ?? "#f59145", ); const [fontSize, setFontSize] = useState( () => (localStorage.getItem("termix-font-size") as FontSizeId) ?? "md", ); const [language, setLanguage] = useState(() => normalizeLanguageCode(localStorage.getItem("i18nextLng")), ); const [storageMode, setStorageMode] = useState<"local" | "cloud">(() => userPrefs?.storageMode === "cloud" ? "cloud" : "local", ); useEffect(() => { if (userPrefs?.storageMode) { setStorageMode(userPrefs.storageMode === "cloud" ? "cloud" : "local"); } }, [userPrefs?.storageMode]); // Settings toggles — all backed by localStorage const [commandAutocomplete, setCommandAutocomplete] = useState( () => localStorage.getItem("commandAutocomplete") === "true", ); const [terminalLinkClickBehavior, setTerminalLinkClickBehavior] = useState( () => localStorage.getItem("terminalLinkClickBehavior") ?? "confirm", ); const [commandPaletteEnabled, setCommandPaletteEnabled] = useState(() => { const v = localStorage.getItem("commandPaletteShortcutEnabled"); return v !== null ? v === "true" : true; }); const [showHostTags, setShowHostTags] = useState(() => { const v = localStorage.getItem("showHostTags"); return v !== null ? v === "true" : true; }); const [hostTrayOnClick, setHostTrayOnClick] = useState( () => localStorage.getItem("hostTrayOnClick") === "true", ); const [compactHostView, setCompactHostView] = useState( () => localStorage.getItem("compactHostView") === "true", ); const [statusColorScheme, setStatusColorScheme] = useState( () => localStorage.getItem("statusColorScheme") ?? "accent", ); const [pinAppRail, setPinAppRail] = useState( () => localStorage.getItem("pinAppRail") === "true", ); const [expandAppRailOnHover, setExpandAppRailOnHover] = useState( () => localStorage.getItem("expandAppRailOnHover") !== "false", ); const [foldersCollapsed, setFoldersCollapsed] = useState( () => localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false", ); const [confirmSnippetExecution, setConfirmSnippetExecution] = useState( () => localStorage.getItem("confirmSnippetExecution") === "true", ); const [disableUpdateCheck, setDisableUpdateCheck] = useState( () => localStorage.getItem("disableUpdateCheck") === "true", ); const [confirmTabClose, setConfirmTabClose] = useState( () => localStorage.getItem("confirmTabClose") === "true", ); const [hiddenRailTabs, setHiddenRailTabs] = useState>(() => { try { const stored = localStorage.getItem("hiddenRailTabs"); return stored ? new Set(JSON.parse(stored)) : new Set(); } catch { return new Set(); } }); // API keys const [apiKeys, setApiKeys] = useState([]); // RBAC roles const [userRoles, setUserRoles] = useState([]); useEffect(() => { getUserInfo() .then((info) => { setUserId(info.userId); setTotpEnabled(info.totp_enabled ?? false); setIsOidc(info.is_oidc ?? false); setIsDualAuth(info.is_dual_auth ?? false); setUserRole( info.is_admin ? t("newUi.sidebar.userProfile.roleAdministrator") : t("newUi.sidebar.userProfile.roleUser"), ); if (info.is_dual_auth) { setAuthMethod(t("newUi.sidebar.userProfile.authMethodDual")); } else if (info.is_oidc) { setAuthMethod(t("newUi.sidebar.userProfile.authMethodOidc")); } else { setAuthMethod(t("newUi.sidebar.userProfile.authMethodLocal")); } getUserRoles(info.userId) .then(({ roles }) => setUserRoles(roles ?? [])) .catch(() => {}); }) .catch(() => {}); getApiKeys() .then(({ apiKeys: keys }) => setApiKeys(keys)) .catch(() => {}); listWebAuthnCredentials() .then(({ credentials }) => setPasskeys(credentials ?? [])) .catch(() => {}); getVersionInfo() .then((info) => { setVersion(info.localVersion); setVersionStatus(info.status ?? "up_to_date"); }) .catch(() => {}); }, [t]); function saveToCloud(prefs: Parameters[0]) { void saveUserPreferences(prefs).catch(() => {}); } async function handleStorageModeChange(mode: "local" | "cloud") { setStorageMode(mode); onPrefsChange?.({ storageMode: mode }); if (mode === "cloud") { // Snapshot current browser localStorage values so any tab can restore them later const SNAPSHOT_KEYS = [ "termix-accent", "termix-font-size", "i18nextLng", "commandAutocomplete", "commandPaletteShortcutEnabled", "showHostTags", "hostTrayOnClick", "compactHostView", "pinAppRail", "expandAppRailOnHover", "defaultSnippetFoldersCollapsed", "confirmSnippetExecution", "disableUpdateCheck", "confirmTabClose", "hiddenRailTabs", "statusColorScheme", ]; const snap: Record = { __theme: theme }; for (const key of SNAPSHOT_KEYS) snap[key] = localStorage.getItem(key); localSnapshot.current = snap; localStorage.setItem("termix-local-snapshot", JSON.stringify(snap)); try { const prefs = await getUserPreferences(); if (prefs.theme) setTheme(prefs.theme as ThemeId); if (prefs.fontSize) { setFontSize(prefs.fontSize as FontSizeId); applyFontSize(prefs.fontSize as FontSizeId); } if (prefs.accentColor) { setAccentColor(prefs.accentColor); setCustomColorInput(prefs.accentColor); localStorage.setItem("termix-accent", prefs.accentColor); applyAccentColor(prefs.accentColor); } if (prefs.language) { const language = await changeAppLanguage(prefs.language); setLanguage(language); } if (prefs.commandAutocomplete != null) { setCommandAutocomplete(prefs.commandAutocomplete); localStorage.setItem( "commandAutocomplete", String(prefs.commandAutocomplete), ); } if (prefs.commandPaletteEnabled != null) { setCommandPaletteEnabled(prefs.commandPaletteEnabled); localStorage.setItem( "commandPaletteShortcutEnabled", String(prefs.commandPaletteEnabled), ); } if (prefs.showHostTags != null) { setShowHostTags(prefs.showHostTags); localStorage.setItem("showHostTags", String(prefs.showHostTags)); window.dispatchEvent(new CustomEvent("showHostTagsChanged")); } if (prefs.hostTrayOnClick != null) { setHostTrayOnClick(prefs.hostTrayOnClick); localStorage.setItem( "hostTrayOnClick", String(prefs.hostTrayOnClick), ); } if (prefs.compactHostView != null) { setCompactHostView(prefs.compactHostView); localStorage.setItem( "compactHostView", String(prefs.compactHostView), ); window.dispatchEvent(new CustomEvent("compactHostViewChanged")); } if (prefs.pinAppRail != null) { setPinAppRail(prefs.pinAppRail); localStorage.setItem("pinAppRail", String(prefs.pinAppRail)); window.dispatchEvent(new Event("pinAppRailChanged")); } if (prefs.expandAppRailOnHover != null) { setExpandAppRailOnHover(prefs.expandAppRailOnHover); localStorage.setItem( "expandAppRailOnHover", String(prefs.expandAppRailOnHover), ); window.dispatchEvent(new Event("expandAppRailOnHoverChanged")); } if (prefs.foldersCollapsed != null) { setFoldersCollapsed(prefs.foldersCollapsed); localStorage.setItem( "defaultSnippetFoldersCollapsed", String(prefs.foldersCollapsed), ); } if (prefs.confirmSnippetExecution != null) { setConfirmSnippetExecution(prefs.confirmSnippetExecution); localStorage.setItem( "confirmSnippetExecution", String(prefs.confirmSnippetExecution), ); } if (prefs.disableUpdateCheck != null) { setDisableUpdateCheck(prefs.disableUpdateCheck); localStorage.setItem( "disableUpdateCheck", String(prefs.disableUpdateCheck), ); } if (prefs.confirmTabClose != null) { setConfirmTabClose(prefs.confirmTabClose); localStorage.setItem( "confirmTabClose", String(prefs.confirmTabClose), ); } if (prefs.hiddenRailTabs != null) { const s = new Set(JSON.parse(prefs.hiddenRailTabs)); setHiddenRailTabs(s); localStorage.setItem("hiddenRailTabs", prefs.hiddenRailTabs); window.dispatchEvent(new CustomEvent("hiddenRailTabsChanged")); } if (prefs.statusColorScheme != null) { setStatusColorScheme(prefs.statusColorScheme); localStorage.setItem("statusColorScheme", prefs.statusColorScheme); window.dispatchEvent(new CustomEvent("statusColorSchemeChanged")); } } catch { // leave UI as-is on error } saveToCloud({ storageMode: "cloud" }); } else { restoreLocalSnapshot(); saveToCloud({ storageMode: "local" }); } } function resetToDefaults() { const DEFAULT_ACCENT = "#f59145"; setTheme("system"); setFontSize("md"); applyFontSize("md"); setAccentColor(DEFAULT_ACCENT); setCustomColorInput(DEFAULT_ACCENT); localStorage.setItem("termix-accent", DEFAULT_ACCENT); applyAccentColor(DEFAULT_ACCENT); setLanguage("en"); void changeAppLanguage("en"); setCommandAutocomplete(false); localStorage.setItem("commandAutocomplete", "false"); setCommandPaletteEnabled(true); localStorage.setItem("commandPaletteShortcutEnabled", "true"); setShowHostTags(true); localStorage.setItem("showHostTags", "true"); window.dispatchEvent(new CustomEvent("showHostTagsChanged")); setHostTrayOnClick(false); localStorage.setItem("hostTrayOnClick", "false"); setCompactHostView(false); localStorage.setItem("compactHostView", "false"); window.dispatchEvent(new CustomEvent("compactHostViewChanged")); setPinAppRail(false); localStorage.setItem("pinAppRail", "false"); window.dispatchEvent(new Event("pinAppRailChanged")); setExpandAppRailOnHover(true); localStorage.setItem("expandAppRailOnHover", "true"); window.dispatchEvent(new Event("expandAppRailOnHoverChanged")); setFoldersCollapsed(true); localStorage.removeItem("defaultSnippetFoldersCollapsed"); setConfirmSnippetExecution(false); localStorage.setItem("confirmSnippetExecution", "false"); setDisableUpdateCheck(false); localStorage.setItem("disableUpdateCheck", "false"); setConfirmTabClose(false); localStorage.setItem("confirmTabClose", "false"); setHiddenRailTabs(new Set()); localStorage.removeItem("hiddenRailTabs"); window.dispatchEvent(new CustomEvent("hiddenRailTabsChanged")); setStatusColorScheme("accent"); localStorage.setItem("statusColorScheme", "accent"); window.dispatchEvent(new CustomEvent("statusColorSchemeChanged")); if (storageMode === "cloud") { saveToCloud({ theme: "system", fontSize: "md", accentColor: DEFAULT_ACCENT, language: "en", commandAutocomplete: false, commandPaletteEnabled: true, showHostTags: true, hostTrayOnClick: false, compactHostView: false, pinAppRail: false, expandAppRailOnHover: true, foldersCollapsed: true, confirmSnippetExecution: false, disableUpdateCheck: false, confirmTabClose: false, hiddenRailTabs: "[]", statusColorScheme: "accent", }); } localSnapshot.current = {}; localStorage.removeItem("termix-local-snapshot"); toast.success(t("newUi.sidebar.userProfile.resetToDefaultsSuccess")); } function restoreLocalSnapshot() { // Prefer the persisted snapshot (survives new tabs/reloads), fall back to in-memory ref. // If neither exists there were never any browser values to restore, so use current localStorage. const persisted = localStorage.getItem("termix-local-snapshot"); const snap: Record = persisted ? (JSON.parse(persisted) as Record) : localSnapshot.current; const hasSnap = Object.keys(snap).length > 0; const restore = (key: string, fallback: string | null = null) => hasSnap ? snap[key] !== undefined ? snap[key] : fallback : (localStorage.getItem(key) ?? fallback); const restoredTheme = ((hasSnap ? snap["__theme"] : theme) as ThemeId) ?? "system"; setTheme(restoredTheme); const restoredFontSize = (restore("termix-font-size", "md") as FontSizeId) ?? "md"; setFontSize(restoredFontSize); applyFontSize(restoredFontSize); const restoredAccent = restore("termix-accent", "#f59145") ?? "#f59145"; setAccentColor(restoredAccent); setCustomColorInput(restoredAccent); localStorage.setItem("termix-accent", restoredAccent); applyAccentColor(restoredAccent); const restoredLang = normalizeLanguageCode(restore("i18nextLng", "en")); setLanguage(restoredLang); void changeAppLanguage(restoredLang); const restoredAutocomplete = restore("commandAutocomplete", "false") === "true"; setCommandAutocomplete(restoredAutocomplete); localStorage.setItem("commandAutocomplete", String(restoredAutocomplete)); const restoredPalette = restore("commandPaletteShortcutEnabled", "true") !== "false"; setCommandPaletteEnabled(restoredPalette); localStorage.setItem( "commandPaletteShortcutEnabled", String(restoredPalette), ); const restoredHostTags = restore("showHostTags", "true") !== "false"; setShowHostTags(restoredHostTags); localStorage.setItem("showHostTags", String(restoredHostTags)); window.dispatchEvent(new CustomEvent("showHostTagsChanged")); const restoredTrayOnClick = restore("hostTrayOnClick", "false") === "true"; setHostTrayOnClick(restoredTrayOnClick); localStorage.setItem("hostTrayOnClick", String(restoredTrayOnClick)); const restoredCompactHostView = restore("compactHostView", "false") === "true"; setCompactHostView(restoredCompactHostView); localStorage.setItem("compactHostView", String(restoredCompactHostView)); window.dispatchEvent(new CustomEvent("compactHostViewChanged")); const restoredPinRail = restore("pinAppRail", "false") === "true"; setPinAppRail(restoredPinRail); localStorage.setItem("pinAppRail", String(restoredPinRail)); window.dispatchEvent(new Event("pinAppRailChanged")); const restoredExpandRailOnHover = restore("expandAppRailOnHover", "true") !== "false"; setExpandAppRailOnHover(restoredExpandRailOnHover); localStorage.setItem( "expandAppRailOnHover", String(restoredExpandRailOnHover), ); window.dispatchEvent(new Event("expandAppRailOnHoverChanged")); const restoredFolders = restore("defaultSnippetFoldersCollapsed", null) !== "false"; setFoldersCollapsed(restoredFolders); const snapFolders = hasSnap ? snap["defaultSnippetFoldersCollapsed"] : localStorage.getItem("defaultSnippetFoldersCollapsed"); if (snapFolders == null) { localStorage.removeItem("defaultSnippetFoldersCollapsed"); } else { localStorage.setItem("defaultSnippetFoldersCollapsed", snapFolders); } const restoredConfirmSnippet = restore("confirmSnippetExecution", "false") === "true"; setConfirmSnippetExecution(restoredConfirmSnippet); localStorage.setItem( "confirmSnippetExecution", String(restoredConfirmSnippet), ); const restoredUpdateCheck = restore("disableUpdateCheck", "false") === "true"; setDisableUpdateCheck(restoredUpdateCheck); localStorage.setItem("disableUpdateCheck", String(restoredUpdateCheck)); const restoredConfirmTab = restore("confirmTabClose", "false") === "true"; setConfirmTabClose(restoredConfirmTab); localStorage.setItem("confirmTabClose", String(restoredConfirmTab)); const restoredHiddenRaw = hasSnap ? snap["hiddenRailTabs"] : localStorage.getItem("hiddenRailTabs"); if (restoredHiddenRaw == null) { setHiddenRailTabs(new Set()); localStorage.removeItem("hiddenRailTabs"); } else { try { setHiddenRailTabs(new Set(JSON.parse(restoredHiddenRaw))); } catch { setHiddenRailTabs(new Set()); } localStorage.setItem("hiddenRailTabs", restoredHiddenRaw); } window.dispatchEvent(new CustomEvent("hiddenRailTabsChanged")); const restoredStatusScheme = restore("statusColorScheme", "accent") ?? "accent"; setStatusColorScheme(restoredStatusScheme); localStorage.setItem("statusColorScheme", restoredStatusScheme); window.dispatchEvent(new CustomEvent("statusColorSchemeChanged")); localStorage.removeItem("termix-local-snapshot"); localSnapshot.current = {}; } function handleThemeChange(id: ThemeId) { setTheme(id); if (storageMode === "cloud") saveToCloud({ theme: id }); } function handleAccentChange(value: string) { setAccentColor(value); setCustomColorInput(value); localStorage.setItem("termix-accent", value); applyAccentColor(value); if (storageMode === "cloud") saveToCloud({ accentColor: value }); } function handleFontSizeChange(id: FontSizeId) { setFontSize(id); applyFontSize(id); if (storageMode === "cloud") saveToCloud({ fontSize: id }); } function handleLanguageChange(code: string) { void changeAppLanguage(code) .then((language) => { setLanguage(language); if (storageMode === "cloud") saveToCloud({ language }); }) .catch(() => {}); } function toggle(id: UserProfileSection) { setOpenSection((prev) => (prev === id ? null : id)); } async function handleStartTotpSetup() { setTotpLoading(true); try { const result = await setupTOTP(); setTotpQrCode(result.qr_code); setTotpSecret(result.secret); setTotpCode(""); setTotpStep("setup"); } catch { toast.error(t("newUi.sidebar.userProfile.totpSetupFailed")); } finally { setTotpLoading(false); } } async function handleVerifyTotp() { if (!totpCode || totpCode.length !== 6) { toast.error(t("newUi.sidebar.userProfile.totpEnter6Digits")); return; } setTotpLoading(true); try { const result = await enableTOTP(totpCode); setTotpBackupCodes(result.backup_codes ?? []); setTotpEnabled(true); setTotpStep("backup"); toast.success(t("newUi.sidebar.userProfile.totpEnabledSuccess")); } catch (e: unknown) { toast.error( apiErrorMessage(e, t("newUi.sidebar.userProfile.totpInvalidCode")), ); } finally { setTotpLoading(false); } } async function handleDisableTotp() { if (!disableTotpInput) { toast.error(t("newUi.sidebar.userProfile.totpDisableInputRequired")); return; } setTotpLoading(true); try { await disableTOTP(disableTotpInput); setTotpEnabled(false); setShowDisableTotp(false); setDisableTotpInput(""); toast.success(t("newUi.sidebar.userProfile.totpDisabledSuccess")); } catch (e: unknown) { toast.error( apiErrorMessage(e, t("newUi.sidebar.userProfile.totpDisableFailed")), ); } finally { setTotpLoading(false); } } async function handleRegisterPasskey() { setPasskeyLoading(true); try { await registerWebAuthnCredential( passkeyName || "Passkey", passkeyUserVerification, ); const { credentials } = await listWebAuthnCredentials(); setPasskeys(credentials ?? []); setPasskeyName(""); toast.success(t("newUi.sidebar.userProfile.passkeyAdded")); } catch (e: unknown) { toast.error( apiErrorMessage(e, t("newUi.sidebar.userProfile.passkeyAddFailed")), ); } finally { setPasskeyLoading(false); } } async function handleDeletePasskey(credentialId: string) { setPasskeyLoading(true); try { await deleteWebAuthnCredential(credentialId); setPasskeys((prev) => prev.filter((item) => item.id !== credentialId)); toast.success(t("newUi.sidebar.userProfile.passkeyDeleted")); } catch (e: unknown) { toast.error( apiErrorMessage(e, t("newUi.sidebar.userProfile.passkeyDeleteFailed")), ); } finally { setPasskeyLoading(false); } } function downloadBackupCodes() { const blob = new Blob([totpBackupCodes.join("\n")], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "termix-backup-codes.txt"; a.click(); URL.revokeObjectURL(url); } async function handleDeleteAccount() { if (!deletePassword.trim()) { toast.error(t("newUi.sidebar.userProfile.deletePasswordRequired")); return; } setDeleteLoading(true); try { await deleteAccount(deletePassword); await logoutUser().catch(() => {}); window.location.reload(); } catch (e: unknown) { toast.error( apiErrorMessage(e, t("newUi.sidebar.userProfile.deleteFailed")), ); setDeleteLoading(false); } } const canChangePasword = !isOidc || isDualAuth; return (
setApiKeys((prev) => [key, ...prev])} userId={userId} /> {/* Storage mode toggle */}
{t("newUi.sidebar.userProfile.storageModeSwitch")}

{t("newUi.sidebar.userProfile.storageModeDescription")}

{/* Account */} } open={openSection === "account"} onToggle={() => toggle("account")} >
{t("newUi.sidebar.userProfile.usernameLabel")} {username ?? "—"}
{t("newUi.sidebar.userProfile.roleLabel")}
{userRole || "—"} {userRoles.map((r) => ( {r.roleDisplayName} ))}
{t("newUi.sidebar.userProfile.authMethodLabel")} {authMethod || "—"}
{t("newUi.sidebar.userProfile.twoFaLabel")} {totpEnabled ? ( <> {t("newUi.sidebar.userProfile.twoFaOn")} ) : ( {t("newUi.sidebar.userProfile.twoFaOff")} )}
{t("newUi.sidebar.userProfile.versionLabel")}
{version ? `v${version}` : "—"} {versionStatus === "beta" ? t("dashboard.beta").toUpperCase() : versionStatus === "requires_update" ? t("dashboard.updateAvailable").toUpperCase() : t("dashboardTab.stable")}
{isElectron() && onChangeServer && (
{t("serverConfig.changeServer")} {t("newUi.sidebar.userProfile.changeServerDescription")}
)}
{t("newUi.sidebar.userProfile.deleteAccount")} {t("newUi.sidebar.userProfile.deleteAccountDescription")}
{/* Appearance */} } open={openSection === "appearance"} onToggle={() => toggle("appearance")} >
{t("newUi.sidebar.userProfile.languageLabel")}
{t("newUi.sidebar.userProfile.themeLabel")}
{THEMES.filter((th) => th.id !== "system").map((th) => (
{t("newUi.sidebar.userProfile.fontSizeLabel")}
{FONT_SIZES.map((fs) => ( ))}
{t("newUi.sidebar.userProfile.accentColorLabel")}
{ACCENT_PRESET_COLORS.map((ac) => (
{t("newUi.sidebar.userProfile.settingsTerminal")} { setCommandAutocomplete(v); localStorage.setItem("commandAutocomplete", v.toString()); if (storageMode === "cloud") saveToCloud({ commandAutocomplete: v }); }} />
{t("newUi.sidebar.userProfile.terminalLinkBehavior")} {t("newUi.sidebar.userProfile.terminalLinkBehaviorDesc")}
{ setCommandPaletteEnabled(v); localStorage.setItem( "commandPaletteShortcutEnabled", v.toString(), ); window.dispatchEvent( new Event("commandPaletteShortcutEnabledChanged"), ); if (storageMode === "cloud") saveToCloud({ commandPaletteEnabled: v }); }} /> { onPrefsChange?.({ reopenTabsOnLogin: v }); import("@/main-axios").then(({ saveUserPreferences }) => { saveUserPreferences({ reopenTabsOnLogin: v }).catch( () => {}, ); }); }} /> { setConfirmTabClose(v); localStorage.setItem("confirmTabClose", v.toString()); if (storageMode === "cloud") saveToCloud({ confirmTabClose: v }); }} />
{t("newUi.sidebar.userProfile.settingsSidebar")} { setShowHostTags(v); localStorage.setItem("showHostTags", v.toString()); window.dispatchEvent(new Event("showHostTagsChanged")); if (storageMode === "cloud") saveToCloud({ showHostTags: v }); }} /> { setHostTrayOnClick(v); localStorage.setItem("hostTrayOnClick", v.toString()); window.dispatchEvent(new Event("hostTrayOnClickChanged")); if (storageMode === "cloud") saveToCloud({ hostTrayOnClick: v }); }} /> { setCompactHostView(v); localStorage.setItem("compactHostView", v.toString()); window.dispatchEvent(new Event("compactHostViewChanged")); if (storageMode === "cloud") saveToCloud({ compactHostView: v }); }} /> { const scheme = v ? "status" : "accent"; setStatusColorScheme(scheme); localStorage.setItem("statusColorScheme", scheme); window.dispatchEvent(new Event("statusColorSchemeChanged")); if (storageMode === "cloud") saveToCloud({ statusColorScheme: scheme }); }} /> { setPinAppRail(v); localStorage.setItem("pinAppRail", v.toString()); window.dispatchEvent(new Event("pinAppRailChanged")); if (storageMode === "cloud") saveToCloud({ pinAppRail: v }); }} /> { setExpandAppRailOnHover(v); localStorage.setItem("expandAppRailOnHover", v.toString()); window.dispatchEvent( new Event("expandAppRailOnHoverChanged"), ); if (storageMode === "cloud") saveToCloud({ expandAppRailOnHover: v }); }} />
{t("newUi.sidebar.userProfile.settingsNavigation")}

{t("newUi.sidebar.userProfile.navigationTabsDesc")}

{( [ { id: "hosts", icon: , label: t("nav.hosts"), }, { id: "credentials", icon: , label: t("nav.credentials"), }, { id: "connections", icon: , label: t("nav.connections"), }, { id: "quick-connect", icon: , label: t("nav.quickConnect"), }, { id: "serial", icon: , label: t("nav.serial"), }, { id: "ssh-tools", icon: , label: t("nav.sshTools"), }, { id: "snippets", icon: , label: t("nav.snippets"), }, { id: "history", icon: , label: t("nav.history"), }, { id: "session-logs", icon: , label: t("nav.sessionLogs"), }, { id: "split-screen", icon: , label: t("nav.splitScreen"), }, { id: "network_graph", icon: , label: t("nav.networkGraph"), }, ] as { id: string; icon: React.ReactNode; label: string }[] ).map((tab) => (
{tab.icon} {tab.label} { const next = new Set(hiddenRailTabs); if (visible) next.delete(tab.id); else next.add(tab.id); setHiddenRailTabs(next); const serialized = JSON.stringify([...next]); localStorage.setItem("hiddenRailTabs", serialized); window.dispatchEvent(new Event("hiddenRailTabsChanged")); if (storageMode === "cloud") saveToCloud({ hiddenRailTabs: serialized }); }} />
))}
{t("newUi.sidebar.userProfile.settingsSnippets")} { setFoldersCollapsed(v); localStorage.setItem( "defaultSnippetFoldersCollapsed", v.toString(), ); window.dispatchEvent( new Event("defaultSnippetFoldersCollapsedChanged"), ); if (storageMode === "cloud") saveToCloud({ foldersCollapsed: v }); }} /> { setConfirmSnippetExecution(v); localStorage.setItem("confirmSnippetExecution", v.toString()); if (storageMode === "cloud") saveToCloud({ confirmSnippetExecution: v }); }} />
{t("newUi.sidebar.userProfile.settingsUpdates")} { setDisableUpdateCheck(v); localStorage.setItem("disableUpdateCheck", v.toString()); if (storageMode === "cloud") saveToCloud({ disableUpdateCheck: v }); }} />
{/* Security */} } open={openSection === "security"} onToggle={() => toggle("security")} >
{t("newUi.sidebar.userProfile.totpAuthenticator")} {t("hosts.docsLink")}
{totpEnabled ? t("newUi.sidebar.userProfile.totpEnabled") : t("newUi.sidebar.userProfile.totpDisabled")}
{totpEnabled ? ( ) : ( )}
{/* Disable TOTP form */} {totpEnabled && showDisableTotp && (
{t("newUi.sidebar.userProfile.totpDisableTitle")} setDisableTotpInput(e.target.value)} className="text-sm" />
)} {/* TOTP setup: scan QR */} {!totpEnabled && totpStep === "setup" && (
{t("newUi.sidebar.userProfile.setupTotp")}
{totpQrCode ? (
{t("newUi.sidebar.userProfile.qrCode")}
) : (
{t("newUi.sidebar.userProfile.qrCode")}
)}
{totpSecret}
{t("newUi.sidebar.userProfile.totpInstructions")}
)} {/* TOTP setup: verify code */} {!totpEnabled && totpStep === "verify" && (
{t("newUi.sidebar.userProfile.totpVerifyTitle")}
setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6)) } onKeyDown={(e) => e.key === "Enter" && handleVerifyTotp()} className="text-center font-mono tracking-widest text-lg h-10" maxLength={6} />
)} {/* TOTP setup: backup codes */} {totpStep === "backup" && totpBackupCodes.length > 0 && (
{t("newUi.sidebar.userProfile.totpBackupTitle")}
{totpBackupCodes.map((code) => ( {code} ))}
)}
{t("newUi.sidebar.userProfile.passkeys")} {t("newUi.sidebar.userProfile.passkeysDesc")}
setPasskeyName(e.target.value)} className="h-8 text-xs" disabled={passkeyLoading} />
{passkeys.length === 0 ? (
{t("newUi.sidebar.userProfile.noPasskeys")}
) : ( passkeys.map((passkey) => (
{passkey.name} {passkey.deviceType || "unknown"} {passkey.backedUp ? " / synced" : ""}
)) )}
{canChangePasword && ( )}
{/* API Keys */} } open={openSection === "api-keys"} onToggle={() => toggle("api-keys")} >
{t("newUi.sidebar.userProfile.apiKeyCount", { count: apiKeys.length, })}
{apiKeys.length === 0 ? (
{t("newUi.sidebar.userProfile.noApiKeys")}
) : ( apiKeys.map((key) => (
{key.name} {key.isActive && ( {t("newUi.sidebar.userProfile.apiKeyActive")} )}
{key.tokenPrefix}… {t("newUi.sidebar.userProfile.apiKeyUser")}:{" "} {key.username} {key.expiresAt && ( {t("newUi.sidebar.userProfile.apiKeyExpires")}:{" "} {new Date(key.expiresAt).toLocaleDateString()} )}
)) )}

{t("newUi.sidebar.userProfile.apiKeyUsageHint")}{" "} Authorization: Bearer {" "} {t("newUi.sidebar.userProfile.apiKeyUsageHintHeader")}

{t("newUi.sidebar.userProfile.apiKeyPermissionsHint")}

{isElectron() && ( } open={openSection === "c2s-tunnels"} onToggle={() => toggle("c2s-tunnels")} > )} {/* Delete account dialog */} { setShowDeleteConfirm(open); if (!open) setDeletePassword(""); }} > {t("newUi.sidebar.userProfile.deleteAccount")} {t("newUi.sidebar.userProfile.deleteAccountPermanent")}
{t("newUi.sidebar.userProfile.deleteAccountWarning")}
setDeletePassword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleDeleteAccount()} />
); }