* fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * feat: Add docker management support (local squash) * Fix RBAC role system bugs and improve UX (#446) * Fix RBAC role system bugs and improve UX - Fix user list dropdown selection in host sharing - Fix role sharing permissions to include role-based access - Fix translation template interpolation for success messages - Standardize system roles to admin and user only - Auto-assign user role to new registrations - Remove blocking confirmation dialogs in modal contexts - Add missing i18n keys for common actions - Fix button type to prevent unintended form submissions * Enhance RBAC system with UI improvements and security fixes - Move role assignment to Users tab with per-user role management - Protect system roles (admin/user) from editing and manual assignment - Simplify permission system: remove Use level, keep View and Manage - Hide Update button and Sharing tab for view-only/shared hosts - Prevent users from sharing hosts with themselves - Unify table and modal styling across admin panels - Auto-assign system roles on user registration - Add permission metadata to host interface * Add empty state message for role assignment - Display helpful message when no custom roles available - Clarify that system roles are auto-assigned - Add noCustomRolesToAssign translation in English and Chinese * fix: Prevent credential sharing errors for shared hosts - Skip credential resolution for shared hosts with credential authentication to prevent decryption errors (credentials are encrypted per-user) - Add warning alert in sharing tab when host uses credential authentication - Inform users that shared users cannot connect to credential-based hosts - Add translations for credential sharing warning (EN/ZH) This prevents authentication failures when sharing hosts configured with credential authentication while maintaining security by keeping credentials isolated per user. * feat: Improve rbac UI and fixes some bugs --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> * SOCKS5 support (#452) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * SOCKS5 support Adding single and chain socks5 proxy support * fix: cleanup files --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> * Notes and Expiry fields add (#453) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Notes and Expiry add * fix: cleanup files --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> * fix: ssh host types * fix: sudo incorrect styling and remove expiration date * feat: add sudo password and add diagonal bg's * fix: snippet running on enter key * fix: base64 decoding * fix: improve server stats / rbac * fix: wrap ssh host json export in hosts array * feat: auto trim host inputs, fix file manager jump hosts, dashboard prevent duplicates, file manager terminal not size updating, improve left sidebar sorting, hide/show tags, add apperance user profile tab, add new host manager tabs. * feat: improve terminal connection speed * fix: sqlite constriant errors and support non-root user (nginx perm issue) * feat: add beta syntax highlighing to terminal * feat: update imports and improve admin settings user management * chore: update translations * chore: update translations * feat: Complete light mode implementation with semantic theme system (#450) - Add comprehensive light/dark mode CSS variables with semantic naming - Implement theme-aware scrollbars using CSS variables - Add light mode backgrounds: --bg-base, --bg-elevated, --bg-surface, etc. - Add theme-aware borders: --border-base, --border-panel, --border-subtle - Add semantic text colors: --foreground-secondary, --foreground-subtle - Convert oklch colors to hex for better compatibility - Add theme awareness to CodeMirror editors - Update dark mode colors for consistency (background, sidebar, card, muted, input) - Add Tailwind color mappings for semantic classes Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: syntax errors * chore: updating/match themes and split admin settings * feat: add translation workflow and remove old translation.json * fix: translation workflow error * fix: translation workflow error * feat: improve translation system and update workflow * fix: wrong path for translations * fix: change translation to flat files * fix: gh rule error * chore: auto-translate to multiple languages (#458) * chore: improve organization and made a few styling changes in host manager * feat: improve terminal stability and split out the host manager * fix: add unnversiioned files * chore: migrate all to use the new theme system * fix: wrong animation line colors * fix: rbac implementation general issues (local squash) * fix: remove unneeded files * feat: add 10 new langs * chore: update gitnore * chore: auto-translate to multiple languages (#459) * fix: improve tunnel system * fix: properly split tabs, still need to fix up the host manager * chore: cleanup files (possible RC) * feat: add norwegian * chore: auto-translate to multiple languages (#461) * fix: small qol fixes and began readme update * fix: run cleanup script * feat: add docker docs button * feat: general bug fixes and readme updates * fix: translations * chore: auto-translate to multiple languages (#462) * fix: cleanup files * fix: test new translation issue and add better server-stats support * fix: fix translate error * chore: auto-translate to multiple languages (#463) * fix: fix translate mismatching text * chore: auto-translate to multiple languages (#465) * fix: fix translate mismatching text * fix: fix translate mismatching text * chore: auto-translate to multiple languages (#466) * fix: fix translate mismatching text * fix: fix translate mismatching text * fix: fix translate mismatching text * chore: auto-translate to multiple languages (#467) * fix: fix translate mismatching text * chore: auto-translate to multiple languages (#468) * feat: add to readme, a few qol changes, and improve server stats in general * chore: auto-translate to multiple languages (#469) * feat: turned disk uage into graph and fixed issue with termina console * fix: electron build error and hide icons when shared * chore: run clean * fix: general server stats issues, file manager decoding, ui qol * fix: add dashboard line breaks * fix: docker console error * fix: docker console not loading and mismatched stripped background for electron * fix: docker console not loading * chore: docker console not loading in docker * chore: translate readme to chinese * chore: match package lock to package json * chore: nginx config issue for dokcer console * chore: auto-translate to multiple languages (#470) --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Denis <38875137+Medvedinca@users.noreply.github.com> Co-authored-by: Peet McKinney <68706879+PeetMcK@users.noreply.github.com>
741 lines
28 KiB
TypeScript
741 lines
28 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
||
import { Label } from "@/components/ui/label.tsx";
|
||
import { Button } from "@/components/ui/button.tsx";
|
||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||
import {
|
||
Tabs,
|
||
TabsContent,
|
||
TabsList,
|
||
TabsTrigger,
|
||
} from "@/components/ui/tabs.tsx";
|
||
import { Separator } from "@/components/ui/separator.tsx";
|
||
import { Switch } from "@/components/ui/switch.tsx";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select.tsx";
|
||
import {
|
||
User,
|
||
Shield,
|
||
AlertCircle,
|
||
Palette,
|
||
Sun,
|
||
Moon,
|
||
Monitor,
|
||
} from "lucide-react";
|
||
import { useTheme } from "@/components/theme-provider";
|
||
import { TOTPSetup } from "@/ui/desktop/user/TOTPSetup.tsx";
|
||
import {
|
||
getUserInfo,
|
||
getVersionInfo,
|
||
deleteAccount,
|
||
logoutUser,
|
||
isElectron,
|
||
getUserRoles,
|
||
type UserRole,
|
||
} from "@/ui/main-axios.ts";
|
||
import { PasswordReset } from "@/ui/desktop/user/PasswordReset.tsx";
|
||
import { useTranslation } from "react-i18next";
|
||
import { LanguageSwitcher } from "@/ui/desktop/user/LanguageSwitcher.tsx";
|
||
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||
|
||
interface UserProfileProps {
|
||
isTopbarOpen?: boolean;
|
||
rightSidebarOpen?: boolean;
|
||
rightSidebarWidth?: number;
|
||
}
|
||
|
||
async function handleLogout() {
|
||
try {
|
||
await logoutUser();
|
||
|
||
if (isElectron()) {
|
||
localStorage.removeItem("jwt");
|
||
|
||
const configuredServerUrl = (
|
||
window as Window &
|
||
typeof globalThis & {
|
||
configuredServerUrl?: string;
|
||
}
|
||
).configuredServerUrl;
|
||
|
||
if (configuredServerUrl) {
|
||
const iframe = document.querySelector("iframe");
|
||
if (iframe && iframe.contentWindow) {
|
||
try {
|
||
const serverOrigin = new URL(configuredServerUrl).origin;
|
||
iframe.contentWindow.postMessage(
|
||
{
|
||
type: "CLEAR_AUTH_DATA",
|
||
timestamp: Date.now(),
|
||
},
|
||
serverOrigin,
|
||
);
|
||
} catch (err) {
|
||
console.error("User profile operation failed:", err);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
window.location.reload();
|
||
} catch (error) {
|
||
console.error("Logout failed:", error);
|
||
window.location.reload();
|
||
}
|
||
}
|
||
|
||
export function UserProfile({
|
||
isTopbarOpen = true,
|
||
rightSidebarOpen = false,
|
||
rightSidebarWidth = 400,
|
||
}: UserProfileProps) {
|
||
const { t } = useTranslation();
|
||
const { state: sidebarState } = useSidebar();
|
||
const { theme, setTheme } = useTheme();
|
||
const [userInfo, setUserInfo] = useState<{
|
||
username: string;
|
||
is_admin: boolean;
|
||
is_oidc: boolean;
|
||
is_dual_auth: boolean;
|
||
totp_enabled: boolean;
|
||
} | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [versionInfo, setVersionInfo] = useState<{ version: string } | null>(
|
||
null,
|
||
);
|
||
|
||
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
||
const [deletePassword, setDeletePassword] = useState("");
|
||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||
const [fileColorCoding, setFileColorCoding] = useState<boolean>(
|
||
localStorage.getItem("fileColorCoding") !== "false",
|
||
);
|
||
const [commandAutocomplete, setCommandAutocomplete] = useState<boolean>(
|
||
localStorage.getItem("commandAutocomplete") === "true",
|
||
);
|
||
const [terminalSyntaxHighlighting, setTerminalSyntaxHighlighting] =
|
||
useState<boolean>(
|
||
() => localStorage.getItem("terminalSyntaxHighlighting") === "true",
|
||
);
|
||
const [defaultSnippetFoldersCollapsed, setDefaultSnippetFoldersCollapsed] =
|
||
useState<boolean>(
|
||
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false",
|
||
);
|
||
const [showHostTags, setShowHostTags] = useState<boolean>(() => {
|
||
const saved = localStorage.getItem("showHostTags");
|
||
return saved !== null ? saved === "true" : true;
|
||
});
|
||
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
|
||
|
||
useEffect(() => {
|
||
fetchUserInfo();
|
||
fetchVersion();
|
||
}, []);
|
||
|
||
const fetchVersion = async () => {
|
||
try {
|
||
const info = await getVersionInfo();
|
||
setVersionInfo({ version: info.localVersion });
|
||
} catch {
|
||
const { toast } = await import("sonner");
|
||
toast.error(t("user.failedToLoadVersionInfo"));
|
||
}
|
||
};
|
||
|
||
const fetchUserInfo = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const info = await getUserInfo();
|
||
setUserInfo({
|
||
username: info.username,
|
||
is_admin: info.is_admin,
|
||
is_oidc: info.is_oidc,
|
||
is_dual_auth: info.is_dual_auth || false,
|
||
totp_enabled: info.totp_enabled || false,
|
||
});
|
||
|
||
try {
|
||
const rolesResponse = await getUserRoles(info.userId);
|
||
setUserRoles(rolesResponse.roles || []);
|
||
} catch (rolesErr) {
|
||
console.error("Failed to fetch user roles:", rolesErr);
|
||
setUserRoles([]);
|
||
}
|
||
} catch (err: unknown) {
|
||
const error = err as { response?: { data?: { error?: string } } };
|
||
setError(error?.response?.data?.error || t("errors.loadFailed"));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleTOTPStatusChange = (enabled: boolean) => {
|
||
if (userInfo) {
|
||
setUserInfo({ ...userInfo, totp_enabled: enabled });
|
||
}
|
||
};
|
||
|
||
const handleFileColorCodingToggle = (enabled: boolean) => {
|
||
setFileColorCoding(enabled);
|
||
localStorage.setItem("fileColorCoding", enabled.toString());
|
||
window.dispatchEvent(new Event("fileColorCodingChanged"));
|
||
};
|
||
|
||
const handleCommandAutocompleteToggle = (enabled: boolean) => {
|
||
setCommandAutocomplete(enabled);
|
||
localStorage.setItem("commandAutocomplete", enabled.toString());
|
||
};
|
||
|
||
const handleTerminalSyntaxHighlightingToggle = (enabled: boolean) => {
|
||
setTerminalSyntaxHighlighting(enabled);
|
||
localStorage.setItem("terminalSyntaxHighlighting", enabled.toString());
|
||
window.dispatchEvent(new Event("terminalSyntaxHighlightingChanged"));
|
||
};
|
||
|
||
const handleDefaultSnippetFoldersCollapsedToggle = (enabled: boolean) => {
|
||
setDefaultSnippetFoldersCollapsed(enabled);
|
||
localStorage.setItem("defaultSnippetFoldersCollapsed", enabled.toString());
|
||
window.dispatchEvent(new Event("defaultSnippetFoldersCollapsedChanged"));
|
||
};
|
||
|
||
const handleShowHostTagsToggle = (enabled: boolean) => {
|
||
setShowHostTags(enabled);
|
||
localStorage.setItem("showHostTags", enabled.toString());
|
||
window.dispatchEvent(new Event("showHostTagsChanged"));
|
||
};
|
||
|
||
const handleDeleteAccount = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setDeleteLoading(true);
|
||
setDeleteError(null);
|
||
|
||
if (!deletePassword.trim()) {
|
||
setDeleteError(t("leftSidebar.passwordRequired"));
|
||
setDeleteLoading(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await deleteAccount(deletePassword);
|
||
handleLogout();
|
||
} catch (err: unknown) {
|
||
setDeleteError(
|
||
(err as { response?: { data?: { error?: string } } })?.response?.data
|
||
?.error || t("leftSidebar.failedToDeleteAccount"),
|
||
);
|
||
setDeleteLoading(false);
|
||
}
|
||
};
|
||
|
||
const topMarginPx = isTopbarOpen ? 74 : 26;
|
||
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
|
||
const bottomMarginPx = 8;
|
||
const wrapperStyle: React.CSSProperties = {
|
||
marginLeft: leftMarginPx,
|
||
marginRight: rightSidebarOpen
|
||
? `calc(var(--right-sidebar-width, ${rightSidebarWidth}px) + 8px)`
|
||
: 17,
|
||
marginTop: topMarginPx,
|
||
marginBottom: bottomMarginPx,
|
||
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
|
||
transition:
|
||
"margin-left 200ms linear, margin-right 200ms linear, margin-top 200ms linear",
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div
|
||
style={wrapperStyle}
|
||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||
>
|
||
<div className="h-full w-full flex flex-col">
|
||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||
<h1 className="font-bold text-lg">{t("nav.userProfile")}</h1>
|
||
</div>
|
||
<Separator className="p-0.25 w-full" />
|
||
<div className="flex-1 flex items-center justify-center">
|
||
<div className="animate-pulse text-foreground-secondary">
|
||
{t("common.loading")}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error || !userInfo) {
|
||
return (
|
||
<div
|
||
style={wrapperStyle}
|
||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||
>
|
||
<div className="h-full w-full flex flex-col">
|
||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||
<h1 className="font-bold text-lg">{t("nav.userProfile")}</h1>
|
||
</div>
|
||
<Separator className="p-0.25 w-full" />
|
||
<div className="flex-1 flex items-center justify-center p-6">
|
||
<Alert
|
||
variant="destructive"
|
||
className="bg-red-900/20 border-red-500/50"
|
||
>
|
||
<AlertCircle className="h-4 w-4" />
|
||
<AlertTitle className="text-red-400">
|
||
{t("common.error")}
|
||
</AlertTitle>
|
||
<AlertDescription className="text-red-300">
|
||
{error || t("errors.loadFailed")}
|
||
</AlertDescription>
|
||
</Alert>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
style={wrapperStyle}
|
||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||
>
|
||
<div className="h-full w-full flex flex-col">
|
||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||
<h1 className="font-bold text-lg">{t("nav.userProfile")}</h1>
|
||
</div>
|
||
<Separator className="p-0.25 w-full" />
|
||
|
||
<div className="px-6 py-4 overflow-auto thin-scrollbar flex-1">
|
||
<Tabs defaultValue="profile" className="w-full">
|
||
<TabsList className="mb-4 bg-elevated border-2 border-edge">
|
||
<TabsTrigger
|
||
value="profile"
|
||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||
>
|
||
<User className="w-4 h-4" />
|
||
{t("profile.account")}
|
||
</TabsTrigger>
|
||
<TabsTrigger
|
||
value="appearance"
|
||
className="flex items-center gap-2 data-[state=active]:bg-button"
|
||
>
|
||
<Palette className="w-4 h-4" />
|
||
{t("profile.appearance")}
|
||
</TabsTrigger>
|
||
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
|
||
<TabsTrigger
|
||
value="security"
|
||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||
>
|
||
<Shield className="w-4 h-4" />
|
||
{t("profile.security")}
|
||
</TabsTrigger>
|
||
)}
|
||
</TabsList>
|
||
|
||
<TabsContent value="profile" className="space-y-4">
|
||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||
<h3 className="text-lg font-semibold mb-4">
|
||
{t("profile.accountInfo")}
|
||
</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("common.username")}
|
||
</Label>
|
||
<p className="text-lg font-medium mt-1 text-foreground">
|
||
{userInfo.username}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.role")}
|
||
</Label>
|
||
<div className="mt-1">
|
||
{userRoles.length > 0 ? (
|
||
<div className="flex flex-wrap gap-2">
|
||
{userRoles.map((role) => (
|
||
<span
|
||
key={role.roleId}
|
||
className="inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium bg-muted/50 text-foreground border border-border"
|
||
>
|
||
{t(role.roleDisplayName)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className="text-lg font-medium text-foreground">
|
||
{userInfo.is_admin
|
||
? t("interface.administrator")
|
||
: t("interface.user")}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.authMethod")}
|
||
</Label>
|
||
<p className="text-lg font-medium mt-1 text-foreground">
|
||
{userInfo.is_dual_auth
|
||
? t("profile.externalAndLocal")
|
||
: userInfo.is_oidc
|
||
? t("profile.external")
|
||
: t("profile.local")}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.twoFactorAuth")}
|
||
</Label>
|
||
<p className="text-lg font-medium mt-1">
|
||
{userInfo.is_oidc && !userInfo.is_dual_auth ? (
|
||
<span className="text-muted-foreground">
|
||
{t("auth.lockedOidcAuth")}
|
||
</span>
|
||
) : userInfo.totp_enabled ? (
|
||
<span className="text-green-400 flex items-center gap-1">
|
||
<Shield className="w-4 h-4" />
|
||
{t("common.enabled")}
|
||
</span>
|
||
) : (
|
||
<span className="text-muted-foreground">
|
||
{t("common.disabled")}
|
||
</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("common.version")}
|
||
</Label>
|
||
<p className="text-lg font-medium mt-1 text-foreground">
|
||
{versionInfo?.version || t("common.loading")}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 pt-6 border-t border-edge">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-red-400">
|
||
{t("leftSidebar.deleteAccount")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("leftSidebar.deleteAccountWarningShort")}
|
||
</p>
|
||
</div>
|
||
<Button
|
||
variant="destructive"
|
||
onClick={() => setDeleteAccountOpen(true)}
|
||
>
|
||
{t("leftSidebar.deleteAccount")}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="appearance" className="space-y-4">
|
||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||
<h3 className="text-lg font-semibold mb-4">
|
||
{t("profile.languageLocalization")}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("common.language")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.selectPreferredLanguage")}
|
||
</p>
|
||
</div>
|
||
<LanguageSwitcher />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||
<h3 className="text-lg font-semibold mb-4">
|
||
{t("profile.appearance")}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.theme")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.appearanceDesc")}
|
||
</p>
|
||
</div>
|
||
<Select value={theme} onValueChange={setTheme}>
|
||
<SelectTrigger className="w-[140px]">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="light">
|
||
<div className="flex items-center gap-2">
|
||
<Sun className="w-4 h-4" />
|
||
{t("profile.themeLight")}
|
||
</div>
|
||
</SelectItem>
|
||
<SelectItem value="dark">
|
||
<div className="flex items-center gap-2">
|
||
<Moon className="w-4 h-4" />
|
||
{t("profile.themeDark")}
|
||
</div>
|
||
</SelectItem>
|
||
<SelectItem value="system">
|
||
<div className="flex items-center gap-2">
|
||
<Monitor className="w-4 h-4" />
|
||
{t("profile.themeSystem")}
|
||
</div>
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||
<h3 className="text-lg font-semibold mb-4">
|
||
{t("profile.fileManagerSettings")}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.fileColorCoding")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.fileColorCodingDesc")}
|
||
</p>
|
||
</div>
|
||
<Switch
|
||
checked={fileColorCoding}
|
||
onCheckedChange={handleFileColorCodingToggle}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||
<h3 className="text-lg font-semibold mb-4">
|
||
{t("profile.terminalSettings")}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.commandAutocomplete")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.commandAutocompleteDesc")}
|
||
</p>
|
||
</div>
|
||
<Switch
|
||
checked={commandAutocomplete}
|
||
onCheckedChange={handleCommandAutocompleteToggle}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.terminalSyntaxHighlighting")}{" "}
|
||
<span className="text-xs text-yellow-500 font-semibold">
|
||
(BETA)
|
||
</span>
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.terminalSyntaxHighlightingDesc")}
|
||
</p>
|
||
</div>
|
||
<Switch
|
||
checked={terminalSyntaxHighlighting}
|
||
onCheckedChange={handleTerminalSyntaxHighlightingToggle}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||
<h3 className="text-lg font-semibold mb-4">
|
||
{t("profile.hostSidebarSettings")}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.showHostTags")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.showHostTagsDesc")}
|
||
</p>
|
||
</div>
|
||
<Switch
|
||
checked={showHostTags}
|
||
onCheckedChange={handleShowHostTagsToggle}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||
<h3 className="text-lg font-semibold mb-4">
|
||
{t("profile.snippetsSettings")}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.defaultSnippetFoldersCollapsed")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.defaultSnippetFoldersCollapsedDesc")}
|
||
</p>
|
||
</div>
|
||
<Switch
|
||
checked={defaultSnippetFoldersCollapsed}
|
||
onCheckedChange={
|
||
handleDefaultSnippetFoldersCollapsedToggle
|
||
}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="security" className="space-y-4">
|
||
<TOTPSetup
|
||
isEnabled={userInfo.totp_enabled}
|
||
onStatusChange={handleTOTPStatusChange}
|
||
/>
|
||
|
||
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
|
||
<PasswordReset userInfo={userInfo} />
|
||
)}
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{deleteAccountOpen && (
|
||
<div
|
||
className="fixed top-0 left-0 right-0 bottom-0 z-[999999] pointer-events-auto isolate"
|
||
style={{
|
||
transform: "translateZ(0)",
|
||
willChange: "z-index",
|
||
}}
|
||
>
|
||
<div
|
||
className="w-[400px] h-full bg-canvas border-r-2 border-edge flex flex-col shadow-2xl relative isolate z-[9999999]"
|
||
style={{
|
||
boxShadow: "4px 0 20px rgba(0, 0, 0, 0.5)",
|
||
transform: "translateZ(0)",
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="flex items-center justify-between p-4 border-b border-edge">
|
||
<h2 className="text-lg font-semibold text-foreground">
|
||
{t("leftSidebar.deleteAccount")}
|
||
</h2>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
setDeleteAccountOpen(false);
|
||
setDeletePassword("");
|
||
setDeleteError(null);
|
||
}}
|
||
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-foreground transition-colors flex items-center justify-center"
|
||
title={t("leftSidebar.closeDeleteAccount")}
|
||
>
|
||
<span className="text-lg font-bold leading-none">×</span>
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto p-4 thin-scrollbar">
|
||
<div className="space-y-4">
|
||
<div className="text-sm text-foreground-secondary">
|
||
{t("leftSidebar.deleteAccountWarning")}
|
||
<Alert variant="destructive" className="mb-5 mt-5">
|
||
<AlertTitle>{t("common.warning")}</AlertTitle>
|
||
<AlertDescription>
|
||
{t("leftSidebar.deleteAccountWarningDetails")}
|
||
</AlertDescription>
|
||
</Alert>
|
||
|
||
{deleteError && (
|
||
<Alert variant="destructive">
|
||
<AlertTitle>{t("common.error")}</AlertTitle>
|
||
<AlertDescription>{deleteError}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
<form onSubmit={handleDeleteAccount} className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="delete-password">
|
||
{t("leftSidebar.confirmPassword")}
|
||
</Label>
|
||
<PasswordInput
|
||
id="delete-password"
|
||
value={deletePassword}
|
||
onChange={(e) => setDeletePassword(e.target.value)}
|
||
placeholder={t("placeholders.confirmPassword")}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex gap-2">
|
||
<Button
|
||
type="submit"
|
||
variant="destructive"
|
||
className="flex-1"
|
||
disabled={deleteLoading || !deletePassword.trim()}
|
||
>
|
||
{deleteLoading
|
||
? t("leftSidebar.deleting")
|
||
: t("leftSidebar.deleteAccount")}
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => {
|
||
setDeleteAccountOpen(false);
|
||
setDeletePassword("");
|
||
setDeleteError(null);
|
||
}}
|
||
>
|
||
{t("leftSidebar.cancel")}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className="flex-1 cursor-pointer"
|
||
onClick={() => {
|
||
setDeleteAccountOpen(false);
|
||
setDeletePassword("");
|
||
setDeleteError(null);
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|