Compare commits

..
Author SHA1 Message Date
Eduard Gert d7c5f7e183 Hide update available for mobile devices (#106) (#460)
(cherry picked from commit 7f248ae060385acb1245591bd46e2bb6d53ed908)
2025-04-28 11:58:26 +02:00
Eduard Gert ebbe865ce0 Add custom dns domain (#458)
* Update domain validator

* Add custom dns domain
2025-04-28 11:58:16 +02:00
Eduard Gert 6c0ab88488 Update domain validator (#459) 2025-04-28 11:53:44 +02:00
Eduard Gert a50576d851 Fix nameserver port input for Safari (#456) 2025-04-28 11:21:25 +02:00
Eduard Gert 676250266c Fix browse posture checks table filters (#448) 2025-04-07 10:23:07 +02:00
Vladislav Tropnikov 042c65a652 Add display of ID if user does not have email (#450)
* Add display of ID if user does not have email

* Update PeerNameCell.tsx

* Add more possible id parameters

* Hide user if there is nothing

* change id order

* Keep default behavior
2025-03-27 17:30:26 +01:00
9 changed files with 172 additions and 79 deletions

No files matched your search

+1
View File
@@ -14,5 +14,6 @@ export interface Account {
jwt_allow_groups: string[];
regular_users_view_blocked: boolean;
routing_peer_dns_resolution_enabled: boolean;
dns_domain: string;
};
}
@@ -590,7 +590,7 @@ function NameserverInput({
</div>
<Input
maxWidthClass={"max-w-[130px]"}
maxWidthClass={"min-w-[150px] max-w-[150px]"}
customPrefix={"Port"}
placeholder={"53"}
value={port}
@@ -31,7 +31,7 @@ export const ResourceSingleAddressInput = ({ value, onChange }: Props) => {
// Case 1: If it has characters (potential domain) but is not a CIDR block
if (hasChars && !isCIDRBlock) {
if (!validator.isValidDomainWithWildcard(value)) {
if (!validator.isValidDomain(value)) {
return "Please enter a valid domain, e.g. intra.example.com or *.example.com";
}
return ""; // Valid domain
+4 -1
View File
@@ -20,6 +20,9 @@ export default function PeerNameCell({ peer, linkToPeer = true }: Props) {
return users?.find((user) => user.id === peer.user_id);
}, [users, peer.user_id]);
const displayUserEmailOrName = userOfPeer?.email || userOfPeer?.name;
const displayUserId = userOfPeer?.id || peer?.user_id;
return (
<div>
<div
@@ -40,7 +43,7 @@ export default function PeerNameCell({ peer, linkToPeer = true }: Props) {
}
>
<div className={"text-nb-gray-400 font-light truncate"}>
{userOfPeer?.email}
{displayUserEmailOrName || (displayUserId && `user: ${displayUserId}`)}
</div>
</ActiveInactiveRow>
</div>
+11 -2
View File
@@ -6,21 +6,30 @@ import {
TooltipTrigger,
} from "@components/Tooltip";
import MemoizedNetBirdIcon from "@components/ui/MemoizedNetBirdIcon";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { parseVersionString } from "@utils/version";
import { ArrowRightIcon, ArrowUpCircleIcon } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
import { useApplicationContext } from "@/contexts/ApplicationProvider";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
type Props = {
version: string;
os: string;
};
export default function PeerVersionCell({ version }: Props) {
export default function PeerVersionCell({ version, os }: Props) {
const { latestVersion, latestUrl } = useApplicationContext();
const updateAvailable = useMemo(() => {
const operatingSystem = getOperatingSystem(os);
if (
operatingSystem === OperatingSystem.IOS ||
operatingSystem === OperatingSystem.ANDROID
)
return false;
return parseVersionString(version) < parseVersionString(latestVersion);
}, [version, latestVersion]);
}, [os, version, latestVersion]);
const updateIcon = useMemo(() => {
return <ArrowUpCircleIcon size={15} className={"text-netbird"} />;
+3 -1
View File
@@ -154,7 +154,9 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
header: ({ column }) => {
return <DataTableHeader column={column}>Version</DataTableHeader>;
},
cell: ({ row }) => <PeerVersionCell version={row.original.version} />,
cell: ({ row }) => (
<PeerVersionCell version={row.original.version} os={row.original.os} />
),
},
{
id: "status",
@@ -3,14 +3,12 @@ import { Checkbox } from "@components/Checkbox";
import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader";
import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
import { useLocalStorage } from "@hooks/useLocalStorage";
import {
ColumnDef,
RowSelectionState,
SortingState,
} from "@tanstack/react-table";
import useFetchApi from "@utils/api";
import { usePathname } from "next/navigation";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import { PostureCheck } from "@/interfaces/PostureCheck";
@@ -25,18 +23,14 @@ export default function PostureCheckBrowseTable({ onAdd }: Readonly<Props>) {
const { data: postureChecks, isLoading } =
useFetchApi<PostureCheck[]>("/posture-checks");
const { mutate } = useSWRConfig();
const path = usePathname();
// Default sorting state of the table
const [sorting, setSorting] = useLocalStorage<SortingState>(
"netbird-table-sort" + path,
[
{
id: "name",
desc: true,
},
],
);
const [sorting, setSorting] = useState<SortingState>([
{
id: "name",
desc: true,
},
]);
const [selectedRows, setSelectedRows] = useState<RowSelectionState>({});
@@ -47,6 +41,7 @@ export default function PostureCheckBrowseTable({ onAdd }: Readonly<Props>) {
rowSelection={selectedRows}
setRowSelection={setSelectedRows}
isLoading={isLoading}
keepStateInLocalStorage={false}
text={"Posture Check"}
sorting={sorting}
wrapperClassName={""}
+117 -27
View File
@@ -1,10 +1,18 @@
import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import { notify } from "@components/Notification";
import { useHasChanges } from "@hooks/useHasChanges";
import * as Tabs from "@radix-ui/react-tabs";
import { useApiCall } from "@utils/api";
import { GlobeIcon, NetworkIcon } from "lucide-react";
import React, { useState } from "react";
import { validator } from "@utils/helpers";
import { isNetBirdHosted } from "@utils/netbird";
import { ExternalLinkIcon, GlobeIcon, NetworkIcon } from "lucide-react";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import SettingsIcon from "@/assets/icons/SettingsIcon";
import { Account } from "@/interfaces/Account";
@@ -13,18 +21,23 @@ type Props = {
account: Account;
};
export default function NetworkSettingsTab({ account }: Props) {
export default function NetworkSettingsTab({ account }: Readonly<Props>) {
const { mutate } = useSWRConfig();
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
const saveRequest = useApiCall<Account>("/accounts/" + account.id, true);
const [routingPeerDNSSetting, setRoutingPeerDNSSetting] = useState(
account.settings.routing_peer_dns_resolution_enabled,
);
const [customDNSDomain, setCustomDNSDomain] = useState(
account.settings.dns_domain || "",
);
const toggleSetting = async (toggle: boolean) => {
const toggleNetworkDNSSetting = async (toggle: boolean) => {
notify({
title: "Save Network Settings",
description: "Network settings successfully saved.",
title: "DNS Wildcard Routing",
description: `DNS Wildcard Routing successfully ${
toggle ? "enabled" : "disabled"
}.`,
promise: saveRequest
.put({
id: account.id,
@@ -37,10 +50,43 @@ export default function NetworkSettingsTab({ account }: Props) {
setRoutingPeerDNSSetting(toggle);
mutate("/accounts");
}),
loadingMessage: "Saving the network settings...",
loadingMessage: "Updating DNS wildcard setting...",
});
};
const { hasChanges, updateRef } = useHasChanges([customDNSDomain]);
const saveChanges = async () => {
notify({
title: "Custom DNS Domain",
description: `Custom DNS Domain successfully updated.`,
promise: saveRequest
.put({
id: account.id,
settings: {
...account.settings,
dns_domain: customDNSDomain || "",
},
})
.then(() => {
mutate("/accounts");
updateRef([customDNSDomain]);
}),
loadingMessage: "Updating Custom DNS domain...",
});
};
const domainError = useMemo(() => {
if (customDNSDomain == "") return "";
const valid = validator.isValidDomain(customDNSDomain, {
allowWildcard: false,
allowOnlyTld: false,
});
if (!valid) {
return "Please enter a valid domain, e.g. example.com or intra.example.com";
}
}, [customDNSDomain]);
return (
<Tabs.Content value={"networks"}>
<div className={"p-default py-6 max-w-2xl"}>
@@ -51,36 +97,80 @@ export default function NetworkSettingsTab({ account }: Props) {
icon={<SettingsIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/settings#network"}
label={"Network"}
href={"/settings?tab=networks"}
label={"Networks"}
icon={<NetworkIcon size={14} />}
active
/>
</Breadcrumbs>
<div className={"flex items-start justify-between"}>
<h1>Networks</h1>
<div>
<h1>Networks</h1>
</div>
<Button
variant={"primary"}
disabled={!hasChanges}
onClick={saveChanges}
>
Save Changes
</Button>
</div>
<div className={"flex flex-col gap-6 w-full mt-8"}>
<div>
<FancyToggleSwitch
value={routingPeerDNSSetting}
onChange={toggleSetting}
label={
<>
<GlobeIcon size={15} />
Enable DNS Wildcard Routing
</>
<div
className={
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
}
helpText={
<>
Allow routing using DNS wildcards. This requires NetBird
client v0.35 or higher. Changes will only take effect after
restarting the clients.
</>
}
/>
>
<div className={"min-w-[330px]"}>
<Label>DNS Domain</Label>
<HelpText>
Specify a custom DNS domain for your network. This will be
used for all your peers.
</HelpText>
</div>
<div className={"w-full"}>
<Input
placeholder={
isNetBirdHosted() ? "netbird.cloud" : "netbird.selfhosted"
}
errorTooltip={true}
errorTooltipPosition={"top"}
error={domainError}
value={customDNSDomain}
onChange={(e) => setCustomDNSDomain(e.target.value)}
/>
</div>
</div>
</div>
<FancyToggleSwitch
value={routingPeerDNSSetting}
onChange={toggleNetworkDNSSetting}
label={
<>
<GlobeIcon size={15} />
Enable DNS Wildcard Routing
</>
}
helpText={
<>
Allow routing using DNS wildcards. This requires NetBird client
v0.35 or higher. Changes will only take effect after restarting
the clients.{" "}
<InlineLink
href={
"https://docs.netbird.io/how-to/accessing-entire-domains-within-networks#enabling-dns-wildcard-routing"
}
target={"_blank"}
>
Learn more
<ExternalLinkIcon size={12} />
</InlineLink>
</>
}
/>
</div>
</div>
</Tabs.Content>
+27 -34
View File
@@ -41,44 +41,37 @@ export const sleep = (ms: number) => {
};
export const validator = {
isValidDomain: (domain: string) => {
const unicodeDomain =
/^(?!.*\.\.)(?!.*\.$)(?!.*\s)(?:(?!-)(?!.*--)[a-zA-Z0-9\u00A1-\uFFFF-]{1,63}(?<!-)\.)+(?!-)(?!.*--)[a-zA-Z0-9\u00A1-\uFFFF-]{2,63}$/u;
try {
const minMaxChars = [1, 255];
const isValidDomainLength =
domain.length >= minMaxChars[0] && domain.length <= minMaxChars[1];
const includesDot = domain.includes(".");
const hasNoWhitespace = !domain.includes(" ");
return (
unicodeDomain.test(domain) &&
includesDot &&
hasNoWhitespace &&
isValidDomainLength
);
} catch (e) {
return false;
}
},
isValidDomainWithWildcard: (domain: string) => {
// Basic checks
if (!domain || domain.length > 255 || domain.includes(" ")) {
return false;
}
isValidDomain: (
domain: string,
options?: { allowWildcard?: boolean; allowOnlyTld?: boolean },
) => {
const { allowWildcard = true, allowOnlyTld = true } = options || {
allowWildcard: true,
allowOnlyTld: true,
};
// Handle wildcard
if (domain.includes("*")) {
if (!domain.startsWith("*.") || domain.indexOf("*", 1) !== -1) {
try {
const includesAtLeastOneDot = domain.includes(".");
const hasWhitespace = domain.includes(" ");
const domainRegex =
/^(?!-)[a-z0-9\u00a1-\uffff-*]{0,63}(?<!-)(\.[a-z0-9\u00a1-\uffff-*]{0,63})*$/i;
const isValidUnicodeDomain = domainRegex.test(domain);
if (domain.length < 1 || domain.length > 255) {
return false;
}
domain = "sub" + domain.slice(1); // Replace * with valid subdomain for testing
if (!allowWildcard && domain.startsWith("*.")) {
return false;
}
if (allowWildcard && !domain.startsWith("*.") && domain.includes("*")) {
return false;
}
if (!allowOnlyTld && domain.startsWith(".")) {
return false;
}
return includesAtLeastOneDot && isValidUnicodeDomain && !hasWhitespace;
} catch (error) {
return false;
}
// Split and validate each part
const parts = domain.split(".");
const validPart = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
return parts.every((part) => validPart.test(part));
},
isValidEmail: (email: string) => {
const regExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;