diff --git a/src/components/skeletons/SkeletonSettings.tsx b/src/components/skeletons/SkeletonSettings.tsx
new file mode 100644
index 00000000..8b531519
--- /dev/null
+++ b/src/components/skeletons/SkeletonSettings.tsx
@@ -0,0 +1,20 @@
+import * as React from "react";
+import Skeleton from "react-loading-skeleton";
+
+export const SkeletonSettings = () => {
+ return (
+
+ );
+};
diff --git a/src/interfaces/Account.ts b/src/interfaces/Account.ts
index 174904e9..70fe17f0 100644
--- a/src/interfaces/Account.ts
+++ b/src/interfaces/Account.ts
@@ -10,6 +10,8 @@ export interface Account {
user_approval_required: boolean;
};
peer_login_expiration_enabled: boolean;
+ peer_expose_enabled?: boolean;
+ peer_expose_groups?: string[];
peer_login_expiration: number;
peer_inactivity_expiration_enabled: boolean;
peer_inactivity_expiration: number;
diff --git a/src/modules/activity/ActivityDescription.tsx b/src/modules/activity/ActivityDescription.tsx
index 5d7fcfc8..b71849b9 100644
--- a/src/modules/activity/ActivityDescription.tsx
+++ b/src/modules/activity/ActivityDescription.tsx
@@ -664,6 +664,35 @@ export default function ActivityDescription({ event }: Props) {
);
+ /**
+ * Reverse Proxy
+ */
+
+ if (event.activity_code == "service.peer.expose")
+ return (
+
+ Peer {m.peer_name} exposed service{" "}
+ {m.domain} with auth{" "}
+ {m.auth ? "Enabled" : "Disabled"}
+
+ );
+
+ if (event.activity_code == "service.peer.unexpose")
+ return (
+
+ Peer {m.peer_name} unexposed service{" "}
+ {m.domain}
+
+ );
+
+ if (event.activity_code == "service.peer.expose.expire")
+ return (
+
+ Service {m.domain} exposed by peer{" "}
+ {m.peer_name} was removed due to renewal expiration
+
+ );
+
/**
* Networks
*/
diff --git a/src/modules/settings/ClientSettingsTab.tsx b/src/modules/settings/ClientSettingsTab.tsx
index e9f39a33..f3851580 100644
--- a/src/modules/settings/ClientSettingsTab.tsx
+++ b/src/modules/settings/ClientSettingsTab.tsx
@@ -6,6 +6,7 @@ import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import { notify } from "@components/Notification";
+import { PeerGroupSelector } from "@components/PeerGroupSelector";
import {
SelectDropdown,
SelectOption,
@@ -13,7 +14,7 @@ import {
import { useHasChanges } from "@hooks/useHasChanges";
import * as Tabs from "@radix-ui/react-tabs";
import { useApiCall } from "@utils/api";
-import { validator } from "@utils/helpers";
+import { cn, validator } from "@utils/helpers";
import {
ClockFadingIcon,
ExternalLinkIcon,
@@ -27,6 +28,10 @@ import SettingsIcon from "@/assets/icons/SettingsIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { Account } from "@/interfaces/Account";
import { SmallBadge } from "@components/ui/SmallBadge";
+import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
+import useGroupHelper from "@/modules/groups/useGroupHelper";
+import { useGroups } from "@/contexts/GroupsProvider";
+import { SkeletonSettings } from "@components/skeletons/SkeletonSettings";
type Props = {
account: Account;
@@ -48,6 +53,16 @@ const latestOrCustomVersion = [
] as SelectOption[];
export default function ClientSettingsTab({ account }: Readonly) {
+ const { isLoading: isGroupsLoading } = useGroups();
+
+ return isGroupsLoading ? (
+
+ ) : (
+
+ );
+}
+
+function ClientSettingsTabContent({ account }: Readonly) {
const { permission } = usePermissions();
const { mutate } = useSWRConfig();
@@ -69,9 +84,23 @@ export default function ClientSettingsTab({ account }: Readonly) {
isCustomVersion ? autoUpdateSetting : "",
);
+ const [peerExposeEnabled, setPeerExposeEnabled] = useState(
+ account?.settings?.peer_expose_enabled ?? false,
+ );
+ const [peerExposeGroups, setPeerExposeGroups, { save: saveGroups }] =
+ useGroupHelper({
+ initial: account.settings?.peer_expose_groups,
+ });
+ const peerExposeGroupNames = useMemo(
+ () => peerExposeGroups.map((g) => g.name).sort(),
+ [peerExposeGroups],
+ );
+
const { hasChanges, updateRef } = useHasChanges([
autoUpdateMethod,
autoUpdateCustomVersion,
+ peerExposeEnabled,
+ peerExposeGroupNames,
]);
const handleUpdateMethodChange = (value: string) => {
@@ -99,16 +128,24 @@ export default function ClientSettingsTab({ account }: Readonly) {
return (
!hasChanges ||
!permission.settings.update ||
- (autoUpdateMethod === "custom" && !canSaveCustomVersion)
+ (autoUpdateMethod === "custom" && !canSaveCustomVersion) ||
+ (peerExposeEnabled && peerExposeGroups.length === 0)
);
}, [
hasChanges,
permission.settings.update,
autoUpdateMethod,
canSaveCustomVersion,
+ peerExposeEnabled,
+ peerExposeGroups,
]);
const saveChanges = async () => {
+ const groups = await saveGroups();
+ const peerExposeGroupIds = groups
+ .map((group) => group.id)
+ .filter(Boolean) as string[];
+
notify({
title: "Client Settings",
description: `Client settings successfully updated.`,
@@ -118,11 +155,18 @@ export default function ClientSettingsTab({ account }: Readonly) {
settings: {
...account.settings,
auto_update_version: autoUpdateCustomVersion || autoUpdateMethod,
+ peer_expose_enabled: peerExposeEnabled,
+ peer_expose_groups: peerExposeGroupIds,
},
})
.then(() => {
mutate("/accounts");
- updateRef([autoUpdateMethod, autoUpdateCustomVersion]);
+ updateRef([
+ autoUpdateMethod,
+ autoUpdateCustomVersion,
+ peerExposeEnabled,
+ peerExposeGroupNames,
+ ]);
}),
loadingMessage: "Updating client settings...",
});
@@ -152,7 +196,7 @@ export default function ClientSettingsTab({ account }: Readonly) {
return (
-
+
) {
-
+
@@ -223,7 +267,63 @@ export default function ClientSettingsTab({ account }: Readonly) {
-
+
+
+
+
+ Expose Services from CLI
+
+
+ Allow peers to expose local services through the NetBird reverse
+ proxy using the CLI. This requires at least NetBird{" "}
+ v0.66.0 .{" "}
+
+ Learn more
+
+
+
+
+
+
+
+
+
+
Allowed peer groups
+
+ Select which peer groups are allowed to expose services. At
+ least one group is required.
+
+
+
+
+
+
+
Experimental
@@ -241,25 +341,26 @@ export default function ClientSettingsTab({ account }: Readonly) {
+
+
+ Enable Lazy Connections
+ >
+ }
+ helpText={
+ <>
+ Allow to establish connections between peers only when
+ required. This requires NetBird client v0.45 or higher.
+ Changes will only take effect after restarting the clients.
+ >
+ }
+ disabled={!permission.settings.update}
+ />
-
-
- Enable Lazy Connections
- >
- }
- helpText={
- <>
- Allow to establish connections between peers only when required.
- This requires NetBird client v0.45 or higher. Changes will only
- take effect after restarting the clients.
- >
- }
- disabled={!permission.settings.update}
- />