import { useState, useRef, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { getUserInfo, getApiKeys, createApiKey, deleteApiKey, changePassword, deleteAccount, logoutUser, setupTOTP, enableTOTP, disableTOTP, getVersionInfo, getUserRoles, saveUserPreferences, } from "@/main-axios"; 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, Copy, Eye, EyeOff, KeyRound, Network, Palette, Plus, Shield, ShieldCheck, Trash2, Type, User, X, } 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 i18n 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, userPrefs, onPrefsChange, }: { username?: string; onLogout?: () => void; userPrefs?: { reopenTabsOnLogin: boolean }; onPrefsChange?: (prefs: { reopenTabsOnLogin: boolean }) => 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(""); // 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(); // 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( () => localStorage.getItem("i18nextLng") ?? "en", ); // Settings toggles — all backed by localStorage const [commandAutocomplete, setCommandAutocomplete] = useState( () => localStorage.getItem("commandAutocomplete") === "true", ); const [commandHistoryTracking, setCommandHistoryTracking] = useState( () => localStorage.getItem("commandHistoryTracking") === "true", ); const [terminalSyntaxHighlighting, setTerminalSyntaxHighlighting] = useState( () => localStorage.getItem("terminalSyntaxHighlighting") === "true", ); 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 [pinAppRail, setPinAppRail] = useState( () => localStorage.getItem("pinAppRail") === "true", ); 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", ); // 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(() => {}); getVersionInfo() .then((info) => { setVersion(info.localVersion); setVersionStatus(info.status ?? "up_to_date"); }) .catch(() => {}); }, [t]); function saveAppearancePreference(prefs: { theme?: ThemeId; fontSize?: FontSizeId; accentColor?: string; language?: string; }) { void saveUserPreferences(prefs).catch(() => {}); } function handleThemeChange(id: ThemeId) { setTheme(id); saveAppearancePreference({ theme: id }); } function handleAccentChange(value: string) { setAccentColor(value); setCustomColorInput(value); localStorage.setItem("termix-accent", value); applyAccentColor(value); saveAppearancePreference({ accentColor: value }); } function handleFontSizeChange(id: FontSizeId) { setFontSize(id); applyFontSize(id); saveAppearancePreference({ fontSize: id }); } function handleLanguageChange(code: string) { setLanguage(code); localStorage.setItem("i18nextLng", code); i18n.changeLanguage(code); saveAppearancePreference({ language: code }); } 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); } } 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} /> {/* 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")}
{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()); }} /> { setCommandHistoryTracking(v); localStorage.setItem("commandHistoryTracking", v.toString()); window.dispatchEvent( new Event("commandHistoryTrackingChanged"), ); }} /> { setTerminalSyntaxHighlighting(v); localStorage.setItem( "terminalSyntaxHighlighting", v.toString(), ); window.dispatchEvent( new Event("terminalSyntaxHighlightingChanged"), ); }} /> { setCommandPaletteEnabled(v); localStorage.setItem( "commandPaletteShortcutEnabled", v.toString(), ); window.dispatchEvent( new Event("commandPaletteShortcutEnabledChanged"), ); }} /> { onPrefsChange?.({ reopenTabsOnLogin: v }); import("@/main-axios").then(({ saveUserPreferences }) => { saveUserPreferences({ reopenTabsOnLogin: v }).catch( () => {}, ); }); }} /> { setConfirmTabClose(v); localStorage.setItem("confirmTabClose", v.toString()); }} />
{t("newUi.sidebar.userProfile.settingsSidebar")} { setShowHostTags(v); localStorage.setItem("showHostTags", v.toString()); window.dispatchEvent(new Event("showHostTagsChanged")); }} /> { setHostTrayOnClick(v); localStorage.setItem("hostTrayOnClick", v.toString()); window.dispatchEvent(new Event("hostTrayOnClickChanged")); }} /> { setPinAppRail(v); localStorage.setItem("pinAppRail", v.toString()); window.dispatchEvent(new Event("pinAppRailChanged")); }} />
{t("newUi.sidebar.userProfile.settingsSnippets")} { setFoldersCollapsed(v); localStorage.setItem( "defaultSnippetFoldersCollapsed", v.toString(), ); window.dispatchEvent( new Event("defaultSnippetFoldersCollapsedChanged"), ); }} /> { setConfirmSnippetExecution(v); localStorage.setItem("confirmSnippetExecution", v.toString()); }} />
{t("newUi.sidebar.userProfile.settingsUpdates")} { setDisableUpdateCheck(v); localStorage.setItem("disableUpdateCheck", v.toString()); }} />
{/* Security */} } open={openSection === "security"} onToggle={() => toggle("security")} >
{t("newUi.sidebar.userProfile.totpAuthenticator")} {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} ))}
)}
{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()} />
); }