fix: rbac implementation general issues (local squash)
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
||||
FolderOpen,
|
||||
Pencil,
|
||||
EllipsisVertical,
|
||||
ArrowDownUp,
|
||||
Container,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BiMoney, BiSupport } from "react-icons/bi";
|
||||
@@ -27,6 +29,7 @@ import { GrUpdate } from "react-icons/gr";
|
||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||
import { getRecentActivity, getSSHHosts } from "@/ui/main-axios.ts";
|
||||
import type { RecentActivityItem } from "@/ui/main-axios.ts";
|
||||
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -52,8 +55,10 @@ interface SSHHost {
|
||||
enableTerminal: boolean;
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
enableDocker: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: unknown[];
|
||||
statsConfig?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -88,7 +93,10 @@ export function CommandPalette({
|
||||
const handleAddHost = () => {
|
||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||
if (sshManagerTab) {
|
||||
updateTab(sshManagerTab.id, { initialTab: "add_host" });
|
||||
updateTab(sshManagerTab.id, {
|
||||
initialTab: "add_host",
|
||||
hostConfig: undefined,
|
||||
});
|
||||
setCurrentTab(sshManagerTab.id);
|
||||
} else {
|
||||
const id = addTab({
|
||||
@@ -104,7 +112,10 @@ export function CommandPalette({
|
||||
const handleAddCredential = () => {
|
||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||
if (sshManagerTab) {
|
||||
updateTab(sshManagerTab.id, { initialTab: "add_credential" });
|
||||
updateTab(sshManagerTab.id, {
|
||||
initialTab: "add_credential",
|
||||
hostConfig: undefined,
|
||||
});
|
||||
setCurrentTab(sshManagerTab.id);
|
||||
} else {
|
||||
const id = addTab({
|
||||
@@ -216,6 +227,22 @@ export function CommandPalette({
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleHostTunnelClick = (host: SSHHost) => {
|
||||
const title = host.name?.trim()
|
||||
? host.name
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
addTab({ type: "tunnel", title, hostConfig: host });
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleHostDockerClick = (host: SSHHost) => {
|
||||
const title = host.name?.trim()
|
||||
? host.name
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
addTab({ type: "docker", title, hostConfig: host });
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleHostEditClick = (host: SSHHost) => {
|
||||
const title = host.name?.trim()
|
||||
? host.name
|
||||
@@ -301,6 +328,33 @@ export function CommandPalette({
|
||||
const title = host.name?.trim()
|
||||
? host.name
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
|
||||
// Parse statsConfig to determine if metrics should be shown
|
||||
let shouldShowMetrics = true;
|
||||
try {
|
||||
const statsConfig = host.statsConfig
|
||||
? JSON.parse(host.statsConfig)
|
||||
: DEFAULT_STATS_CONFIG;
|
||||
shouldShowMetrics = statsConfig.metricsEnabled !== false;
|
||||
} catch {
|
||||
shouldShowMetrics = true;
|
||||
}
|
||||
|
||||
// Check if host has at least one tunnel connection
|
||||
let hasTunnelConnections = false;
|
||||
try {
|
||||
const tunnelConnections = Array.isArray(
|
||||
host.tunnelConnections,
|
||||
)
|
||||
? host.tunnelConnections
|
||||
: JSON.parse(host.tunnelConnections as string);
|
||||
hasTunnelConnections =
|
||||
Array.isArray(tunnelConnections) &&
|
||||
tunnelConnections.length > 0;
|
||||
} catch {
|
||||
hasTunnelConnections = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={`host-${index}-${host.id}`}
|
||||
@@ -335,30 +389,62 @@ export function CommandPalette({
|
||||
side="right"
|
||||
className="w-56 bg-canvas border-edge text-foreground"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleHostServerDetailsClick(host);
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
<span className="flex-1">
|
||||
{t("commandPalette.openServerDetails")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleHostFileManagerClick(host);
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="flex-1">
|
||||
{t("commandPalette.openFileManager")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{shouldShowMetrics && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleHostServerDetailsClick(host);
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
<span className="flex-1">
|
||||
{t("hosts.openServerStats")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableFileManager && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleHostFileManagerClick(host);
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="flex-1">
|
||||
{t("hosts.openFileManager")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableTunnel && hasTunnelConnections && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleHostTunnelClick(host);
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
|
||||
>
|
||||
<ArrowDownUp className="h-4 w-4" />
|
||||
<span className="flex-1">
|
||||
{t("hosts.openTunnels")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableDocker && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleHostDockerClick(host);
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
|
||||
>
|
||||
<Container className="h-4 w-4" />
|
||||
<span className="flex-1">
|
||||
{t("hosts.openDocker")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -367,9 +453,7 @@ export function CommandPalette({
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
<span className="flex-1">
|
||||
{t("commandPalette.edit")}
|
||||
</span>
|
||||
<span className="flex-1">{t("common.edit")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -317,7 +317,10 @@ export function Dashboard({
|
||||
const handleAddHost = () => {
|
||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||
if (sshManagerTab) {
|
||||
updateTab(sshManagerTab.id, { initialTab: "add_host" });
|
||||
updateTab(sshManagerTab.id, {
|
||||
initialTab: "add_host",
|
||||
hostConfig: undefined,
|
||||
});
|
||||
setCurrentTab(sshManagerTab.id);
|
||||
} else {
|
||||
const id = addTab({
|
||||
@@ -332,7 +335,10 @@ export function Dashboard({
|
||||
const handleAddCredential = () => {
|
||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||
if (sshManagerTab) {
|
||||
updateTab(sshManagerTab.id, { initialTab: "add_credential" });
|
||||
updateTab(sshManagerTab.id, {
|
||||
initialTab: "add_credential",
|
||||
hostConfig: undefined,
|
||||
});
|
||||
setCurrentTab(sshManagerTab.id);
|
||||
} else {
|
||||
const id = addTab({
|
||||
|
||||
@@ -18,6 +18,7 @@ export function HostManager({
|
||||
isTopbarOpen,
|
||||
initialTab = "host_viewer",
|
||||
hostConfig,
|
||||
_updateTimestamp,
|
||||
rightSidebarOpen = false,
|
||||
rightSidebarWidth = 400,
|
||||
}: HostManagerProps): React.ReactElement {
|
||||
@@ -36,20 +37,39 @@ export function HostManager({
|
||||
const ignoreNextHostConfigChangeRef = useRef<boolean>(false);
|
||||
const lastProcessedHostIdRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Sync state when tab is updated externally (via updateTab or addTab)
|
||||
useEffect(() => {
|
||||
if (initialTab) {
|
||||
setActiveTab(initialTab);
|
||||
}
|
||||
}, [initialTab]);
|
||||
// Always sync on timestamp changes
|
||||
if (_updateTimestamp !== undefined) {
|
||||
// Update activeTab if initialTab has changed
|
||||
if (initialTab && initialTab !== activeTab) {
|
||||
setActiveTab(initialTab);
|
||||
}
|
||||
|
||||
// Update editingHost when hostConfig changes
|
||||
useEffect(() => {
|
||||
if (hostConfig) {
|
||||
setEditingHost(hostConfig);
|
||||
setActiveTab("add_host");
|
||||
lastProcessedHostIdRef.current = hostConfig.id;
|
||||
// Update editingHost if hostConfig has changed
|
||||
if (hostConfig && hostConfig.id !== editingHost?.id) {
|
||||
setEditingHost(hostConfig);
|
||||
lastProcessedHostIdRef.current = hostConfig.id;
|
||||
} else if (!hostConfig && editingHost) {
|
||||
// Clear editingHost if hostConfig is now undefined
|
||||
setEditingHost(null);
|
||||
}
|
||||
|
||||
// Clear editingCredential if switching away from add_credential
|
||||
if (initialTab !== "add_credential" && editingCredential) {
|
||||
setEditingCredential(null);
|
||||
}
|
||||
} else {
|
||||
// Initial mount - set state from props
|
||||
if (initialTab) {
|
||||
setActiveTab(initialTab);
|
||||
}
|
||||
if (hostConfig) {
|
||||
setEditingHost(hostConfig);
|
||||
lastProcessedHostIdRef.current = hostConfig.id;
|
||||
}
|
||||
}
|
||||
}, [hostConfig?.id]);
|
||||
}, [_updateTimestamp, initialTab, hostConfig?.id]);
|
||||
|
||||
const handleEditHost = (host: SSHHost) => {
|
||||
setEditingHost(host);
|
||||
|
||||
@@ -1465,6 +1465,7 @@ export function HostManagerEditor({
|
||||
<Tabs
|
||||
value={authTab}
|
||||
onValueChange={(value) => {
|
||||
if (editingHost?.isShared) return;
|
||||
const newAuthType = value as
|
||||
| "password"
|
||||
| "key"
|
||||
@@ -1478,25 +1479,29 @@ export function HostManagerEditor({
|
||||
<TabsList className="bg-button border border-edge-medium">
|
||||
<TabsTrigger
|
||||
value="password"
|
||||
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
|
||||
disabled={editingHost?.isShared}
|
||||
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t("hosts.password")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="key"
|
||||
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
|
||||
disabled={editingHost?.isShared}
|
||||
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t("hosts.key")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="credential"
|
||||
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
|
||||
disabled={editingHost?.isShared}
|
||||
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t("hosts.credential")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="none"
|
||||
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium"
|
||||
disabled={editingHost?.isShared}
|
||||
className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t("hosts.none")}
|
||||
</TabsTrigger>
|
||||
@@ -1709,26 +1714,34 @@ export function HostManagerEditor({
|
||||
name="credentialId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<CredentialSelector
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
onCredentialSelect={(credential) => {
|
||||
if (
|
||||
credential &&
|
||||
!form.getValues(
|
||||
"overrideCredentialUsername",
|
||||
)
|
||||
) {
|
||||
form.setValue(
|
||||
"username",
|
||||
credential.username,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormDescription>
|
||||
{t("hosts.credentialDescription")}
|
||||
</FormDescription>
|
||||
{editingHost?.isShared ? (
|
||||
<div className="text-sm text-muted-foreground p-3 bg-base border border-edge-medium rounded-md">
|
||||
{t("hosts.cannotChangeAuthAsSharedUser")}
|
||||
</div>
|
||||
) : (
|
||||
<CredentialSelector
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
onCredentialSelect={(credential) => {
|
||||
if (
|
||||
credential &&
|
||||
!form.getValues(
|
||||
"overrideCredentialUsername",
|
||||
)
|
||||
) {
|
||||
form.setValue(
|
||||
"username",
|
||||
credential.username,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!editingHost?.isShared && (
|
||||
<FormDescription>
|
||||
{t("hosts.credentialDescription")}
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -3769,7 +3782,7 @@ export function HostManagerEditor({
|
||||
</ScrollArea>
|
||||
<footer className="shrink-0 w-full pb-0">
|
||||
<Separator className="p-0.25" />
|
||||
{!(editingHost?.permissionLevel === "view") && (
|
||||
{!editingHost?.isShared && (
|
||||
<Button className="translate-y-2" type="submit" variant="outline">
|
||||
{editingHost
|
||||
? editingHost.id
|
||||
|
||||
@@ -76,10 +76,8 @@ interface User {
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
const PERMISSION_LEVELS = [
|
||||
{ value: "view", labelKey: "rbac.view" },
|
||||
{ value: "manage", labelKey: "rbac.manage" },
|
||||
];
|
||||
// Only view permission is supported (manage removed due to encryption constraints)
|
||||
const PERMISSION_LEVELS = [{ value: "view", labelKey: "rbac.view" }];
|
||||
|
||||
export function HostSharingTab({
|
||||
hostId,
|
||||
@@ -430,26 +428,12 @@ export function HostSharingTab({
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Permission Level */}
|
||||
{/* Permission Level - Always "view" (read-only) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="permission-level">
|
||||
{t("rbac.permissionLevel")}
|
||||
</Label>
|
||||
<Select
|
||||
value={permissionLevel || "use"}
|
||||
onValueChange={(v) => setPermissionLevel(v || "use")}
|
||||
>
|
||||
<SelectTrigger id="permission-level">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PERMISSION_LEVELS.map((level) => (
|
||||
<SelectItem key={level.value} value={level.value}>
|
||||
{t(level.labelKey)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label>{t("rbac.permissionLevel")}</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("rbac.view")} - {t("rbac.viewDesc")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expiration */}
|
||||
@@ -496,7 +480,6 @@ export function HostSharingTab({
|
||||
<TableHead>{t("rbac.permissionLevel")}</TableHead>
|
||||
<TableHead>{t("rbac.grantedBy")}</TableHead>
|
||||
<TableHead>{t("rbac.expires")}</TableHead>
|
||||
<TableHead>{t("rbac.accessCount")}</TableHead>
|
||||
<TableHead className="text-right">
|
||||
{t("common.actions")}
|
||||
</TableHead>
|
||||
@@ -506,7 +489,7 @@ export function HostSharingTab({
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={7}
|
||||
colSpan={6}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
{t("common.loading")}
|
||||
@@ -515,7 +498,7 @@ export function HostSharingTab({
|
||||
) : accessList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={7}
|
||||
colSpan={6}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
{t("rbac.noAccessRecords")}
|
||||
@@ -582,7 +565,6 @@ export function HostSharingTab({
|
||||
t("rbac.never")
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{access.accessCount}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user