import React from 'react'; import { Computer, Server, File, Hammer, ChevronUp, User2, HardDrive, Trash2, Users, Shield, Settings } from "lucide-react"; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarInset, } from "@/components/ui/sidebar.tsx" import { Separator, } from "@/components/ui/separator.tsx" import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@radix-ui/react-dropdown-menu"; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet"; import {Checkbox} from "@/components/ui/checkbox.tsx"; import {Input} from "@/components/ui/input.tsx"; import {Label} from "@/components/ui/label.tsx"; import {Button} from "@/components/ui/button.tsx"; import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx"; import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table.tsx"; import axios from "axios"; interface SidebarProps { onSelectView: (view: string) => void; getView?: () => string; disabled?: boolean; isAdmin?: boolean; username?: string | null; children?: React.ReactNode; } function handleLogout() { document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; window.location.reload(); } function getCookie(name: string) { return document.cookie.split('; ').reduce((r, v) => { const parts = v.split('='); return parts[0] === name ? decodeURIComponent(parts[1]) : r; }, ""); } const apiBase = import.meta.env.DEV ? "http://localhost:8081/users" : "/users"; const API = axios.create({ baseURL: apiBase, }); export function HomepageSidebar({ onSelectView, getView, disabled, isAdmin, username, children, }: SidebarProps): React.ReactElement { const [adminSheetOpen, setAdminSheetOpen] = React.useState(false); const [allowRegistration, setAllowRegistration] = React.useState(true); const [regLoading, setRegLoading] = React.useState(false); const [oidcConfig, setOidcConfig] = React.useState({ client_id: '', client_secret: '', issuer_url: '', authorization_url: '', token_url: '', identifier_path: 'sub', name_path: 'name', scopes: 'openid email profile' }); const [oidcLoading, setOidcLoading] = React.useState(false); const [oidcError, setOidcError] = React.useState(null); const [oidcSuccess, setOidcSuccess] = React.useState(null); const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false); const [deletePassword, setDeletePassword] = React.useState(""); const [deleteLoading, setDeleteLoading] = React.useState(false); const [deleteError, setDeleteError] = React.useState(null); const [adminCount, setAdminCount] = React.useState(0); const [users, setUsers] = React.useState>([]); const [usersLoading, setUsersLoading] = React.useState(false); const [newAdminUsername, setNewAdminUsername] = React.useState(""); const [makeAdminLoading, setMakeAdminLoading] = React.useState(false); const [makeAdminError, setMakeAdminError] = React.useState(null); const [makeAdminSuccess, setMakeAdminSuccess] = React.useState(null); React.useEffect(() => { if (adminSheetOpen) { const jwt = getCookie("jwt"); if (jwt && isAdmin) { API.get("/oidc-config").then(res => { if (res.data) { setOidcConfig(res.data); } }).catch((error) => { }); fetchUsers(); } } else { const jwt = getCookie("jwt"); if (jwt && isAdmin) { fetchAdminCount(); } } }, [adminSheetOpen, isAdmin]); React.useEffect(() => { if (!isAdmin) { setAdminSheetOpen(false); setUsers([]); setAdminCount(0); } }, [isAdmin]); const handleToggle = async (checked: boolean) => { if (!isAdmin) { return; } setRegLoading(true); const jwt = getCookie("jwt"); try { await API.patch( "/registration-allowed", {allowed: checked}, {headers: {Authorization: `Bearer ${jwt}`}} ); setAllowRegistration(checked); } catch (e) { } finally { setRegLoading(false); } }; const handleOIDCConfigSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!isAdmin) { return; } setOidcLoading(true); setOidcError(null); setOidcSuccess(null); const requiredFields = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url']; const missingFields = requiredFields.filter(field => !oidcConfig[field as keyof typeof oidcConfig]); if (missingFields.length > 0) { setOidcError(`Missing required fields: ${missingFields.join(', ')}`); setOidcLoading(false); return; } const jwt = getCookie("jwt"); try { await API.post( "/oidc-config", oidcConfig, {headers: {Authorization: `Bearer ${jwt}`}} ); setOidcSuccess("OIDC configuration updated successfully!"); } catch (err: any) { setOidcError(err?.response?.data?.error || "Failed to update OIDC configuration"); } finally { setOidcLoading(false); } }; const handleOIDCConfigChange = (field: string, value: string) => { setOidcConfig(prev => ({ ...prev, [field]: value })); }; const handleDeleteAccount = async (e: React.FormEvent) => { e.preventDefault(); setDeleteLoading(true); setDeleteError(null); if (!deletePassword.trim()) { setDeleteError("Password is required"); setDeleteLoading(false); return; } const jwt = getCookie("jwt"); try { await API.delete("/delete-account", { headers: {Authorization: `Bearer ${jwt}`}, data: {password: deletePassword} }); handleLogout(); } catch (err: any) { setDeleteError(err?.response?.data?.error || "Failed to delete account"); setDeleteLoading(false); } }; const fetchUsers = async () => { const jwt = getCookie("jwt"); if (!jwt || !isAdmin) { return; } setUsersLoading(true); try { const response = await API.get("/list", { headers: {Authorization: `Bearer ${jwt}`} }); setUsers(response.data.users); const adminUsers = response.data.users.filter((user: any) => user.is_admin); setAdminCount(adminUsers.length); } catch (err: any) { console.error("Failed to fetch users:", err); } finally { setUsersLoading(false); } }; const fetchAdminCount = async () => { const jwt = getCookie("jwt"); if (!jwt || !isAdmin) { return; } try { const response = await API.get("/list", { headers: {Authorization: `Bearer ${jwt}`} }); const adminUsers = response.data.users.filter((user: any) => user.is_admin); setAdminCount(adminUsers.length); } catch (err: any) { console.error("Failed to fetch admin count:", err); } }; const makeUserAdmin = async (e: React.FormEvent) => { e.preventDefault(); if (!newAdminUsername.trim()) return; if (!isAdmin) { return; } setMakeAdminLoading(true); setMakeAdminError(null); setMakeAdminSuccess(null); const jwt = getCookie("jwt"); try { await API.post("/make-admin", {username: newAdminUsername.trim()}, {headers: {Authorization: `Bearer ${jwt}`}} ); setMakeAdminSuccess(`User ${newAdminUsername} is now an admin`); setNewAdminUsername(""); fetchUsers(); } catch (err: any) { setMakeAdminError(err?.response?.data?.error || "Failed to make user admin"); } finally { setMakeAdminLoading(false); } }; const removeAdminStatus = async (username: string) => { if (!confirm(`Are you sure you want to remove admin status from ${username}?`)) return; if (!isAdmin) { return; } const jwt = getCookie("jwt"); try { await API.post("/remove-admin", {username}, {headers: {Authorization: `Bearer ${jwt}`}} ); fetchUsers(); } catch (err: any) { console.error("Failed to remove admin status:", err); } }; const deleteUser = async (username: string) => { if (!confirm(`Are you sure you want to delete user ${username}? This action cannot be undone.`)) return; if (!isAdmin) { return; } const jwt = getCookie("jwt"); try { await API.delete("/delete-user", { headers: {Authorization: `Bearer ${jwt}`}, data: {username} }); fetchUsers(); } catch (err: any) { console.error("Failed to delete user:", err); } }; return (
Termix onSelectView("ssh_manager")} disabled={disabled}> SSH Manager
onSelectView("terminal")} disabled={disabled}> Terminal onSelectView("tunnel")} disabled={disabled}> Tunnel onSelectView("config_editor")} disabled={disabled}> Config Editor
window.open("https://dashix.dev", "_blank")} disabled={disabled}> Tools
{username ? username : 'Signed out'} {isAdmin && ( { if (isAdmin) { setAdminSheetOpen(true); } }}> Admin Settings )} Sign out setDeleteAccountOpen(true)} disabled={isAdmin && adminCount <= 1} > Delete Account {isAdmin && adminCount <= 1 && " (Last Admin)"} {/* Admin Settings Sheet */} {isAdmin && ( { if (open && !isAdmin) return; setAdminSheetOpen(open); }}> Admin Settings
Reg OIDC Users Admins {/* Registration Settings Tab */}

User Registration

{/* OIDC Configuration Tab */}

External Authentication (OIDC)

Configure external identity provider for OIDC/OAuth2 authentication. Users will see an "External" login option once configured.

{oidcError && ( Error {oidcError} )}
handleOIDCConfigChange('client_id', e.target.value)} placeholder="your-client-id" required />
handleOIDCConfigChange('client_secret', e.target.value)} placeholder="your-client-secret" required />
handleOIDCConfigChange('authorization_url', e.target.value)} placeholder="https://your-provider.com/application/o/authorize/" required />
handleOIDCConfigChange('issuer_url', e.target.value)} placeholder="https://your-provider.com/application/o/termix/" required />
handleOIDCConfigChange('token_url', e.target.value)} placeholder="https://your-provider.com/application/o/token/" required />
handleOIDCConfigChange('identifier_path', e.target.value)} placeholder="sub" required />

