Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a401733b3 | ||
|
|
07b6895380 |
@@ -13,5 +13,6 @@
|
||||
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
|
||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
||||
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
|
||||
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID"
|
||||
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
|
||||
"wasmPath": "$NETBIRD_WASM_PATH"
|
||||
}
|
||||
@@ -61,11 +61,12 @@ export NETBIRD_GOOGLE_ANALYTICS_ID=${NETBIRD_GOOGLE_ANALYTICS_ID}
|
||||
export NETBIRD_GOOGLE_TAG_MANAGER_ID=${NETBIRD_GOOGLE_TAG_MANAGER_ID}
|
||||
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
|
||||
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH:-https://pkgs.netbird.io/wasm/client}
|
||||
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
|
||||
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import { Callout } from "@components/Callout";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { ArrowUpRightIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
import PeersProvider from "@/contexts/PeersProvider";
|
||||
@@ -59,6 +60,17 @@ export default function NetworkRoutes() {
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
|
||||
<Callout className={"max-w-xl mt-3"} variant={"warning"}>
|
||||
<span>
|
||||
We recommend using the new Networks concept to easier visualise
|
||||
and manage access to your resources.{" "}
|
||||
<InlineLink href={"/networks"}>
|
||||
Go to Networks
|
||||
<ArrowUpRightIcon size={14} />
|
||||
</InlineLink>
|
||||
</span>
|
||||
</Callout>
|
||||
</div>
|
||||
|
||||
<RestrictedAccess hasAccess={permission.routes.read}>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/DropdownMenu";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Separator from "@components/Separator";
|
||||
@@ -12,12 +20,14 @@ import { cn } from "@utils/helpers";
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
HelpCircle,
|
||||
MoreVertical,
|
||||
PencilLineIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
ShieldXIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
@@ -25,8 +35,10 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Network } from "@/interfaces/Network";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
|
||||
import NetworkModal from "@/modules/networks/NetworkModal";
|
||||
import { NetworkProvider } from "@/modules/networks/NetworkProvider";
|
||||
import {
|
||||
NetworkProvider,
|
||||
useNetworksContext,
|
||||
} from "@/modules/networks/NetworkProvider";
|
||||
import { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
|
||||
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
|
||||
|
||||
@@ -77,35 +89,24 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
|
||||
|
||||
<div className={"flex justify-between max-w-6xl"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!network.description && "gap-2",
|
||||
)}
|
||||
className={"w-full lg:w-1/2 flex justify-between items-center"}
|
||||
>
|
||||
<NetworkInformationSquare
|
||||
name={network.name}
|
||||
active={isActive}
|
||||
size={"lg"}
|
||||
description={network.description}
|
||||
/>
|
||||
{permission.networks.update && (
|
||||
<button
|
||||
className={
|
||||
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
}
|
||||
onClick={() => setNetworkModal(true)}
|
||||
>
|
||||
<PencilLineIcon size={18} />
|
||||
</button>
|
||||
)}
|
||||
<NetworkModal
|
||||
open={networkModal}
|
||||
setOpen={setNetworkModal}
|
||||
onUpdated={() => {
|
||||
mutate(`/networks/${network.id}`);
|
||||
}}
|
||||
network={network}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center w-full",
|
||||
!network.description && "gap-2",
|
||||
)}
|
||||
>
|
||||
<NetworkInformationSquare
|
||||
name={network.name}
|
||||
active={isActive}
|
||||
size={"lg"}
|
||||
description={network.description}
|
||||
/>
|
||||
</div>
|
||||
<NetworkProvider network={network}>
|
||||
<NetworkActions />
|
||||
</NetworkProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -124,6 +125,56 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkActions() {
|
||||
const { permission } = usePermissions();
|
||||
const { deleteNetwork, openEditNetworkModal, network } = useNetworksContext();
|
||||
const router = useRouter();
|
||||
|
||||
if (!network) return;
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
asChild={true}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Button variant={"secondary"} className={"!px-3"}>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-auto" align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => openEditNetworkModal(network)}
|
||||
disabled={!permission.networks.update}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<PencilLineIcon size={14} className={"shrink-0"} />
|
||||
Rename
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
deleteNetwork(network).then(() => router.push("/networks"))
|
||||
}
|
||||
variant={"danger"}
|
||||
disabled={!permission.networks.delete}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
Delete
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
const isHighlyAvailable = !!(
|
||||
network?.routing_peers_count && network?.routing_peers_count >= 2
|
||||
@@ -154,7 +205,7 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
const policyCount = network.policies?.length ?? 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card className={"w-full lg:w-1/2"}>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
tooltip={false}
|
||||
|
||||
@@ -4,8 +4,6 @@ import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import { Callout } from "@components/Callout";
|
||||
import Card from "@components/Card";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
@@ -38,12 +36,10 @@ import {
|
||||
FlagIcon,
|
||||
Globe,
|
||||
History,
|
||||
LockIcon,
|
||||
MapPin,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
PencilIcon,
|
||||
TerminalSquare,
|
||||
TimerResetIcon,
|
||||
} from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
@@ -66,6 +62,7 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
|
||||
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
|
||||
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
|
||||
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
|
||||
@@ -83,9 +80,8 @@ export default function PeerPage() {
|
||||
|
||||
const peerKey = useMemo(() => {
|
||||
let id = peer?.id ?? "";
|
||||
let ssh = peer?.ssh_enabled ? "1" : "0";
|
||||
let expiration = peer?.login_expiration_enabled ? "1" : "0";
|
||||
return `${id}-${ssh}-${expiration}`;
|
||||
return `${id}-${expiration}`;
|
||||
}, [peer]);
|
||||
|
||||
if (isRestricted) {
|
||||
@@ -107,7 +103,7 @@ export default function PeerPage() {
|
||||
);
|
||||
|
||||
return peer && !isLoading ? (
|
||||
<PeerProvider peer={peer} key={peerId}>
|
||||
<PeerProvider peer={peer} key={peerId} isPeerDetailPage={true}>
|
||||
<PeerOverview key={peerKey} />
|
||||
</PeerProvider>
|
||||
) : (
|
||||
@@ -141,8 +137,7 @@ function PeerOverview() {
|
||||
const PeerGeneralInformation = () => {
|
||||
const router = useRouter();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { peer, user, peerGroups, openSSHDialog, update } = usePeer();
|
||||
const [ssh, setSsh] = useState(peer.ssh_enabled);
|
||||
const { peer, user, peerGroups, update } = usePeer();
|
||||
const [name, setName] = useState(peer.name);
|
||||
const [showEditNameModal, setShowEditNameModal] = useState(false);
|
||||
const [loginExpiration, setLoginExpiration] = useState(
|
||||
@@ -161,7 +156,6 @@ const PeerGeneralInformation = () => {
|
||||
* Detect if there are changes in the peer information, if there are changes, then enable the save button.
|
||||
*/
|
||||
const { hasChanges, updateRef: updateHasChangedRef } = useHasChanges([
|
||||
ssh,
|
||||
selectedGroups,
|
||||
loginExpiration,
|
||||
inactivityExpiration,
|
||||
@@ -174,7 +168,6 @@ const PeerGeneralInformation = () => {
|
||||
if (permission.peers.update) {
|
||||
const updateRequest = update({
|
||||
name: newName ?? name,
|
||||
ssh,
|
||||
loginExpiration,
|
||||
inactivityExpiration,
|
||||
});
|
||||
@@ -190,7 +183,6 @@ const PeerGeneralInformation = () => {
|
||||
mutate("/peers/" + peer.id);
|
||||
mutate("/groups");
|
||||
updateHasChangedRef([
|
||||
ssh,
|
||||
selectedGroups,
|
||||
loginExpiration,
|
||||
inactivityExpiration,
|
||||
@@ -314,41 +306,7 @@ const PeerGeneralInformation = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FullTooltip
|
||||
content={
|
||||
<div
|
||||
className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}
|
||||
>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
className={"w-full block"}
|
||||
disabled={permission.peers.update}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
value={ssh}
|
||||
disabled={!permission.peers.update}
|
||||
onChange={(set) =>
|
||||
!set
|
||||
? setSsh(false)
|
||||
: openSSHDialog().then((confirm) => setSsh(confirm))
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<TerminalSquare size={16} />
|
||||
SSH Access
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Enable the SSH server on this peer to access the machine via an secure shell."
|
||||
}
|
||||
/>
|
||||
</FullTooltip>
|
||||
<PeerSSHToggle />
|
||||
|
||||
{/* Remote Access Buttons */}
|
||||
<div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { notify } from "@components/Notification";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { IconCircleX } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
NetBirdStatus,
|
||||
useNetBirdClient,
|
||||
} from "@/modules/remote-access/useNetBirdClient";
|
||||
import { cn } from "@utils/helpers";
|
||||
|
||||
export default function RDPPage() {
|
||||
const { peerId } = useRDPQueryParams();
|
||||
@@ -31,7 +31,7 @@ export default function RDPPage() {
|
||||
} = useFetchApi<Peer>(`/peers/${peerId}`, true, false, !!peerId);
|
||||
|
||||
return (
|
||||
<div className={"w-screen h-screen overflow-hidden"}>
|
||||
<div className={"w-screen h-screen overflow-hidden fixed inset-0"}>
|
||||
{peerId && peer && !isLoading ? (
|
||||
<RDPSession key={peer.id} peer={peer} />
|
||||
) : (
|
||||
@@ -55,7 +55,7 @@ function RDPSession({ peer }: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${peer.name} - ${peer.ip} - RDP`;
|
||||
}, []);
|
||||
}, [peer.ip, peer.name, connected, rdp]);
|
||||
|
||||
const sendErrorNotification = (title: string, message: string) => {
|
||||
notify({
|
||||
@@ -104,6 +104,7 @@ function RDPSession({ peer }: Props) {
|
||||
port: credentials.port,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
domain: credentials.domain,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { PageNotFound } from "@components/ui/PageNotFound";
|
||||
import useFetchApi, { ErrorResponse } from "@utils/api";
|
||||
import { isNativeSSHSupported } from "@utils/version";
|
||||
import { CircleXIcon, InfoIcon, Loader2Icon } from "lucide-react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
@@ -86,7 +87,8 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
if (isSSHConnected || isSSHConnecting) return;
|
||||
connected.current = false;
|
||||
try {
|
||||
const rules = [`tcp/${port}`];
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const rules = [`tcp/${aclPort}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
await ssh({
|
||||
hostname: peer.ip,
|
||||
@@ -107,7 +109,8 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
if (connected.current) return;
|
||||
connected.current = true;
|
||||
try {
|
||||
const rules = [`tcp/${port}`];
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const rules = [`tcp/${aclPort}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
const res = await ssh({
|
||||
hostname: peer.ip,
|
||||
|
||||
BIN
src/assets/ssh/ssh-client.png
Normal file
BIN
src/assets/ssh/ssh-client.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
@@ -309,7 +309,7 @@ export function PeerGroupSelector({
|
||||
"flex items-center gap-2 border-nb-gray-700 flex-wrap h-full"
|
||||
}
|
||||
>
|
||||
{resource && showResources && (
|
||||
{resource && (
|
||||
<ResourceBadge
|
||||
className={"py-[3px]"}
|
||||
resource={resources?.find((r) => r.id === resource.id)}
|
||||
|
||||
@@ -34,31 +34,34 @@ export default function PolicyDirection({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [disabled]);
|
||||
|
||||
const isNetworkResource =
|
||||
!!destinationResource && destinationResource?.type !== "peer";
|
||||
|
||||
const topBadgeClass = useMemo(() => {
|
||||
if (destinationResource) return "blueDark";
|
||||
if (isNetworkResource) return "blueDark";
|
||||
if (value === "bi") return "green";
|
||||
if (value === "in") return "blueDark";
|
||||
return "gray";
|
||||
}, [value, destinationResource]);
|
||||
}, [value, isNetworkResource]);
|
||||
|
||||
const topArrowClass = useMemo(() => {
|
||||
if (destinationResource) return "fill-sky-500";
|
||||
if (isNetworkResource) return "fill-sky-500";
|
||||
if (value === "bi") return "fill-green-500";
|
||||
if (value === "in") return "fill-sky-500";
|
||||
return "fill-gray-500";
|
||||
}, [value, destinationResource]);
|
||||
}, [value, isNetworkResource]);
|
||||
|
||||
const bottomBadgeClass = useMemo(() => {
|
||||
if (destinationResource) return "gray";
|
||||
if (isNetworkResource) return "gray";
|
||||
if (value === "bi") return "green";
|
||||
return "gray";
|
||||
}, [value, destinationResource]);
|
||||
}, [value, isNetworkResource]);
|
||||
|
||||
const bottomArrowClass = useMemo(() => {
|
||||
if (destinationResource) return "fill-gray-500";
|
||||
if (isNetworkResource) return "fill-gray-500";
|
||||
if (value === "bi") return "fill-green-500";
|
||||
return "fill-gray-500";
|
||||
}, [value, destinationResource]);
|
||||
}, [value, isNetworkResource]);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { notify } from "@components/Notification";
|
||||
import SkeletonPeerDetail from "@components/skeletons/SkeletonPeerDetail";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import React, { useMemo } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import { Group, GroupPeer } from "@/interfaces/Group";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { User } from "@/interfaces/User";
|
||||
import { PeerSSHInstructions } from "@/modules/peer/PeerSSHInstructions";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
peer: Peer;
|
||||
isPeerDetailPage?: boolean;
|
||||
};
|
||||
|
||||
const PeerContext = React.createContext(
|
||||
@@ -28,18 +31,25 @@ const PeerContext = React.createContext(
|
||||
approval_required?: boolean;
|
||||
ip?: string;
|
||||
}) => Promise<Peer>;
|
||||
openSSHDialog: () => Promise<boolean>;
|
||||
toggleSSH: (newState: boolean) => Promise<void>;
|
||||
setSSHInstructionsModal: (open: boolean) => void;
|
||||
deletePeer: () => void;
|
||||
isLoading: boolean;
|
||||
},
|
||||
);
|
||||
|
||||
export default function PeerProvider({ children, peer }: Props) {
|
||||
export default function PeerProvider({
|
||||
children,
|
||||
peer,
|
||||
isPeerDetailPage = false,
|
||||
}: Props) {
|
||||
const user = usePeerUser(peer);
|
||||
const { peerGroups, isLoading } = usePeerGroups(peer);
|
||||
const peerRequest = useApiCall<Peer>("/peers", true);
|
||||
const { confirm } = useDialog();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { permission } = usePermissions();
|
||||
const [sshInstructionsModal, setSSHInstructionsModal] = useState(false);
|
||||
|
||||
const deletePeer = async () => {
|
||||
const choice = await confirm({
|
||||
@@ -94,25 +104,20 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
);
|
||||
};
|
||||
|
||||
const openSSHDialog = async (): Promise<boolean> => {
|
||||
return await confirm({
|
||||
title: `Enable SSH Server for ${peer.name}?`,
|
||||
description: (
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<div>
|
||||
Enabling this option allows remote SSH access to this machine from
|
||||
other connected network participants.
|
||||
</div>
|
||||
<div>
|
||||
Make sure SSH is allowed in the NetBird Client under{" "}
|
||||
<span className={"text-white"}>Settings → Allow SSH</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
confirmText: "Enable",
|
||||
cancelText: "Cancel",
|
||||
type: "warning",
|
||||
maxWidthClass: "max-w-lg",
|
||||
const toggleSSH = async (enable: boolean) => {
|
||||
if (!permission.peers.update) return;
|
||||
notify({
|
||||
title: peer.name,
|
||||
description: enable
|
||||
? "SSH Access successfully enabled"
|
||||
: "SSH Access successfully disabled",
|
||||
promise: update({ ssh: enable }).then(() => {
|
||||
isPeerDetailPage ? mutate(`/peers/${peer.id}`) : mutate("/peers");
|
||||
setSSHInstructionsModal(false);
|
||||
}),
|
||||
loadingMessage: enable
|
||||
? "Enabling SSH Access..."
|
||||
: "Disabling SSH Access...",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -123,16 +128,25 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
peerGroups,
|
||||
user,
|
||||
update,
|
||||
openSSHDialog,
|
||||
toggleSSH,
|
||||
setSSHInstructionsModal,
|
||||
deletePeer,
|
||||
isLoading,
|
||||
}}
|
||||
>
|
||||
{sshInstructionsModal && (
|
||||
<PeerSSHInstructions
|
||||
open={sshInstructionsModal}
|
||||
onOpenChange={setSSHInstructionsModal}
|
||||
onSuccess={() => toggleSSH(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</PeerContext.Provider>
|
||||
) : (
|
||||
) : isPeerDetailPage ? (
|
||||
<SkeletonPeerDetail />
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -289,7 +289,7 @@ export function AccessControlModalContent({
|
||||
showRoutes={true}
|
||||
showResources={false}
|
||||
showPeers={true}
|
||||
showResourceCounter={true}
|
||||
showResourceCounter={false}
|
||||
showPeerCount={allowEditPeers}
|
||||
disableInlineRemoveGroup={false}
|
||||
values={sourceGroups}
|
||||
|
||||
@@ -37,6 +37,9 @@ export default function AccessControlActiveCell({ policy }: Readonly<Props>) {
|
||||
if (rule.destinationResource) {
|
||||
rule.destinations = null;
|
||||
}
|
||||
if (rule.sourceResource) {
|
||||
rule.sources = null;
|
||||
}
|
||||
});
|
||||
|
||||
updatePolicy(
|
||||
|
||||
@@ -16,7 +16,9 @@ export default function AccessControlDirectionCell({
|
||||
}, [policy]);
|
||||
|
||||
const bidirectional = firstRule ? firstRule.bidirectional : false;
|
||||
const isSingleResource = !!firstRule?.destinationResource;
|
||||
const isSingleResource =
|
||||
!!firstRule?.destinationResource &&
|
||||
firstRule?.destinationResource?.type !== "peer";
|
||||
|
||||
return (
|
||||
<div className={"flex h-full"}>
|
||||
|
||||
@@ -282,7 +282,10 @@ export const useAccessControl = ({
|
||||
const hasPortSupport = (p: Protocol) => p === "tcp" || p === "udp";
|
||||
const portDisabled = !hasPortSupport(protocol);
|
||||
|
||||
const isDestinationPeer = destinationResource?.type === "peer";
|
||||
|
||||
const destinationHasResources = useMemo(() => {
|
||||
if (isDestinationPeer) return false;
|
||||
if (destinationResource) return true;
|
||||
|
||||
return destinationGroups.some((group) => {
|
||||
@@ -294,9 +297,10 @@ export const useAccessControl = ({
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}, [destinationGroups, destinationResource]);
|
||||
}, [destinationGroups, destinationResource, isDestinationPeer]);
|
||||
|
||||
const destinationOnlyResources = useMemo(() => {
|
||||
if (isDestinationPeer) return false;
|
||||
if (destinationResource) return true;
|
||||
|
||||
return (
|
||||
@@ -318,13 +322,13 @@ export const useAccessControl = ({
|
||||
return hasResources && !hasPeers;
|
||||
})
|
||||
);
|
||||
}, [destinationGroups, destinationResource]);
|
||||
}, [destinationGroups, destinationResource, isDestinationPeer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (destinationOnlyResources && direction !== "in") {
|
||||
if (destinationOnlyResources && direction !== "in" && !isDestinationPeer) {
|
||||
setDirection("in");
|
||||
}
|
||||
}, [destinationOnlyResources, direction, setDirection]);
|
||||
}, [destinationOnlyResources, direction, setDirection, isDestinationPeer]);
|
||||
|
||||
return {
|
||||
protocol,
|
||||
|
||||
@@ -72,6 +72,8 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
|
||||
};
|
||||
|
||||
const removePeerFromGroup = async (g: Group) => {
|
||||
if (g.name === "All") return Promise.resolve(g);
|
||||
|
||||
const newPeerGroups = g.peers?.filter((p) => {
|
||||
const groupPeer = p as GroupPeer;
|
||||
return groupPeer.id !== peer?.id;
|
||||
|
||||
@@ -29,7 +29,7 @@ const NetworksContext = React.createContext(
|
||||
resource?: NetworkResource,
|
||||
) => void;
|
||||
openPolicyModal: (network?: Network, resource?: NetworkResource) => void;
|
||||
deleteNetwork: (network: Network) => void;
|
||||
deleteNetwork: (network: Network) => Promise<void>;
|
||||
deleteResource: (network: Network, resource: NetworkResource) => void;
|
||||
deleteRouter: (network: Network, router: NetworkRouter) => void;
|
||||
network?: Network;
|
||||
@@ -124,15 +124,19 @@ export const NetworkProvider = ({ children, network }: Props) => {
|
||||
|
||||
if (!choice) return;
|
||||
|
||||
const promise = deleteCall({}, `/${network.id}`).then(() => {
|
||||
mutate("/networks");
|
||||
mutate("/groups");
|
||||
});
|
||||
|
||||
notify({
|
||||
title: network.name,
|
||||
description: "Network deleted successfully.",
|
||||
loadingMessage: "Deleting network...",
|
||||
promise: deleteCall({}, `/${network.id}`).then(() => {
|
||||
mutate("/networks");
|
||||
mutate("/groups");
|
||||
}),
|
||||
promise,
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
const deleteResource = async (
|
||||
@@ -250,8 +254,9 @@ export const NetworkProvider = ({ children, network }: Props) => {
|
||||
mutate("/networks");
|
||||
await askForResource(network);
|
||||
}}
|
||||
onUpdated={() => {
|
||||
onUpdated={(n) => {
|
||||
mutate("/networks");
|
||||
mutate(`/networks/${n.id}`);
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
|
||||
@@ -50,7 +50,7 @@ export const NetworkInformationSquare = ({
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<div className={"mt-[0px] flex items-center flex-wrap"}>
|
||||
<div className={"mt-[0px] flex items-start flex-wrap flex-col"}>
|
||||
<p
|
||||
className={cn(
|
||||
"font-medium text-left whitespace-nowrap",
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function NetworkNameCell({ network }: Readonly<Props>) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"flex gap-4 items-center min-w-[300px] max-w-[300px]"}>
|
||||
<div className={"flex gap-4 items-center min-w-[300px] max-w-[450px]"}>
|
||||
<NetworkInformationSquare
|
||||
name={network.name}
|
||||
active={isActive}
|
||||
|
||||
111
src/modules/peer/PeerSSHInstructions.tsx
Normal file
111
src/modules/peer/PeerSSHInstructions.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import Button from "@components/Button";
|
||||
import Code from "@components/Code";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import {
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from "@components/modal/Modal";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import Separator from "@components/Separator";
|
||||
import Steps from "@components/Steps";
|
||||
import { Lightbox } from "@components/ui/Lightbox";
|
||||
import { Mark } from "@components/ui/Mark";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { ExternalLinkIcon, TerminalSquare } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import sshImage from "@/assets/ssh/ssh-client.png";
|
||||
|
||||
type Props = {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const PeerSSHInstructions = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
maxWidthClass={cn("relative", "max-w-2xl")}
|
||||
showClose={true}
|
||||
>
|
||||
<ModalHeader
|
||||
icon={<TerminalSquare size={16} className={"text-netbird"} />}
|
||||
title={"Enable SSH Access"}
|
||||
description={
|
||||
"Allow remote SSH access to this machine from other connected network participants. NetBird's embedded SSH server is running on port 44338."
|
||||
}
|
||||
color={"netbird"}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className={"px-8 py-3 flex flex-col gap-0 z-0"}>
|
||||
<Steps>
|
||||
<Steps.Step step={1}>
|
||||
<p className={"font-normal"}>
|
||||
If you are using NetBird via CLI, you can enable SSH by running
|
||||
</p>
|
||||
<Code codeToCopy={"netbird down"}>
|
||||
<Code.Line>{`netbird down # if NetBird is already running`}</Code.Line>
|
||||
</Code>
|
||||
<Code>
|
||||
<Code.Line>{`netbird up --allow-server-ssh`}</Code.Line>
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
|
||||
<Steps.Step step={2}>
|
||||
<p className={"font-normal"}>
|
||||
If you are using NetBird via the Desktop Client, click on the
|
||||
NetBird tray icon, go to <Mark>Settings</Mark> and click{" "}
|
||||
<Mark>Allow SSH</Mark> <br />
|
||||
</p>
|
||||
<Lightbox image={sshImage} />
|
||||
</Steps.Step>
|
||||
|
||||
<Steps.Step step={3} line={false}>
|
||||
<p className={"font-normal"}>
|
||||
Once the NetBird SSH server is allowed on the client, <br />
|
||||
click <Mark>Confirm & Enable</Mark> below to finish the setup.
|
||||
</p>
|
||||
</Steps.Step>
|
||||
</Steps>
|
||||
</div>
|
||||
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/ssh"}
|
||||
target={"_blank"}
|
||||
>
|
||||
SSH
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={onSuccess}
|
||||
data-cy={"create-setup-key"}
|
||||
>
|
||||
Confirm & Enable
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
47
src/modules/peer/PeerSSHToggle.tsx
Normal file
47
src/modules/peer/PeerSSHToggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { LockIcon, TerminalSquare } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { usePeer } from "@/contexts/PeerProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
|
||||
export const PeerSSHToggle = () => {
|
||||
const { permission } = usePermissions();
|
||||
const { peer, toggleSSH, setSSHInstructionsModal } = usePeer();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
className={"w-full block"}
|
||||
disabled={permission.peers.update}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
value={peer.ssh_enabled}
|
||||
disabled={!permission.peers.update}
|
||||
onChange={(enable) =>
|
||||
enable ? setSSHInstructionsModal(true) : toggleSSH(false)
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<TerminalSquare size={16} />
|
||||
SSH Access
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Enable the SSH server on this peer to access the machine via an secure shell."
|
||||
}
|
||||
/>
|
||||
</FullTooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -24,7 +24,8 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { ExitNodeDropdownButton } from "@/modules/exit-node/ExitNodeDropdownButton";
|
||||
|
||||
export default function PeerActionCell() {
|
||||
const { peer, deletePeer, update, openSSHDialog } = usePeer();
|
||||
const { peer, deletePeer, update, toggleSSH, setSSHInstructionsModal } =
|
||||
usePeer();
|
||||
const router = useRouter();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { permission } = usePermissions();
|
||||
@@ -48,21 +49,6 @@ export default function PeerActionCell() {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSSH = async () => {
|
||||
const text = peer.ssh_enabled ? "disabled" : "enabled";
|
||||
notify({
|
||||
title: `SSH Server is ${text}`,
|
||||
description: `The SSH Server for the peer ${peer.name} was successfully ${text}.`,
|
||||
promise: update({
|
||||
ssh: !peer.ssh_enabled,
|
||||
}).then(() => {
|
||||
mutate("/peers");
|
||||
mutate("/groups");
|
||||
}),
|
||||
loadingMessage: "Updating SSH access...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"flex justify-end pr-4 gap-3"}>
|
||||
<DropdownMenu modal={false}>
|
||||
@@ -118,10 +104,8 @@ export default function PeerActionCell() {
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
peer.ssh_enabled
|
||||
? toggleSSH()
|
||||
: openSSHDialog().then((enable) =>
|
||||
enable ? toggleSSH() : null,
|
||||
)
|
||||
? toggleSSH(false)
|
||||
: setSSHInstructionsModal(true)
|
||||
}
|
||||
disabled={!permission.peers.update}
|
||||
>
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { IconChevronDown } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
import { usePeer } from "@/contexts/PeerProvider";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
import { cn } from "@utils/helpers";
|
||||
|
||||
export const PeerConnectButton = () => {
|
||||
const { peer } = usePeer();
|
||||
@@ -50,7 +50,7 @@ export const PeerConnectButton = () => {
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"max-w-[200px] text-xs"}>
|
||||
Connecting via SSH or RDP is only available when the peer is online.
|
||||
Connecting via SSH or RDP is only available when the peer is online.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import MemoizedNetBirdIcon from "@components/ui/MemoizedNetBirdIcon";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { parseVersionString } from "@utils/version";
|
||||
import { trim } from "lodash";
|
||||
import { ArrowRightIcon, ArrowUpCircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
@@ -39,8 +38,6 @@ export default function PeerVersionCell({ version, os, serial }: Props) {
|
||||
return <ArrowUpCircleIcon size={15} className={"text-netbird"} />;
|
||||
}, []);
|
||||
|
||||
const isWasmClient = trim(os) === "js";
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col gap-1"}>
|
||||
{updateAvailable ? (
|
||||
@@ -114,7 +111,7 @@ export default function PeerVersionCell({ version, os, serial }: Props) {
|
||||
>
|
||||
<PeerOperatingSystemIcon os={os} />
|
||||
|
||||
{isWasmClient ? "Web Client" : os}
|
||||
{os}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
)}
|
||||
|
||||
@@ -259,10 +259,17 @@ export default function PeersTable({
|
||||
const [showBrowserPeers, setShowBrowserPeers] = useState(false);
|
||||
|
||||
const withBrowserPeers = useCallback(
|
||||
(condition: boolean) =>
|
||||
peers?.filter((peer) =>
|
||||
condition ? trim(peer.os) === "js" : trim(peer.os) !== "js",
|
||||
) ?? [],
|
||||
(condition: boolean) => {
|
||||
const isWebClient = (peer: Peer) => {
|
||||
return trim(peer?.os) == "js" || peer.kernel_version === "wasm";
|
||||
};
|
||||
|
||||
return (
|
||||
peers?.filter((peer) =>
|
||||
condition ? isWebClient(peer) : !isWebClient(peer),
|
||||
) ?? []
|
||||
);
|
||||
},
|
||||
[peers],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import * as React from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import Separator from "@components/Separator";
|
||||
import { IconLoader2 } from "@tabler/icons-react";
|
||||
import {
|
||||
ChevronsLeftRightEllipsis,
|
||||
ExternalLinkIcon,
|
||||
@@ -10,18 +15,13 @@ import {
|
||||
MonitorIcon,
|
||||
User2,
|
||||
} from "lucide-react";
|
||||
import Separator from "@components/Separator";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Button from "@components/Button";
|
||||
import { Label } from "@components/Label";
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Input } from "@components/Input";
|
||||
import * as React from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import {
|
||||
RDP_DOCS_LINK,
|
||||
RDPCredentials,
|
||||
} from "@/modules/remote-access/rdp/useRemoteDesktop";
|
||||
import { IconLoader2 } from "@tabler/icons-react";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -61,9 +61,31 @@ export const RDPCredentialsModal = ({
|
||||
|
||||
const handleConnect = useCallback(() => {
|
||||
if (hasAnyError || !onConnect) return;
|
||||
|
||||
let parsedUsername = username;
|
||||
let parsedDomain = "";
|
||||
|
||||
// Parse DOMAIN\username format
|
||||
if (username.includes("\\")) {
|
||||
const parts = username.split("\\");
|
||||
if (parts.length === 2) {
|
||||
parsedDomain = parts[0];
|
||||
parsedUsername = parts[1];
|
||||
}
|
||||
}
|
||||
// Parse username@domain format
|
||||
else if (username.includes("@")) {
|
||||
const parts = username.split("@");
|
||||
if (parts.length === 2) {
|
||||
parsedUsername = parts[0];
|
||||
parsedDomain = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
onConnect({
|
||||
username,
|
||||
username: parsedUsername,
|
||||
password,
|
||||
domain: parsedDomain,
|
||||
port: Number(port),
|
||||
});
|
||||
}, [hasAnyError, onConnect, username, password, port]);
|
||||
@@ -111,11 +133,12 @@ export const RDPCredentialsModal = ({
|
||||
<Label>Username & Password</Label>
|
||||
<HelpText>
|
||||
Enter the credentials required to authenticate with the remote
|
||||
host.
|
||||
host. For domain accounts, use DOMAIN\username or username@domain
|
||||
format.
|
||||
</HelpText>
|
||||
<div className={"flex flex-col gap-2 w-full"}>
|
||||
<Input
|
||||
placeholder={"Administrator"}
|
||||
placeholder={"Administrator or DOMAIN\\username"}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -29,7 +29,6 @@ export interface RDPSession {
|
||||
shutdown(): void;
|
||||
sendInput(input: unknown): void;
|
||||
onClipboardPaste?(content: ClipboardData): Promise<void>;
|
||||
inputHandler?: IronRDPInputHandler;
|
||||
}
|
||||
interface TerminationInfo {
|
||||
reason(): string;
|
||||
@@ -57,11 +56,6 @@ interface RDPConfig {
|
||||
declare global {
|
||||
interface Window {
|
||||
IronRDPBridge: IronRDPWASMBridge;
|
||||
IronRDPInputHandler?: new (
|
||||
ironrdp: IronRDPModule,
|
||||
session: RDPSession,
|
||||
canvas: HTMLCanvasElement,
|
||||
) => IronRDPInputHandler;
|
||||
initializeIronRDP: () => Promise<boolean>;
|
||||
onIronRDPReady?: () => void;
|
||||
createRDCleanPathProxy?: (
|
||||
@@ -70,9 +64,6 @@ declare global {
|
||||
) => Promise<string>;
|
||||
}
|
||||
}
|
||||
interface IronRDPInputHandler {
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
const IRON_RDP_PKG = "/ironrdp-pkg/ironrdp_web.js";
|
||||
|
||||
@@ -115,7 +106,8 @@ export class IronRDPWASMBridge {
|
||||
port: number,
|
||||
username: string,
|
||||
password: string,
|
||||
canvas: HTMLCanvasElement,
|
||||
domain?: string,
|
||||
canvas?: HTMLCanvasElement,
|
||||
enableClipboard = true,
|
||||
netbirdClient?: {
|
||||
createRDPProxy: (hostname: string, port: string) => Promise<string>;
|
||||
@@ -132,9 +124,9 @@ export class IronRDPWASMBridge {
|
||||
const config: RDPConfig = {
|
||||
username,
|
||||
password,
|
||||
domain: "",
|
||||
width: canvas.width || 1024,
|
||||
height: canvas.height || 768,
|
||||
domain: domain || "",
|
||||
width: canvas?.width || 1024,
|
||||
height: canvas?.height || 768,
|
||||
enable_tls: true,
|
||||
enable_credssp: true,
|
||||
enable_nla: true,
|
||||
@@ -177,9 +169,6 @@ export class IronRDPWASMBridge {
|
||||
builder.authToken("");
|
||||
const session = await builder.connect();
|
||||
this.sessions.set(sessionId, session);
|
||||
if (canvas) {
|
||||
this.attachInputHandler(session, canvas);
|
||||
}
|
||||
if (enableClipboard) {
|
||||
this.startClipboardEventListeners();
|
||||
}
|
||||
@@ -203,24 +192,7 @@ export class IronRDPWASMBridge {
|
||||
this.handleLocalClipboardRequest();
|
||||
});
|
||||
}
|
||||
private attachInputHandler(
|
||||
session: RDPSession,
|
||||
canvas: HTMLCanvasElement,
|
||||
): void {
|
||||
if (!window.IronRDPInputHandler) {
|
||||
console.warn("IronRDPInputHandler not loaded - input will not work");
|
||||
return;
|
||||
}
|
||||
if (!this.ironrdp) {
|
||||
console.warn("IronRDP module not available");
|
||||
return;
|
||||
}
|
||||
session.inputHandler = new window.IronRDPInputHandler(
|
||||
this.ironrdp,
|
||||
session,
|
||||
canvas,
|
||||
);
|
||||
}
|
||||
|
||||
private startSession(session: RDPSession, sessionId: string): void {
|
||||
session
|
||||
.run()
|
||||
@@ -234,9 +206,6 @@ export class IronRDPWASMBridge {
|
||||
});
|
||||
}
|
||||
private cleanupSession(session: RDPSession, sessionId: string): void {
|
||||
if (session.inputHandler) {
|
||||
session.inputHandler.destroy();
|
||||
}
|
||||
this.sessions.delete(sessionId);
|
||||
|
||||
// Stop clipboard event listeners if no active sessions
|
||||
@@ -244,12 +213,82 @@ export class IronRDPWASMBridge {
|
||||
this.stopClipboardEventListeners();
|
||||
}
|
||||
}
|
||||
private formatWSAError(wsaCode: number): string {
|
||||
const wsaDescriptions: Record<number, string> = {
|
||||
10004: "interrupted system call",
|
||||
10009: "bad file descriptor",
|
||||
10013: "permission denied",
|
||||
10014: "bad address",
|
||||
10022: "invalid argument",
|
||||
10024: "too many open files",
|
||||
10035: "resource temporarily unavailable",
|
||||
10036: "operation now in progress",
|
||||
10037: "operation already in progress",
|
||||
10038: "socket operation on nonsocket",
|
||||
10039: "destination address required",
|
||||
10040: "message too long",
|
||||
10041: "protocol wrong type for socket",
|
||||
10042: "bad protocol option",
|
||||
10043: "protocol not supported",
|
||||
10044: "socket type not supported",
|
||||
10045: "operation not supported",
|
||||
10046: "protocol family not supported",
|
||||
10047: "address family not supported by protocol family",
|
||||
10048: "address already in use",
|
||||
10049: "cannot assign requested address",
|
||||
10050: "network is down",
|
||||
10051: "network is unreachable",
|
||||
10052: "network dropped connection on reset",
|
||||
10053: "software caused connection abort",
|
||||
10054: "connection reset by peer",
|
||||
10055: "no buffer space available",
|
||||
10056: "socket is already connected",
|
||||
10057: "socket is not connected",
|
||||
10058: "cannot send after socket shutdown",
|
||||
10060: "connection timed out",
|
||||
10061: "connection refused",
|
||||
10064: "host is down",
|
||||
10065: "no route to host",
|
||||
10067: "too many processes",
|
||||
10091: "network subsystem is unavailable",
|
||||
10092: "Winsock version not supported",
|
||||
10093: "successful WSAStartup not yet performed",
|
||||
10101: "graceful shutdown in progress",
|
||||
10109: "class type not found",
|
||||
11001: "host not found",
|
||||
11002: "nonauthoritative host not found",
|
||||
11003: "this is a nonrecoverable error",
|
||||
11004: "valid name, no data record of requested type",
|
||||
};
|
||||
|
||||
return wsaDescriptions[wsaCode] || "unknown error";
|
||||
}
|
||||
|
||||
private formatRDCleanPathError(backtraceMsg: string): string {
|
||||
const wsaMatch = backtraceMsg.match(/WSA last error = (\d+)/);
|
||||
if (wsaMatch) {
|
||||
const wsaCode = parseInt(wsaMatch[1], 10);
|
||||
const description = this.formatWSAError(wsaCode);
|
||||
return `Connection failed: ${description} (WSA ${wsaCode})`;
|
||||
}
|
||||
|
||||
const httpMatch = backtraceMsg.match(/HTTP status code = (\d+)/);
|
||||
if (httpMatch) {
|
||||
return `Connection failed: HTTP ${httpMatch[1]}`;
|
||||
}
|
||||
|
||||
return backtraceMsg;
|
||||
}
|
||||
|
||||
private logIronError(error: unknown): void {
|
||||
const ironError = error as any;
|
||||
if (!ironError || !ironError.__wbg_ptr) return;
|
||||
try {
|
||||
if (ironError.backtrace) {
|
||||
console.error("IronRDP backtrace:", ironError.backtrace());
|
||||
const backtraceMsg = ironError.backtrace();
|
||||
const formattedMsg = this.formatRDCleanPathError(backtraceMsg);
|
||||
console.error("IronRDP error:", formattedMsg);
|
||||
console.debug("IronRDP backtrace:", backtraceMsg);
|
||||
}
|
||||
if (ironError.kind) {
|
||||
const errorKind = ironError.kind();
|
||||
@@ -269,13 +308,13 @@ export class IronRDPWASMBridge {
|
||||
console.error("Could not extract IronError details:", e);
|
||||
}
|
||||
}
|
||||
getSession(sessionId: string): RDPSession | null {
|
||||
return this.sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
disconnect(sessionId: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
if (session.inputHandler) {
|
||||
session.inputHandler.destroy();
|
||||
session.inputHandler = undefined;
|
||||
}
|
||||
if (session.shutdown) {
|
||||
session.shutdown();
|
||||
}
|
||||
|
||||
1089
src/modules/remote-access/rdp/useIronRDPInputHandler.ts
Normal file
1089
src/modules/remote-access/rdp/useIronRDPInputHandler.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useIronRDPInputHandler } from "./useIronRDPInputHandler";
|
||||
import {
|
||||
CertificatePromptInfo,
|
||||
useRDPCertificateHandler,
|
||||
@@ -14,6 +15,7 @@ interface RDPConfig {
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
domain?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
@@ -21,6 +23,7 @@ interface RDPConfig {
|
||||
export interface RDPCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
domain?: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
@@ -38,8 +41,7 @@ export enum RDPStatus {
|
||||
CONNECTING = 2,
|
||||
}
|
||||
|
||||
export const RDP_DOCS_LINK =
|
||||
"https://docs.netbird.io/how-to/browser-client#rdp-connection";
|
||||
export const RDP_DOCS_LINK = "https://docs.netbird.io/how-to/browser-client";
|
||||
|
||||
export const useRemoteDesktop = (client: any) => {
|
||||
const [status, setStatus] = useState(RDPStatus.DISCONNECTED);
|
||||
@@ -59,10 +61,20 @@ export const useRemoteDesktop = (client: any) => {
|
||||
reject: (reason?: any) => void;
|
||||
} | null>(null);
|
||||
|
||||
const [rdpSession, setRdpSession] = useState<any>(null);
|
||||
const [ironrdpModule, setIronrdpModule] = useState<any>(null);
|
||||
|
||||
const { handleRDCleanPathResponse, acceptCertificate } =
|
||||
useRDPCertificateHandler();
|
||||
const certificateAccepted = useRef(false);
|
||||
|
||||
const { isActive, focusCanvas } = useIronRDPInputHandler({
|
||||
ironrdp: ironrdpModule,
|
||||
session: rdpSession,
|
||||
canvas: canvasRef.current,
|
||||
isConnected: status === RDPStatus.CONNECTED,
|
||||
});
|
||||
|
||||
/**
|
||||
* Reset the RDP state, optionally preserving config and/or certificate state
|
||||
*/
|
||||
@@ -75,6 +87,9 @@ export const useRemoteDesktop = (client: any) => {
|
||||
) => {
|
||||
session.current = null;
|
||||
setStatus(RDPStatus.DISCONNECTED);
|
||||
setRdpSession(null);
|
||||
setIronrdpModule(null);
|
||||
|
||||
if (!options.preserveConfig) {
|
||||
setConfig(null);
|
||||
}
|
||||
@@ -172,11 +187,17 @@ export const useRemoteDesktop = (client: any) => {
|
||||
rdpConfig.port,
|
||||
rdpConfig.username,
|
||||
rdpConfig.password,
|
||||
rdpConfig.domain,
|
||||
canvas,
|
||||
true,
|
||||
client.client,
|
||||
);
|
||||
|
||||
// Store the ironrdp module and session for the input handler hook
|
||||
setIronrdpModule((client.ironRDPBridge as any).ironrdp || null);
|
||||
const actualSession = client.ironRDPBridge.getSession(sessionId);
|
||||
setRdpSession(actualSession);
|
||||
|
||||
session.current = {
|
||||
id: sessionId,
|
||||
disconnect: (options = {}) => {
|
||||
@@ -192,6 +213,7 @@ export const useRemoteDesktop = (client: any) => {
|
||||
};
|
||||
setStatus(RDPStatus.CONNECTED);
|
||||
lastConnectedConfigRef.current = rdpConfig;
|
||||
canvasRef?.current?.focus();
|
||||
return RDPStatus.CONNECTED;
|
||||
} catch (err) {
|
||||
const ironError = err as IronError;
|
||||
@@ -232,6 +254,7 @@ export const useRemoteDesktop = (client: any) => {
|
||||
setPendingCertificate(null);
|
||||
certificatePromiseRef.current = null;
|
||||
certificateAccepted.current = true;
|
||||
canvasRef?.current?.focus();
|
||||
},
|
||||
[pendingCertificate, acceptCertificate],
|
||||
);
|
||||
@@ -287,6 +310,7 @@ export const useRemoteDesktop = (client: any) => {
|
||||
await connect(newConfig);
|
||||
} finally {
|
||||
setIsResizing(false);
|
||||
canvasRef?.current?.focus();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
@@ -318,6 +342,10 @@ export const useRemoteDesktop = (client: any) => {
|
||||
session: session.current,
|
||||
canvasRef,
|
||||
|
||||
// Input handler
|
||||
inputHandlerActive: isActive,
|
||||
focusCanvas,
|
||||
|
||||
// Certificate handling
|
||||
pendingCertificate,
|
||||
acceptCertificatePrompt,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import Button from "@components/Button";
|
||||
import { DropdownMenuItem } from "@components/DropdownMenu";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { CircleHelpIcon, TerminalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { SSHCredentialsModal } from "@/modules/remote-access/ssh/SSHCredentialsModal";
|
||||
import { SSHTooltip } from "@/modules/remote-access/ssh/SSHTooltip";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
@@ -41,7 +41,8 @@ export const SSHButton = ({ peer, isDropdown = false }: Props) => {
|
||||
)}
|
||||
<div>
|
||||
<SSHTooltip
|
||||
disabled={!disabled}
|
||||
isOnline={peer.connected}
|
||||
isSSHEnabled={peer.ssh_enabled}
|
||||
hasPermission={hasPermission}
|
||||
side={isDropdown ? "left" : "top"}
|
||||
>
|
||||
|
||||
@@ -1,57 +1,99 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { usePeer } from "@/contexts/PeerProvider";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
hasPermission?: boolean;
|
||||
hasPermission: boolean;
|
||||
isOnline?: boolean;
|
||||
isSSHEnabled?: boolean;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
};
|
||||
export const SSHTooltip = ({
|
||||
disabled,
|
||||
children,
|
||||
hasPermission,
|
||||
isOnline,
|
||||
isSSHEnabled,
|
||||
side = "top",
|
||||
}: Props) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const tooltipContent = () => {
|
||||
if (!hasPermission) {
|
||||
return <NoPermissionText />;
|
||||
}
|
||||
if (!isSSHEnabled) {
|
||||
return <SSHDisabledText setShowTooltip={setShowTooltip} />;
|
||||
}
|
||||
if (!isOnline) {
|
||||
return <IsOfflineText />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<FullTooltip
|
||||
customOpen={showTooltip}
|
||||
customOnOpenChange={setShowTooltip}
|
||||
className={"w-full"}
|
||||
side={side}
|
||||
content={
|
||||
<div className={"max-w-xs text-xs flex flex-col gap-2"}>
|
||||
{hasPermission ? (
|
||||
<>
|
||||
<div>
|
||||
This peer is either offline or SSH access is not enabled.
|
||||
</div>
|
||||
<div>
|
||||
Please enable SSH access for this peer in the dashboard and make
|
||||
sure SSH is allowed in the NetBird Client under{" "}
|
||||
<span className={"text-white"}>Settings → Allow SSH</span>.
|
||||
</div>
|
||||
<div>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/ssh"}
|
||||
target={"_blank"}
|
||||
>
|
||||
SSH <ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
You do not have permission to launch the SSH console. Please
|
||||
contact your administrator.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
disabled={disabled}
|
||||
content={tooltipContent()}
|
||||
disabled={isOnline && isSSHEnabled && hasPermission}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const NoPermissionText = () => {
|
||||
return (
|
||||
<div className={"max-w-xs text-xs flex flex-col gap-2"}>
|
||||
<div>
|
||||
You do not have permission to launch the SSH console. Please contact
|
||||
your administrator.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const IsOfflineText = () => {
|
||||
return (
|
||||
<div className={"max-w-[200px] text-xs"}>
|
||||
<div>Connecting via SSH is only available when the peer is online.</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SSHDisabledText = ({
|
||||
setShowTooltip,
|
||||
}: {
|
||||
setShowTooltip: (show: boolean) => void;
|
||||
}) => {
|
||||
const { setSSHInstructionsModal } = usePeer();
|
||||
|
||||
return (
|
||||
<div className={"max-w-xs text-xs flex flex-col gap-2"}>
|
||||
<div>
|
||||
SSH Access is currently disabled for this peer. Please enable SSH access
|
||||
for this peer and make sure SSH is allowed in the NetBird Client.
|
||||
</div>
|
||||
<div>
|
||||
<InlineLink
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowTooltip(false);
|
||||
setSSHInstructionsModal(true);
|
||||
}}
|
||||
href={"#"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Enable SSH Access <ArrowUpRightIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useOidcAccessToken } from "@axa-fr/react-oidc";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
interface SSHConfig {
|
||||
@@ -28,6 +29,7 @@ export const useSSH = (client: any) => {
|
||||
const [config, setConfig] = useState<SSHConfig | null>(null);
|
||||
const session = useRef<SSHConnection | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const { accessToken } = useOidcAccessToken();
|
||||
|
||||
const connect = useCallback(
|
||||
async (config: SSHConfig): Promise<SSHStatus> => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getBrowserInfo } from "@utils/helpers";
|
||||
import { generateKeypair } from "@utils/wireguard";
|
||||
import { trim } from "lodash";
|
||||
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
||||
import { IronRDPInputHandler } from "@/modules/remote-access/rdp/ironrdp-input-handler";
|
||||
import { IronRDPWASMBridge } from "@/modules/remote-access/rdp/ironrdp-wasm-bridge";
|
||||
import { RDPCertificateHandler } from "@/modules/remote-access/rdp/rdp-certificate-handler";
|
||||
import { installWebSocketProxy } from "@/modules/remote-access/rdp/websocket-proxy";
|
||||
@@ -13,7 +12,7 @@ const config = loadConfig();
|
||||
|
||||
const WASM_CONFIG = {
|
||||
SCRIPT_PATH: "/wasm_exec.js",
|
||||
WASM_PATH: "https://pkgs.netbird.io/wasm/client",
|
||||
WASM_PATH: config.wasmPath,
|
||||
INIT_TIMEOUT: 10000,
|
||||
RETRY_DELAY: 100,
|
||||
} as const;
|
||||
@@ -73,9 +72,8 @@ export const useNetBirdClient = () => {
|
||||
|
||||
const rdpComponents = useRef<{
|
||||
bridge: IronRDPWASMBridge | null;
|
||||
inputHandler: typeof IronRDPInputHandler | null;
|
||||
certificateHandler: typeof RDPCertificateHandler | null;
|
||||
}>({ bridge: null, inputHandler: null, certificateHandler: null });
|
||||
}>({ bridge: null, certificateHandler: null });
|
||||
|
||||
const loadWASMRuntime = useCallback((): Promise<void> => {
|
||||
if (document.querySelector(`script[src="${WASM_CONFIG.SCRIPT_PATH}"]`)) {
|
||||
@@ -117,7 +115,6 @@ export const useNetBirdClient = () => {
|
||||
installWebSocketProxy();
|
||||
rdpComponents.current = {
|
||||
bridge: new IronRDPWASMBridge(),
|
||||
inputHandler: IronRDPInputHandler,
|
||||
certificateHandler: RDPCertificateHandler,
|
||||
};
|
||||
}, []);
|
||||
@@ -184,6 +181,7 @@ export const useNetBirdClient = () => {
|
||||
});
|
||||
|
||||
await netBirdClient.current.start();
|
||||
(window as any).netbird = netBirdClient.current;
|
||||
netBirdStore.setState({ status: NetBirdStatus.CONNECTED });
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -206,11 +204,27 @@ export const useNetBirdClient = () => {
|
||||
netBirdStore.setState({ status: NetBirdStatus.DISCONNECTED });
|
||||
await netBirdClient.current.stop();
|
||||
netBirdClient.current = null;
|
||||
delete (window as any).netbird;
|
||||
return Promise.resolve();
|
||||
}, []);
|
||||
|
||||
const detectSSHServerType = useCallback(
|
||||
async (host: string, port: number): Promise<boolean> => {
|
||||
if (!netBirdClient.current?.detectSSHServerType) {
|
||||
throw new Error("NetBird client not ready");
|
||||
}
|
||||
return netBirdClient.current.detectSSHServerType(host, port);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const createSSHConnection = useCallback(
|
||||
async (host: string, port: number, username: string): Promise<any> => {
|
||||
async (
|
||||
host: string,
|
||||
port: number,
|
||||
username: string,
|
||||
jwtToken?: string,
|
||||
): Promise<any> => {
|
||||
if (!netBirdClient.current?.createSSHConnection) {
|
||||
throw new Error("Go client not ready");
|
||||
}
|
||||
@@ -268,7 +282,7 @@ export const useNetBirdClient = () => {
|
||||
{
|
||||
name,
|
||||
wg_pub_key: keyPairs.publicKey,
|
||||
rules: rules ?? ["tcp/22", "tcp/3389", "tcp/44338"],
|
||||
rules: rules ?? ["tcp/22022", "tcp/3389", "tcp/44338"],
|
||||
},
|
||||
`/${peerId}/temporary-access`,
|
||||
);
|
||||
@@ -289,15 +303,15 @@ export const useNetBirdClient = () => {
|
||||
status,
|
||||
wasmStatus,
|
||||
error,
|
||||
client: netBirdClient.current, // Expose the raw NetBird client
|
||||
client: netBirdClient.current,
|
||||
ironRDPBridge: rdpComponents.current.bridge,
|
||||
ironRDPInputHandler: rdpComponents.current.inputHandler,
|
||||
rdpCertificateHandler: rdpComponents.current.certificateHandler,
|
||||
initialize,
|
||||
initializeIronRDP,
|
||||
connect,
|
||||
connectTemporary,
|
||||
disconnect,
|
||||
detectSSHServerType,
|
||||
createSSHConnection,
|
||||
makeRequest,
|
||||
proxyRequest,
|
||||
|
||||
@@ -79,23 +79,13 @@ export default function MacOSTab({
|
||||
</div>
|
||||
<div className={"flex gap-4 mt-1 flex-wrap"}>
|
||||
<Link
|
||||
href={"https://pkgs.netbird.io/macos/amd64"}
|
||||
href={"https://pkgs.netbird.io/macos/universal"}
|
||||
passHref
|
||||
target={"_blank"}
|
||||
>
|
||||
<Button variant={"primary"}>
|
||||
<DownloadIcon size={14} />
|
||||
Download for Intel
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href={"https://pkgs.netbird.io/macos/arm64"}
|
||||
passHref
|
||||
target={"_blank"}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
<DownloadIcon size={14} />
|
||||
Download for Apple Silicon
|
||||
Download NetBird
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Button from "@components/Button";
|
||||
import Code from "@components/Code";
|
||||
import { SelectDropdown } from "@components/select/SelectDropdown";
|
||||
import Steps from "@components/Steps";
|
||||
import TabsContentPadding, { TabsContent } from "@components/Tabs";
|
||||
import { getNetBirdUpCommand, GRPC_API_ORIGIN } from "@utils/netbird";
|
||||
import { DownloadIcon, PackageOpenIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import {
|
||||
HostnameParameter,
|
||||
@@ -24,6 +25,9 @@ export default function WindowsTab({
|
||||
showSetupKeyInfo,
|
||||
hostname,
|
||||
}: Readonly<Props>) {
|
||||
const [windowsUrl, setWindowsUrl] = useState(
|
||||
"https://pkgs.netbird.io/windows/x64",
|
||||
);
|
||||
return (
|
||||
<TabsContent value={String(OperatingSystem.WINDOWS)}>
|
||||
<TabsContentPadding>
|
||||
@@ -35,10 +39,35 @@ export default function WindowsTab({
|
||||
<Steps.Step step={1}>
|
||||
<p>Download and run Windows Installer</p>
|
||||
<div className={"flex gap-4 mt-1"}>
|
||||
<SelectDropdown
|
||||
value={windowsUrl}
|
||||
className={"w-[170px]"}
|
||||
onChange={setWindowsUrl}
|
||||
placeholder={"Select architecture"}
|
||||
options={[
|
||||
{
|
||||
label: "64-Bit",
|
||||
value: "https://pkgs.netbird.io/windows/x64",
|
||||
},
|
||||
{
|
||||
label: "ARM64",
|
||||
value: "https://pkgs.netbird.io/windows/arm64",
|
||||
},
|
||||
{
|
||||
label: "64-Bit (MSI)",
|
||||
value: "https://pkgs.netbird.io/windows/msi/x64",
|
||||
},
|
||||
{
|
||||
label: "ARM64 (MSI)",
|
||||
value: "https://pkgs.netbird.io/windows/msi/arm64",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Link
|
||||
href={"https://pkgs.netbird.io/windows/x64"}
|
||||
href={windowsUrl}
|
||||
passHref
|
||||
target={"_blank"}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant={"primary"}>
|
||||
<DownloadIcon size={14} />
|
||||
|
||||
@@ -17,6 +17,7 @@ interface Config {
|
||||
hotjarTrackID?: number;
|
||||
googleAnalyticsID?: string;
|
||||
googleTagManagerID?: string;
|
||||
wasmPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +67,7 @@ const loadConfig = (): Config => {
|
||||
hotjarTrackID: configJson?.hotjarTrackID || undefined,
|
||||
googleAnalyticsID: configJson?.googleAnalyticsID || undefined,
|
||||
googleTagManagerID: configJson?.googleTagManagerID || undefined,
|
||||
wasmPath: configJson.wasmPath ?? "https://pkgs.netbird.io/wasm/client",
|
||||
} as Config;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user