Compare commits

...
16 Commits
Author SHA1 Message Date
Eduard Gert bf81aeb02d Add fine-grained ssh policy (#522)
* Add fine-grained ssh policy

* Update version text

* Fix coderabbit comment
2025-12-30 09:27:17 +01:00
Eduard Gert b058e66e32 Add auto update setting (#519) 2025-12-29 12:38:50 +01:00
Eduard Gert 8d6b617cbd Update NextJS to 14.2.35 (#518) 2025-12-22 11:02:29 +01:00
Eduard Gert 47db655e9f Update eslint and tailwind (#515) 2025-11-27 17:38:18 +01:00
dependabot[bot] 0661cbf9f4 Bump js-yaml from 4.1.0 to 4.1.1 (#509)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 15:25:50 +01:00
Eduard Gert 240a96fa8b Add onboarding for new accounts (#514) 2025-11-27 14:49:58 +01:00
Eduard Gert 43bc069a49 Increase ssh detection timeout (#512) 2025-11-21 10:32:50 +01:00
Eduard Gert 936de0f4f3 Add ssh policy info for peers (#511) 2025-11-20 14:29:14 +01:00
Eduard Gert d81b75a946 Bump browser ssh versions for ssh rewrite (#510)
* Bump browser ssh versions for ssh rewrite

* Remove cypress temporary
2025-11-18 17:07:58 +01:00
Eduard Gert a632eeeef0 Remove dns0eu (#508) 2025-11-10 14:21:58 +01:00
Eduard Gert e2219aeea0 Add group update activity event (#504) 2025-11-10 10:50:04 +01:00
Eduard Gert 63f4c69eb4 Fix native ssh detection (#505) 2025-11-07 09:33:58 +01:00
Eduard Gert b1af256296 Add wasm client version (#503) 2025-11-06 10:59:41 +01:00
Eduard Gertandaliamerj 4027894a2e Feature/groups page (#498)
* move our group membership from the settings menu, into the Team menu

* add action to the table and new group page

* update group page and return group settings to settings menu

* new update

* fix bug

* group action: add peer to group

* group action: add user to group

* Update wording, redirect to group page after creation

* Add better table loading skeleton

* Adjust group name cell

* Update wording

* Update sort order

* Refactor

* Merge main

* Fix button height

* Fix resources table

* Adjust table loading skeleton

* Adjust table loading skeleton

* Add loading to tab triggers

* Update meta

* Update group location

* Fix rename

* Refactor group details

* Fix linked peers

* Fix group usage

* Fix incrementing peer count

* Prevent renaming to already existing group

* Fix group name click

* Update group nav

* Make group table cells clickable

* Fix breadcrumbs

* Update wording

* Add confirmation before removing users from group

* Add permissions

* Add initial group for network routes

* Add acl and routing peer groups

---------

Co-authored-by: aliamerj <aliamer19ali@gmail.com>
2025-11-05 12:08:49 +01:00
Yanis64andEduard Gert af90792595 Add multi-group support for JWT allow groups with tag system (#500)
* fix: add multi-group support for JWT allow groups with tag system

* Update src/modules/settings/GroupsTab.tsx to use the Badge component

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>

* chore(GroupsTab): import Badge components

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2025-11-03 16:09:15 +01:00
Eduard Gert 9a401733b3 Fix toggle for p2p policies (#501) 2025-10-31 13:21:23 +01:00
159 changed files with 11401 additions and 4163 deletions

No files matched your search

-1
View File
@@ -2,7 +2,6 @@ name: build and push
on:
push:
branches:
- "feature/**"
- main
tags:
- "**"
+1 -1
View File
@@ -61,7 +61,7 @@ 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}
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH}
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
+2444 -2846
View File
File diff suppressed because it is too large. Load diff
+4 -5
View File
@@ -56,7 +56,6 @@
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"elkjs": "^0.10.0",
"eslint": "^8",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"flowbite": "^1.8.1",
@@ -66,7 +65,7 @@
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"lucide-react": "^0.539.0",
"next": "^14.2.28",
"next": "^14.2.35",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18.3.1",
@@ -90,10 +89,10 @@
"@faker-js/faker": "^9.5.1",
"@types/chroma-js": "^3.1.1",
"@types/js-cookie": "^3.0.6",
"eslint-config-next": "^14.2.28",
"cypress": "^13.13.0",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.5",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"
"tailwindcss": "^3.4.17"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ export default function AccessControlPage() {
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/policies"}
href={"/access-control"}
label={"Access Control"}
icon={<AccessControlIcon size={14} />}
/>
+1 -1
View File
@@ -32,7 +32,7 @@ export default function NameServers() {
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/dns"}
href={"/dns/nameservers"}
label={"DNS"}
icon={<DNSIcon size={13} />}
/>
+8
View File
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Group - ${globalMetaTitle}`,
};
export default BlankLayout;
+315
View File
@@ -0,0 +1,315 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import FullTooltip from "@components/FullTooltip";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { GroupBadgeIcon } from "@components/ui/GroupBadgeIcon";
import { PageNotFound } from "@components/ui/PageNotFound";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn, singularize } from "@utils/helpers";
import { FolderGit2Icon, Layers3Icon, PencilIcon } from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useState } from "react";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
import DNSIcon from "@/assets/icons/DNSIcon";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import TeamIcon from "@/assets/icons/TeamIcon";
import { GroupProvider, useGroupContext } from "@/contexts/GroupProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { Group, GROUP_TOOLTIP_TEXT } from "@/interfaces/Group";
import PageContainer from "@/layouts/PageContainer";
import { GroupNameserversSection } from "@/modules/groups/details/GroupNameserversSection";
import { GroupNetworkRoutesSection } from "@/modules/groups/details/GroupNetworkRoutesSection";
import { GroupPeersSection } from "@/modules/groups/details/GroupPeersSection";
import { GroupPoliciesSection } from "@/modules/groups/details/GroupPoliciesSection";
import { GroupResourcesSection } from "@/modules/groups/details/GroupResourcesSection";
import { GroupSetupKeysSection } from "@/modules/groups/details/GroupSetupKeysSection";
import { GroupUsersSection } from "@/modules/groups/details/GroupUsersSection";
import useGroupDetails from "@/modules/groups/details/useGroupDetails";
export default function GroupPage() {
const queryParameter = useSearchParams();
const { isRestricted } = usePermissions();
const groupId = queryParameter.get("id");
const {
data: group,
isLoading,
error,
} = useFetchApi<Group>(`/groups/${groupId}`, true);
useRedirect("/groups", false, !groupId || isRestricted);
if (isRestricted) {
return (
<PageContainer>
<RestrictedAccess page={"Group Information"} />
</PageContainer>
);
}
if (error)
return (
<PageNotFound
title={error?.message}
description={
"The group you are attempting to access cannot be found. It may have been deleted, or you may not have permission to view it. Please verify the URL or return to the dashboard."
}
/>
);
return group && !isLoading ? (
<PageContainer>
<RoutesProvider>
<GroupProvider group={group} isDetailPage={true}>
<div className={"p-default py-6 pb-0 w-full mb-[6px]"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/groups"}
label={"Groups"}
icon={<FolderGit2Icon size={14} />}
/>
<Breadcrumbs.Item label={group.name} active />
</Breadcrumbs>
<GroupDetailsName />
</div>
<GroupOverviewTabs group={group} />
</GroupProvider>
</RoutesProvider>
</PageContainer>
) : (
<FullScreenLoading />
);
}
const GroupDetailsName = () => {
const { group, isJWTGroup, isAllowedToRename, openGroupRenameModal } =
useGroupContext();
const { permission } = usePermissions();
return (
<div className={"w-full"}>
<h1 className={"flex items-center gap-3 w-full whitespace-nowrap"}>
<GroupBadgeIcon id={group?.id} issued={group?.issued} size={20} />
{group.name}
{group.name !== "All" && permission?.groups?.update && (
<div>
<FullTooltip
content={
<div className={"text-xs max-w-xs"}>
{isJWTGroup
? GROUP_TOOLTIP_TEXT.RENAME.JWT
: GROUP_TOOLTIP_TEXT.RENAME.INTEGRATION}
</div>
}
interactive={false}
disabled={isAllowedToRename}
className={"w-full block"}
>
<div
className={cn(
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer",
!isAllowedToRename &&
"opacity-40 cursor-not-allowed pointer-events-none",
)}
onClick={openGroupRenameModal}
>
<PencilIcon size={16} />
</div>
</FullTooltip>
</div>
)}
</h1>
</div>
);
};
const validAllGroupTabs = [
"policies",
"resources",
"network-routes",
"nameservers",
];
const validOtherGroupTabs = ["users", "peers", "setup-keys"];
const GroupOverviewTabs = ({ group }: { group: Group }) => {
const searchParams = useSearchParams();
const getInitialTab = () => {
const isAllGroup = group.name === "All";
const tabParam = searchParams.get("tab");
const validTabs = isAllGroup
? validAllGroupTabs
: [...validAllGroupTabs, ...validOtherGroupTabs];
if (tabParam === null) return isAllGroup ? "policies" : "users";
if (isAllGroup) {
return validTabs.includes(tabParam) ? tabParam : "policies";
}
return validTabs.includes(tabParam) ? tabParam : "users";
};
const [tab, setTab] = useState(getInitialTab());
const { groupDetails, isLoading } = useGroupDetails(group?.id || "");
const peersCount = groupDetails?.peers_count || 0;
const usersCount = groupDetails?.users?.length || 0;
const policiesCount = groupDetails?.policies?.length || 0;
const resourcesCount = groupDetails?.resources_count || 0;
const routesCount = groupDetails?.routes?.length || 0;
const nameserversCount = groupDetails?.nameservers?.length || 0;
const setupKeysCount = groupDetails?.setupKeys?.length || 0;
return (
<Tabs
defaultValue={tab}
onValueChange={(v) => setTab(v)}
value={tab}
className={"pt-2 pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"}>
{group.name !== "All" && (
<TabsTrigger
value={"users"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<TeamIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Users", usersCount)}
</TabsTrigger>
)}
{group.name !== "All" && (
<TabsTrigger
value={"peers"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<PeerIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Peers", peersCount)}
</TabsTrigger>
)}
<TabsTrigger
value={"policies"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<AccessControlIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Policies", policiesCount)}
</TabsTrigger>
<TabsTrigger
value={"resources"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<Layers3Icon size={14} />
{singularize("Resources", resourcesCount)}
</TabsTrigger>
<TabsTrigger
value={"network-routes"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<NetworkRoutesIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Network Routes", routesCount)}
</TabsTrigger>
<TabsTrigger
value={"nameservers"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<DNSIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Nameservers", nameserversCount)}
</TabsTrigger>
{group.name !== "All" && (
<TabsTrigger
value={"setup-keys"}
className={groupDetails === null ? "animate-pulse" : ""}
>
<SetupKeysIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Setup Keys", setupKeysCount)}
</TabsTrigger>
)}
</TabsList>
<TabsContent value={"users"} className={"pb-8"}>
<GroupUsersSection users={groupDetails?.users} isLoading={isLoading} />
</TabsContent>
<TabsContent value={"peers"} className={"pb-8"}>
<GroupPeersSection
peers={groupDetails?.peersOfGroup}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"policies"} className={"pb-8"}>
<GroupPoliciesSection
policies={groupDetails?.policies}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"resources"} className={"pb-8"}>
<GroupResourcesSection
resources={groupDetails?.networkResources}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"network-routes"} className={"pb-8"}>
<GroupNetworkRoutesSection
routes={groupDetails?.routes}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"nameservers"} className={"pb-8"}>
<GroupNameserversSection
nameserverGroups={groupDetails?.nameservers}
isLoading={isLoading}
/>
</TabsContent>
<TabsContent value={"setup-keys"} className={"pb-8"}>
<GroupSetupKeysSection
setupKeys={groupDetails?.setupKeys}
isLoading={isLoading}
/>
</TabsContent>
</Tabs>
);
};
+8
View File
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Groups - ${globalMetaTitle}`,
};
export default BlankLayout;
+56
View File
@@ -0,0 +1,56 @@
"use client";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import { ExternalLinkIcon, FolderGit2Icon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import Breadcrumbs from "@/components/Breadcrumbs";
import InlineLink from "@/components/InlineLink";
import { usePermissions } from "@/contexts/PermissionsProvider";
import PageContainer from "@/layouts/PageContainer";
const GroupsTable = lazy(() => import("@/modules/groups/table/GroupsTable"));
export default function GroupsPage() {
const { permission } = usePermissions();
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/groups"}
label={"Groups"}
icon={<FolderGit2Icon size={14} />}
active
/>
</Breadcrumbs>
<h1 ref={headingRef}>Groups</h1>
<Paragraph>
Here is the overview of the groups of your organization. You can
delete the unused ones.
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/manage-network-access"}
target={"_blank"}
>
Groups
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess hasAccess={permission.groups.read} page={"Groups"}>
<Suspense fallback={<SkeletonTable />}>
<GroupsTable headingTarget={portalTarget} />
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}
+13 -1
View File
@@ -7,7 +7,7 @@ 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";
@@ -16,6 +16,7 @@ import RoutesProvider from "@/contexts/RoutesProvider";
import { Route } from "@/interfaces/Route";
import PageContainer from "@/layouts/PageContainer";
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
import { Callout } from "@components/Callout";
const NetworkRoutesTable = lazy(
() => import("@/modules/route-group/NetworkRoutesTable"),
@@ -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}>
+18 -4
View File
@@ -30,6 +30,7 @@ import { cn } from "@utils/helpers";
import dayjs from "dayjs";
import { isEmpty, trim } from "lodash";
import {
ArrowRightIcon,
Barcode,
CalendarDays,
Cpu,
@@ -65,6 +66,7 @@ import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSectio
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
import Link from "next/link";
export default function PeerPage() {
const queryParameter = useSearchParams();
@@ -148,7 +150,7 @@ const PeerGeneralInformation = () => {
);
const [selectedGroups, setSelectedGroups, { getAllGroupCalls }] =
useGroupHelper({
initial: peerGroups,
initial: peerGroups?.filter((g) => g?.name !== "All"),
peer,
});
@@ -237,9 +239,21 @@ const PeerGeneralInformation = () => {
</h1>
<LoginExpiredBadge loginExpired={peer.login_expired} />
</div>
<div className={"flex items-center gap-8"}>
<Paragraph className={"flex items-center"}>{user?.email}</Paragraph>
</div>
{(user?.id || user?.email) && (
<div className={"flex items-center gap-8"}>
<Paragraph className={"flex items-center"}>
<Link
href={`/team/user?id=${user?.id}`}
className={
"hover:text-nb-gray-200 transition-all flex items-center gap-1"
}
>
{user?.email || user?.id}
<ArrowRightIcon size={14} />
</Link>
</Paragraph>
</div>
)}
</div>
<div className={"flex gap-4"}>
<Button
+2 -2
View File
@@ -19,9 +19,9 @@ import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
import ClientSettingsTab from "@/modules/settings/ClientSettingsTab";
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
import GroupsTab from "@/modules/settings/GroupsTab";
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
import PermissionsTab from "@/modules/settings/PermissionsTab";
import GroupsSettings from "@/modules/settings/GroupsSettings";
export default function NetBirdSettings() {
const queryParams = useSearchParams();
@@ -81,7 +81,7 @@ export default function NetBirdSettings() {
<div className={"border-l border-nb-gray-930 w-full"}>
{account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />}
{account && <GroupsTab account={account} />}
{account && <GroupsSettings account={account} />}
{account && <NetworkSettingsTab account={account} />}
{account && <ClientSettingsTab account={account} />}
{account && <DangerZoneTab account={account} />}
+85 -37
View File
@@ -9,6 +9,7 @@ import { notify } from "@components/Notification";
import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useRedirect from "@hooks/useRedirect";
@@ -16,7 +17,15 @@ import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api";
import { generateColorFromString } from "@utils/helpers";
import dayjs from "dayjs";
import { Ban, GalleryHorizontalEnd, History, Mail, User2 } from "lucide-react";
import {
Ban,
GalleryHorizontalEnd,
History,
KeyRoundIcon,
Mail,
MonitorSmartphoneIcon,
User2,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
@@ -33,6 +42,7 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
import { UserPeersSection } from "@/modules/users/UserPeersSection";
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
export default function UserPage() {
@@ -80,6 +90,7 @@ type Props = {
function UserOverview({ user, initialGroups }: Readonly<Props>) {
const router = useRouter();
const userRequest = useApiCall<User>("/users");
const isServiceUser = !!user?.is_service_user;
const { mutate } = useSWRConfig();
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
@@ -91,7 +102,6 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
});
const [role, setRole] = useState(user.role || Role.User);
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
role,
selectedGroups,
@@ -114,13 +124,24 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
`/${user.id}`,
)
.then(() => {
mutate(`/users?service_user=${user.is_service_user}`);
mutate(`/users?service_user=${isServiceUser}`);
updateChangesRef([role, selectedGroups]);
}),
loadingMessage: "Saving changes...",
});
};
const isProfilePage = !!user?.is_current && !isServiceUser;
const canViewTokens = permission?.pats?.read;
const canViewPeers = permission?.peers?.read;
const showAccessTokens = (user?.is_current || isServiceUser) && canViewTokens;
const showPeers = !isServiceUser && canViewPeers;
const showTabs = isProfilePage && showPeers && showAccessTokens;
const showSeparator = !showTabs;
const [tab, setTab] = useState(isServiceUser ? "access-tokens" : "peers");
return (
<PageContainer>
<div className={"p-default py-6 mb-4"}>
@@ -132,7 +153,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
icon={<TeamIcon size={13} />}
/>
{user.is_service_user ? (
{isServiceUser ? (
<Breadcrumbs.Item
href={"/team/service-users"}
label={"Service Users"}
@@ -158,7 +179,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
"w-10 h-10 rounded-full relative flex items-center justify-center text-white uppercase text-md font-medium bg-nb-gray-900"
}
style={
user.is_service_user
isServiceUser
? {
color: "white",
}
@@ -171,13 +192,13 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
}
}
>
{user.is_service_user ? (
{isServiceUser ? (
<IconSettings2 size={16} />
) : (
user?.name?.charAt(0) || user?.id?.charAt(0)
)}
</div>
<h1 className={"flex items-center gap-3"}>
<h1 className={"flex items-center gap-3"} title={user?.id}>
{user.name || user.id}
</h1>
</div>
@@ -188,7 +209,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
variant={"default"}
className={"w-full"}
onClick={() => {
user.is_service_user
isServiceUser
? router.push("/team/service-users")
: router.push("/team/users");
}}
@@ -212,7 +233,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<UserInformationCard user={user} />
<div className={"flex flex-col gap-8 w-1/2 "}>
{!user.is_service_user && isOwnerOrAdmin && (
{!isServiceUser && isOwnerOrAdmin && (
<div>
<Label>Auto-assigned groups</Label>
<HelpText>
@@ -238,7 +259,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
<UserRoleSelector
value={role}
onChange={setRole}
hideOwner={user.is_service_user}
hideOwner={isServiceUser}
currentUser={user}
disabled={isLoggedInUser || !permission.users.update}
/>
@@ -248,38 +269,65 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
</div>
</div>
{(user.is_current || user.is_service_user) && permission.pats.read && (
<>
<Separator />
<div className={"px-8 py-6"}>
<div className={"max-w-6xl"}>
<div className={"flex justify-between items-center"}>
<div>
<h2>Access Tokens</h2>
<Paragraph>
Access tokens give access to NetBird API.
</Paragraph>
</div>
<div className={"inline-flex gap-4 justify-end"}>
{showSeparator && <Separator />}
<Tabs
defaultValue={tab}
onValueChange={setTab}
value={tab}
className={"pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"} hidden={!showTabs}>
{showPeers && (
<TabsTrigger value={"peers"}>
<MonitorSmartphoneIcon size={16} />
Peers
</TabsTrigger>
)}
{showAccessTokens && (
<TabsTrigger value={"access-tokens"}>
<KeyRoundIcon size={16} />
Access Tokens
</TabsTrigger>
)}
</TabsList>
{showPeers && (
<TabsContent value={"peers"} className={"pb-8"}>
<UserPeersSection user={user} />
</TabsContent>
)}
{showAccessTokens && (
<TabsContent value={"access-tokens"} className={"pb-8"}>
<div className={"px-8"}>
<div className={"max-w-6xl"}>
<div className={"flex justify-between items-center"}>
<div>
<CreateAccessTokenModal user={user}>
<Button
variant={"primary"}
data-cy={"access-token-open-modal"}
disabled={!permission.pats.create}
>
<IconCirclePlus size={16} />
Create Access Token
</Button>
</CreateAccessTokenModal>
<h2>Access Tokens</h2>
<Paragraph>
Access tokens give access to NetBird API.
</Paragraph>
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div>
<CreateAccessTokenModal user={user}>
<Button
variant={"primary"}
data-cy={"access-token-open-modal"}
disabled={!permission.pats.create}
>
<IconCirclePlus size={16} />
Create Access Token
</Button>
</CreateAccessTokenModal>
</div>
</div>
</div>
<AccessTokensTable user={user} />
</div>
<AccessTokensTable user={user} />
</div>
</div>
</>
)}
</TabsContent>
)}
</Tabs>
</PageContainer>
);
}
+9 -3
View File
@@ -4,7 +4,6 @@ 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";
@@ -20,6 +19,8 @@ import {
NetBirdStatus,
useNetBirdClient,
} from "@/modules/remote-access/useNetBirdClient";
import { cn } from "@utils/helpers";
import { isNetbirdSSHProtocolSupported } from "@utils/version";
export default function RDPPage() {
const { peerId } = useRDPQueryParams();
@@ -31,7 +32,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} />
) : (
@@ -84,7 +85,12 @@ function RDPSession({ peer }: Props) {
try {
setCredentials(rdpCredentials);
setIsNetBirdConnecting(true);
await client.connectTemporary(peer.id, [`tcp/${rdpCredentials.port}`]);
const protocol = isNetbirdSSHProtocolSupported(peer.version)
? "netbird-ssh"
: "tcp";
await client.connectTemporary(peer.id, [
`${protocol}/${rdpCredentials.port}`,
]);
setIsNetBirdConnecting(false);
} catch (error) {
sendErrorNotification(
+14 -4
View File
@@ -2,7 +2,6 @@
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";
@@ -13,6 +12,10 @@ import {
NetBirdStatus,
useNetBirdClient,
} from "@/modules/remote-access/useNetBirdClient";
import {
isNativeSSHSupported,
isNetbirdSSHProtocolSupported,
} from "@utils/version";
export default function SSHPage() {
const { peerId, username, port } = useSSHQueryParams();
@@ -88,7 +91,10 @@ function SSHTerminal({ username, port, peer }: Props) {
connected.current = false;
try {
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
const rules = [`tcp/${aclPort}`];
const protocol = isNetbirdSSHProtocolSupported(peer.version)
? "netbird-ssh"
: "tcp";
const rules = [`${protocol}/${aclPort}`];
await client?.connectTemporary(peer.id, rules);
await ssh({
hostname: peer.ip,
@@ -108,9 +114,13 @@ function SSHTerminal({ username, port, peer }: Props) {
if (!peer.id) return;
if (connected.current) return;
connected.current = true;
try {
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
const rules = [`tcp/${aclPort}`];
const protocol = isNetbirdSSHProtocolSupported(peer.version)
? "netbird-ssh"
: "tcp";
const rules = [`${protocol}/${aclPort}`];
await client?.connectTemporary(peer.id, rules);
const res = await ssh({
hostname: peer.ip,
@@ -121,7 +131,7 @@ function SSHTerminal({ username, port, peer }: Props) {
sshConnectedOnce.current = true;
}
} catch (error) {
console.error("Connection failed:", error);
console.error("Connection error:", error);
}
};
+19
View File
@@ -0,0 +1,19 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function JumpcloudIcon(props: Readonly<IconProps>) {
return (
<svg
width="167"
height="82"
viewBox="0 0 167 82"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
>
<path
d="M166.911 58.3592C166.911 64.3815 164.519 70.1571 160.26 74.4155C156.002 78.6739 150.226 81.0662 144.204 81.0662H137.961C137.31 73.4972 129.5 67.0612 118.46 64.0722C121.244 61.3253 123.148 57.8124 123.931 53.9803C124.713 50.1482 124.338 46.17 122.854 42.5515C121.369 38.933 118.842 35.8378 115.594 33.6594C112.345 31.481 108.522 30.3178 104.611 30.3178C100.7 30.3178 96.8772 31.481 93.6289 33.6594C90.3805 35.8378 87.8534 38.933 86.3689 42.5515C84.8843 46.17 84.5094 50.1482 85.2918 53.9803C86.0743 57.8124 87.9786 61.3253 90.7628 64.0722C85.5111 65.3278 80.6301 67.8055 76.5167 71.3037C73.9207 69.8152 71.1411 68.6726 68.2487 67.9049C70.6422 65.5587 72.2829 62.5529 72.9614 59.2707C73.6399 55.9884 73.3255 52.5784 72.0584 49.4755C70.7913 46.3726 68.6288 43.7174 65.8467 41.8484C63.0646 39.9793 59.7888 38.9812 56.4372 38.9812C53.0855 38.9812 49.8098 39.9793 47.0277 41.8484C44.2455 43.7174 42.0831 46.3726 40.816 49.4755C39.5488 52.5784 39.2345 55.9884 39.913 59.2707C40.5915 62.5529 42.2321 65.5587 44.6257 67.9049C35.9237 70.3154 29.5841 75.1364 28.2342 80.9698H21.991C16.0936 80.7777 10.502 78.2999 6.39821 74.0603C2.2944 69.8206 0 64.1513 0 58.2508C0 52.3503 2.2944 46.681 6.39821 42.4413C10.502 38.2016 16.0936 35.7238 21.991 35.5317C24.8814 35.5419 27.7438 36.0981 30.4278 37.1709C32.2478 33.2162 35.1686 29.8695 38.8407 27.5312C42.5128 25.1928 46.7807 23.9618 51.1341 23.9854C51.6885 23.9854 52.2429 23.9854 52.7732 23.9854C53.9093 18.1059 56.8018 12.7093 61.0689 8.50798C65.336 4.30669 70.7769 1.49837 76.6733 0.453829C82.5698 -0.590709 88.6443 0.177651 94.095 2.65746C99.546 5.13728 104.116 9.21191 107.203 14.3434C110.733 13.2708 114.463 13.023 118.104 13.6193C121.746 14.2155 125.202 15.6397 128.206 17.7822C131.21 19.9247 133.682 22.7283 135.432 25.977C137.182 29.2257 138.162 32.8326 138.298 36.52C141.665 35.6031 145.198 35.4762 148.622 36.1492C152.046 36.8222 155.269 38.277 158.038 40.4001C160.808 42.5233 163.049 45.2574 164.588 48.3892C166.127 51.5211 166.922 54.9661 16Line truncated
fill="#4CC2BF"
/>
</svg>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function OIDCIcon(props: Readonly<IconProps>) {
return (
<svg
width="173"
height="174"
viewBox="0 0 173 174"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
>
<path
d="M76.3945 173.48L103.325 154.065L102.072 0L76.3945 20.041V173.48Z"
fill="#FF8E00"
/>
<path
d="M76.7077 173.48C-24.0221 157.466 -26.8926 69.7689 76.0814 50.7288L76.3945 68.8909C3.35034 81.0694 12.6045 146.598 76.3945 156.257L76.7077 173.48Z"
fill="white"
/>
<path
d="M103.011 68.2646C115.468 68.3493 126.32 74.0515 137.144 79.8508L121.174 91.7502H172.216L172.529 56.9916L156.558 68.8909C140.397 60.7278 125.542 50.9315 103.011 50.7288V68.2646Z"
fill="white"
/>
</svg>
);
}
-12
View File
@@ -1,12 +0,0 @@
<svg width="573" height="148" viewBox="0 0 573 148" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_4)">
<path d="M0.739014 125.602V33.09H28.239C34.491 33.09 39.919 34.274 44.524 36.638C49.128 39.004 52.698 42.341 55.233 46.65C57.767 50.958 59.034 56.071 59.034 61.984V96.58C59.034 102.41 57.767 107.5 55.233 111.85C52.698 116.203 49.128 119.581 44.523 121.99C39.918 124.397 34.491 125.601 28.239 125.601L0.739014 125.602ZM16.58 111.028H28.24C32.801 111.028 36.433 109.741 39.138 107.163C41.841 104.587 43.193 101.06 43.193 96.581V61.984C43.193 57.592 41.841 54.107 39.138 51.529C36.433 48.952 32.801 47.663 28.239 47.663H16.58V111.028ZM76.396 125.602V33.09H95.786L121.512 107.86C121.216 104.673 120.942 101.483 120.688 98.292C120.386 94.5366 120.154 90.776 119.991 87.012C119.821 83.169 119.738 79.812 119.738 76.938V33.09H134.185V125.602H114.795L89.323 50.832C89.491 53.283 89.703 56.239 89.956 59.702C90.2137 63.2478 90.4251 66.7967 90.59 70.348C90.758 73.982 90.844 77.318 90.844 80.36V125.602H76.396ZM181.582 126.869C175.245 126.869 169.752 125.812 165.107 123.701C160.46 121.591 156.889 118.569 154.398 114.64C151.905 110.711 150.616 106.086 150.533 100.763H166.374C166.374 104.565 167.747 107.543 170.494 109.698C173.238 111.852 176.976 112.929 181.709 112.929C186.271 112.929 189.839 111.874 192.417 109.761C194.993 107.65 196.282 104.735 196.282 101.017C196.282 97.892 195.374 95.167 193.558 92.842C191.74 90.52 189.142 88.936 185.764 88.09L175.119 85.175C167.852 83.318 162.256 79.979 158.327 75.164C154.398 70.348 152.434 64.518 152.434 57.675C152.434 52.438 153.616 47.875 155.982 43.988C158.347 40.103 161.705 37.103 166.058 34.99C170.408 32.88 175.54 31.822 181.455 31.822C190.409 31.822 197.506 34.125 202.745 38.729C207.983 43.335 210.645 49.523 210.73 57.295H194.888C194.888 53.663 193.704 50.812 191.34 48.741C188.974 46.671 185.637 45.636 181.328 45.636C177.188 45.636 173.978 46.608 171.697 48.551C169.416 50.495 168.275 53.24 168.275 56.788C168.275 60 169.141 62.724 170.873 64.962C172.603 67.202 175.119 68.786 178.413 69.714L189.439 72.756C196.789 74.616 202.407 77.932 206.294 82.Line truncated
<path d="M332.69 126.999C329.057 126.999 326.164 125.941 324.01 123.831C321.855 121.72 320.778 118.847 320.778 115.213C320.778 111.581 321.855 108.686 324.01 106.532C326.164 104.378 329.057 103.3 332.69 103.3C336.322 103.3 339.217 104.378 341.372 106.532C343.526 108.686 344.603 111.582 344.603 115.213C344.603 118.847 343.526 121.72 341.372 123.831C339.217 125.941 336.322 126.999 332.691 126.999H332.69ZM381.862 125.732V33.219H437.369V47.032H397.449V71.112H432.934V84.798H397.45V111.918H437.37V125.732H381.862ZM484.766 126.999C475.725 126.999 468.65 124.528 463.539 119.585C458.426 114.643 455.872 107.906 455.872 99.372V33.219H471.84V99.245C471.84 103.639 472.937 107.061 475.135 109.51C477.331 111.962 480.54 113.185 484.766 113.185C488.905 113.185 492.095 111.962 494.334 109.51C496.572 107.06 497.693 103.639 497.693 99.245V33.219H513.66V99.372C513.66 107.906 511.126 114.642 506.057 119.585C500.987 124.528 493.891 126.999 484.767 126.999H484.766Z" fill="#686868"/>
<path d="M549 0H573V148H549V0Z" fill="#FFCC03"/>
</g>
<defs>
<clipPath id="clip0_4_4">
<rect width="573" height="148" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

-12
View File
@@ -1,12 +0,0 @@
<svg width="573" height="148" viewBox="0 0 573 148" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_4)">
<path d="M0.739014 125.602V33.09H28.239C34.491 33.09 39.919 34.274 44.524 36.638C49.128 39.004 52.698 42.341 55.233 46.65C57.767 50.958 59.034 56.071 59.034 61.984V96.58C59.034 102.41 57.767 107.5 55.233 111.85C52.698 116.203 49.128 119.581 44.523 121.99C39.918 124.397 34.491 125.601 28.239 125.601L0.739014 125.602ZM16.58 111.028H28.24C32.801 111.028 36.433 109.741 39.138 107.163C41.841 104.587 43.193 101.06 43.193 96.581V61.984C43.193 57.592 41.841 54.107 39.138 51.529C36.433 48.952 32.801 47.663 28.239 47.663H16.58V111.028ZM76.396 125.602V33.09H95.786L121.512 107.86C121.216 104.673 120.942 101.483 120.688 98.292C120.386 94.5366 120.154 90.776 119.991 87.012C119.821 83.169 119.738 79.812 119.738 76.938V33.09H134.185V125.602H114.795L89.323 50.832C89.491 53.283 89.703 56.239 89.956 59.702C90.2137 63.2478 90.4251 66.7967 90.59 70.348C90.758 73.982 90.844 77.318 90.844 80.36V125.602H76.396ZM181.582 126.869C175.245 126.869 169.752 125.812 165.107 123.701C160.46 121.591 156.889 118.569 154.398 114.64C151.905 110.711 150.616 106.086 150.533 100.763H166.374C166.374 104.565 167.747 107.543 170.494 109.698C173.238 111.852 176.976 112.929 181.709 112.929C186.271 112.929 189.839 111.874 192.417 109.761C194.993 107.65 196.282 104.735 196.282 101.017C196.282 97.892 195.374 95.167 193.558 92.842C191.74 90.52 189.142 88.936 185.764 88.09L175.119 85.175C167.852 83.318 162.256 79.979 158.327 75.164C154.398 70.348 152.434 64.518 152.434 57.675C152.434 52.438 153.616 47.875 155.982 43.988C158.347 40.103 161.705 37.103 166.058 34.99C170.408 32.88 175.54 31.822 181.455 31.822C190.409 31.822 197.506 34.125 202.745 38.729C207.983 43.335 210.645 49.523 210.73 57.295H194.888C194.888 53.663 193.704 50.812 191.34 48.741C188.974 46.671 185.637 45.636 181.328 45.636C177.188 45.636 173.978 46.608 171.697 48.551C169.416 50.495 168.275 53.24 168.275 56.788C168.275 60 169.141 62.724 170.873 64.962C172.603 67.202 175.119 68.786 178.413 69.714L189.439 72.756C196.789 74.616 202.407 77.932 206.294 82.Line truncated
<path d="M332.69 126.999C329.057 126.999 326.164 125.941 324.01 123.831C321.855 121.72 320.778 118.847 320.778 115.213C320.778 111.581 321.855 108.686 324.01 106.532C326.164 104.378 329.057 103.3 332.69 103.3C336.322 103.3 339.217 104.378 341.372 106.532C343.526 108.686 344.603 111.582 344.603 115.213C344.603 118.847 343.526 121.72 341.372 123.831C339.217 125.941 336.322 126.999 332.691 126.999H332.69ZM381.862 125.732V33.219H437.369V47.032H397.449V71.112H432.934V84.798H397.45V111.918H437.37V125.732H381.862ZM484.766 126.999C475.725 126.999 468.65 124.528 463.539 119.585C458.426 114.643 455.872 107.906 455.872 99.372V33.219H471.84V99.245C471.84 103.639 472.937 107.061 475.135 109.51C477.331 111.962 480.54 113.185 484.766 113.185C488.905 113.185 492.095 111.962 494.334 109.51C496.572 107.06 497.693 103.639 497.693 99.245V33.219H513.66V99.372C513.66 107.906 511.126 114.642 506.057 119.585C500.987 124.528 493.891 126.999 484.767 126.999H484.766Z" fill="#359CEF"/>
<path d="M549 0H573V148H549V0Z" fill="#FFCC03"/>
</g>
<defs>
<clipPath id="clip0_4_4">
<rect width="573" height="148" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

Loaded 20 of 159 files, more files were not shown because too many files have changed in this diff. Show more