JSON path to extract user ID from JWT (e.g., "sub", "email", "preferred_username")

handleOIDCConfigChange('name_path', e.target.value)} placeholder="name" required />

JSON path to extract display name from JWT (e.g., "name", "preferred_username")

handleOIDCConfigChange('scopes', e.target.value)} placeholder="openid email profile" required />

Space-separated list of OAuth2 scopes to request

{oidcSuccess && ( Success {oidcSuccess} )}
{/* Users Management Tab */}

User Management

{usersLoading ? (
Loading users...
) : (
Username Type Actions {users.map((user) => ( {user.username} {user.is_admin && ( Admin )} {user.is_oidc ? "External" : "Local"} ))}
)}
{/* Admins Management Tab */}

Admin Management

{/* Add New Admin Form */}

Make User Admin

setNewAdminUsername(e.target.value)} placeholder="Enter username to make admin" required />
{makeAdminError && ( Error {makeAdminError} )} {makeAdminSuccess && ( Success {makeAdminSuccess} )}
{/* Current Admins Table */}

Current Admins

Username Type Actions {users.filter(user => user.is_admin).map((admin) => ( {admin.username} Admin {admin.is_oidc ? "External" : "Local"} ))}
)} {/* Delete Account Confirmation Sheet */} Delete Account This action cannot be undone. This will permanently delete your account and all associated data.
Warning Deleting your account will remove all your data including SSH hosts, configurations, and settings. This action is irreversible. {deleteError && ( Error {deleteError} )}
{isAdmin && adminCount <= 1 && ( Cannot Delete Account You are the last admin user. You cannot delete your account as this would leave the system without any administrators. Please make another user an admin first, or contact system support. )}
setDeletePassword(e.target.value)} placeholder="Enter your password to confirm" required disabled={isAdmin && adminCount <= 1} />
{children}
) }