import React from "react"; import { cn } from "@/lib/utils.ts"; import { Button } from "@/components/ui/button.tsx"; import { Input } from "@/components/ui/input.tsx"; import { Badge } from "@/components/ui/badge.tsx"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table.tsx"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs.tsx"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; import { toast } from "sonner"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; import { getRoles, getUserList, getUserInfo, shareHost, getHostAccess, revokeHostAccess, getSSHHostById, type Role, type AccessRecord, } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command.tsx"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover.tsx"; import { Plus, Check, ChevronsUpDown, AlertCircle, Trash2, Users, Shield, Clock, UserCircle, } from "lucide-react"; import type { SSHHost } from "@/types"; interface User { id: string; username: string; is_admin: boolean; } export function HostSharingTab({ hostId, isNewHost, }: HostSharingTabProps): React.ReactElement { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); const [shareType, setShareType] = React.useState<"user" | "role">("user"); const [selectedUserId, setSelectedUserId] = React.useState(""); const [selectedRoleId, setSelectedRoleId] = React.useState( null, ); const permissionLevel = "view"; const [expiresInHours, setExpiresInHours] = React.useState(""); const [roles, setRoles] = React.useState([]); const [users, setUsers] = React.useState([]); const [accessList, setAccessList] = React.useState([]); const [loading, setLoading] = React.useState(false); const [currentUserId, setCurrentUserId] = React.useState(""); const [hostData, setHostData] = React.useState(null); const [userComboOpen, setUserComboOpen] = React.useState(false); const [roleComboOpen, setRoleComboOpen] = React.useState(false); const loadRoles = React.useCallback(async () => { try { const response = await getRoles(); setRoles(response.roles || []); } catch (error) { console.error("Failed to load roles:", error); setRoles([]); } }, []); const loadUsers = React.useCallback(async () => { try { const response = await getUserList(); const mappedUsers = (response.users || []).map((user) => ({ id: user.id, username: user.username, is_admin: user.is_admin, })); setUsers(mappedUsers); } catch (error) { console.error("Failed to load users:", error); setUsers([]); } }, []); const loadAccessList = React.useCallback(async () => { if (!hostId) return; setLoading(true); try { const response = await getHostAccess(hostId); setAccessList(response.accessList || []); } catch (error) { console.error("Failed to load access list:", error); setAccessList([]); } finally { setLoading(false); } }, [hostId]); const loadHostData = React.useCallback(async () => { if (!hostId) return; try { const host = await getSSHHostById(hostId); setHostData(host); } catch (error) { console.error("Failed to load host data:", error); setHostData(null); } }, [hostId]); React.useEffect(() => { loadRoles(); loadUsers(); if (!isNewHost) { loadAccessList(); loadHostData(); } }, [loadRoles, loadUsers, loadAccessList, loadHostData, isNewHost]); React.useEffect(() => { const fetchCurrentUser = async () => { try { const userInfo = await getUserInfo(); setCurrentUserId(userInfo.userId); } catch (error) { console.error("Failed to load current user:", error); } }; fetchCurrentUser(); }, []); const handleShare = async () => { if (!hostId) { toast.error(t("rbac.saveHostFirst")); return; } if (shareType === "user" && !selectedUserId) { toast.error(t("rbac.selectUser")); return; } if (shareType === "role" && !selectedRoleId) { toast.error(t("rbac.selectRole")); return; } if (shareType === "user" && selectedUserId === currentUserId) { toast.error(t("rbac.cannotShareWithSelf")); return; } try { await shareHost(hostId, { targetType: shareType, targetUserId: shareType === "user" ? selectedUserId : undefined, targetRoleId: shareType === "role" ? selectedRoleId : undefined, permissionLevel, durationHours: expiresInHours ? parseInt(expiresInHours, 10) : undefined, }); toast.success(t("rbac.sharedSuccessfully")); setSelectedUserId(""); setSelectedRoleId(null); setExpiresInHours(""); loadAccessList(); } catch { toast.error(t("rbac.failedToShare")); } }; const handleRevoke = async (accessId: number) => { if (!hostId) return; const confirmed = await confirmWithToast({ title: t("rbac.confirmRevokeAccess"), description: t("rbac.confirmRevokeAccessDescription"), confirmText: t("common.revoke"), cancelText: t("common.cancel"), }); if (!confirmed) return; try { await revokeHostAccess(hostId, accessId); toast.success(t("rbac.accessRevokedSuccessfully")); loadAccessList(); } catch { toast.error(t("rbac.failedToRevokeAccess")); } }; const formatDate = (dateString: string | null) => { if (!dateString) return "-"; return new Date(dateString).toLocaleString(); }; const isExpired = (expiresAt: string | null) => { if (!expiresAt) return false; return new Date(expiresAt) < new Date(); }; const availableUsers = React.useMemo(() => { return users.filter((user) => user.id !== currentUserId); }, [users, currentUserId]); const selectedUser = availableUsers.find((u) => u.id === selectedUserId); const selectedRole = roles.find((r) => r.id === selectedRoleId); if (isNewHost) { return ( {t("rbac.saveHostFirst")} {t("rbac.saveHostFirstDescription")} ); } return (
{!hostData?.credentialId && ( {t("rbac.credentialRequired")} {t("rbac.credentialRequiredDescription")} )} {hostData?.credentialId && ( <>

{t("rbac.shareHost")}

setShareType(v as "user" | "role")} > {t("rbac.shareWithUser")} {t("rbac.shareWithRole")}
{t("rbac.noUserFound")} {availableUsers.map((user) => ( { setSelectedUserId(user.id); setUserComboOpen(false); }} > {user.username} {user.is_admin ? " (Admin)" : ""} ))}
{t("rbac.noRoleFound")} {roles.map((role) => ( { setSelectedRoleId(role.id); setRoleComboOpen(false); }} > {t(role.displayName)} {role.isSystem ? ` (${t("rbac.systemRole")})` : ""} ))}
{t("rbac.view")} - {t("rbac.viewDesc")}
{ const value = e.target.value; if (value === "" || /^\d+$/.test(value)) { setExpiresInHours(value); } }} placeholder={t("rbac.neverExpires")} min="1" />

{t("rbac.accessList")}

{t("rbac.type")} {t("rbac.target")} {t("rbac.permissionLevel")} {t("rbac.grantedBy")} {t("rbac.expires")} {t("common.actions")} {loading ? ( {t("common.loading")} ) : accessList.length === 0 ? ( {t("rbac.noAccessRecords")} ) : ( accessList.map((access) => ( {access.targetType === "user" ? ( {t("rbac.user")} ) : ( {t("rbac.role")} )} {access.targetType === "user" ? access.username : t(access.roleDisplayName || access.roleName || "")} {access.permissionLevel} {access.grantedByUsername} {access.expiresAt ? (
{formatDate(access.expiresAt)} {isExpired(access.expiresAt) && ( ({t("rbac.expired")}) )}
) : ( t("rbac.never") )}
)) )}
)}
); }