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 ( -
+
) {
-
+
-
+
+
+ + + Allow peers to expose local services through the NetBird reverse + proxy using the CLI.
This requires at least NetBird{" "} + v0.66.0.{" "} + + Learn more + + +
+
+ + + +
+
+ + + Select which peer groups are allowed to expose services. At + least one group is required. + + +
+
+
+ +
- - - 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} - />