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 (
);
}
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 (
);
}
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.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 */}
);
}