fix: rbac implementation general issues (local squash)

This commit is contained in:
LukeGus
2025-12-27 03:04:17 -06:00
parent 4b257dc21c
commit 8af1911358
29 changed files with 2206 additions and 251 deletions

View File

@@ -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>

View File

@@ -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({

View File

@@ -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);

View File

@@ -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

View File

@@ -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"