import { useState, useRef, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { getUserInfo, getApiKeys, createApiKey, deleteApiKey, changePassword, } 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"; type UserProfileSection = | "account" | "appearance" | "security" | "api-keys" | "c2s-tunnels"; const SECTIONS: { id: UserProfileSection; label: string; icon: React.ReactNode; }[] = [ { id: "account", label: "Account", icon: }, { id: "appearance", label: "Appearance", icon: , }, { id: "security", label: "Security", icon: }, { id: "api-keys", label: "API Keys", icon: }, ]; const THEMES: { id: ThemeId; label: string; preview: string }[] = [ { id: "system", label: "System", preview: "auto" }, { id: "light", label: "Light", preview: "#ffffff" }, { id: "dark", label: "Dark", preview: "#1a1c22" }, { id: "dracula", label: "Dracula", preview: "#282a36" }, { id: "catppuccin", label: "Catppuccin", preview: "#1e1e2e" }, { id: "nord", label: "Nord", preview: "#2e3440" }, { id: "solarized", label: "Solarized", preview: "#002b36" }, { id: "tokyo-night", label: "Tokyo Night", preview: "#1a1b26" }, { id: "one-dark", label: "One Dark", preview: "#282c34" }, { id: "gruvbox", label: "Gruvbox", preview: "#282828" }, ]; 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}
)}
); } function NewApiKeyDialog({ open, onOpenChange, onAdd, userId, }: { open: boolean; onOpenChange: (v: boolean) => void; onAdd: (key: any) => void; userId: string; }) { const { t } = useTranslation(); const [name, setName] = useState(""); const [expiry, setExpiry] = useState(""); const handleCreate = async () => { if (!name.trim()) { toast.error("API key name is required"); return; } try { const created = await createApiKey( name.trim(), userId, expiry ? new Date(expiry).toISOString() : undefined, ); onAdd(created); onOpenChange(false); setName(""); setExpiry(""); toast.success(`API key "${name}" created`); } catch { toast.error("Failed to create API key"); } }; return (
{t("newUi.sidebar.userProfile.createApiKeyTitle")} {t("newUi.sidebar.userProfile.createApiKeyDescription")}
setName(e.target.value)} 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("All password fields are required"); return; } if (newPw !== confirmPw) { toast.error("Passwords don't match"); return; } try { await changePassword(currentPw, newPw); toast.success("Password updated. Please log in again."); setCurrentPw(""); setNewPw(""); setConfirmPw(""); onLogout?.(); } catch (e: any) { toast.error(e?.response?.data?.error || "Failed to update password"); } } 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, }: { username?: string; onLogout?: () => void; }) { const { t } = useTranslation(); const [openSection, setOpenSection] = useState( "account", ); const [showTotpSetup, setShowTotpSetup] = useState(false); const [totpEnabled, setTotpEnabled] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showPassword, setShowPassword] = useState(false); 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 [apiKeys, setApiKeys] = useState([]); const [userId, setUserId] = useState(""); useEffect(() => { getUserInfo() .then((info) => { setUserId(info.userId); setTotpEnabled(info.totp_enabled); }) .catch(() => {}); getApiKeys() .then(({ apiKeys: keys }) => setApiKeys(keys)) .catch(() => {}); }, []); const [newKeyOpen, setNewKeyOpen] = useState(false); const colorInputRef = useRef(null); const { theme, setTheme } = useTheme(); function handleAccentChange(value: string) { setAccentColor(value); setCustomColorInput(value); localStorage.setItem("termix-accent", value); applyAccentColor(value); } function handleFontSizeChange(id: FontSizeId) { setFontSize(id); applyFontSize(id); } function toggle(id: UserProfileSection) { setOpenSection((prev) => (prev === id ? null : id)); } return (
setApiKeys((prev) => [key, ...prev])} userId={userId} /> {/* Account */} } open={openSection === "account"} onToggle={() => toggle("account")} >
{t("newUi.sidebar.userProfile.usernameLabel")} {username ?? "user"}
{t("newUi.sidebar.userProfile.roleLabel")} {t("newUi.sidebar.userProfile.roleAdministrator")}
{t("newUi.sidebar.userProfile.authMethodLabel")} {t("newUi.sidebar.userProfile.authMethodLocal")}
{t("newUi.sidebar.userProfile.twoFaLabel")} {totpEnabled ? ( <> {t("newUi.sidebar.userProfile.twoFaOn")} ) : ( {t("newUi.sidebar.userProfile.twoFaOff")} )}
{t("newUi.sidebar.userProfile.versionLabel")}
v1.0.0{" "} {t("newUi.sidebar.userProfile.versionStable")} {t("newUi.sidebar.userProfile.upToDate")}
{t("newUi.sidebar.userProfile.deleteAccount")} {t("newUi.sidebar.userProfile.deleteAccountDescription")}
{/* Appearance */} } open={openSection === "appearance"} onToggle={() => toggle("appearance")} >
{t("newUi.sidebar.userProfile.languageLabel")}
{/* Theme — dropdown */}
{t("newUi.sidebar.userProfile.themeLabel")}
{/* Live preview strip */}
{THEMES.filter((t) => t.id !== "system").map((t) => (
{/* Font Size */}
{t("newUi.sidebar.userProfile.fontSizeLabel")}
{FONT_SIZES.map((fs) => ( ))}
{/* Accent Color */}
{t("newUi.sidebar.userProfile.accentColorLabel")} {/* Preset swatches */}
{ACCENT_PRESET_COLORS.map((ac) => (
{/* Custom color input */}
{t("newUi.sidebar.userProfile.settingsFileManager")}
{t("newUi.sidebar.userProfile.settingsTerminal")}
{t("newUi.sidebar.userProfile.settingsSidebar")}
{t("newUi.sidebar.userProfile.settingsSnippets")}
{t("newUi.sidebar.userProfile.settingsUpdates")}
{/* Security */} } open={openSection === "security"} onToggle={() => toggle("security")} >
{t("newUi.sidebar.userProfile.totpAuthenticator")} {totpEnabled ? t("newUi.sidebar.userProfile.totpEnabled") : t("newUi.sidebar.userProfile.totpDisabled")}
{showTotpSetup && !totpEnabled && (
{t("newUi.sidebar.userProfile.setupTotp")}
{t("newUi.sidebar.userProfile.qrCode")}
JBSWY3DPEHPK3PXP
{t("newUi.sidebar.userProfile.totpInstructions")}
)}
{/* 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}… User: {key.username} {key.expiresAt && ( Exp: {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 confirm dialog */} {t("newUi.sidebar.userProfile.deleteAccount")} {t("newUi.sidebar.userProfile.deleteAccountPermanent")}
{t("newUi.sidebar.userProfile.deleteAccountWarning")}
); }