Compare commits

..
Author SHA1 Message Date
Krzysztof Nazarewski (kdn) e0e292669a feat: remove unnecessary non-TLD validation 2025-03-27 16:38:10 +01:00
12 changed files with 2871 additions and 4035 deletions

No files matched your search

+2764 -3832
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -48,6 +48,7 @@
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"eslint": "^8",
"eslint-config-next": "13.5.5",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"flowbite": "^1.8.1",
@@ -56,12 +57,12 @@
"ip-cidr": "^3.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.460.0",
"next": "^14.2.28",
"next": "13.5.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18.3.1",
"react": "^18",
"react-day-picker": "^8.9.1",
"react-dom": "^18.3.1",
"react-dom": "^18",
"react-ga4": "^2.1.0",
"react-hot-toast": "^2.4.1",
"react-hotjar": "^6.2.0",
@@ -77,7 +78,6 @@
},
"devDependencies": {
"cypress": "^13.13.0",
"eslint-config-next": "^14.2.28",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"
-1
View File
@@ -14,6 +14,5 @@ export interface Account {
jwt_allow_groups: string[];
regular_users_view_blocked: boolean;
routing_peer_dns_resolution_enabled: boolean;
dns_domain: string;
};
}
+23 -26
View File
@@ -3,13 +3,12 @@
import "../app/globals.css";
import { DisableDarkReader } from "@components/DisableDarkReader";
import { TooltipProvider } from "@components/Tooltip";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { cn } from "@utils/helpers";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { Viewport } from "next";
import { Viewport } from "next/dist/lib/metadata/types/extra-types";
import localFont from "next/font/local";
import React, { Suspense } from "react";
import React from "react";
import { Toaster } from "react-hot-toast";
import OIDCProvider from "@/auth/OIDCProvider";
import AnalyticsProvider from "@/contexts/AnalyticsProvider";
@@ -35,29 +34,27 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={cn(inter.className, "dark:bg-nb-gray bg-gray-50")}>
<Suspense fallback={<FullScreenLoading />}>
<AnalyticsProvider>
<DialogProvider>
<GlobalThemeProvider>
<ErrorBoundaryProvider>
<OIDCProvider>
<TooltipProvider delayDuration={0}>
{children}
</TooltipProvider>
</OIDCProvider>
</ErrorBoundaryProvider>
</GlobalThemeProvider>
</DialogProvider>
<Toaster
position={"top-center"}
toastOptions={{
duration: 3000,
}}
/>
<NavigationEvents />
<DisableDarkReader />
</AnalyticsProvider>
</Suspense>
<AnalyticsProvider>
<DialogProvider>
<GlobalThemeProvider>
<ErrorBoundaryProvider>
<OIDCProvider>
<TooltipProvider delayDuration={0}>
{children}
</TooltipProvider>
</OIDCProvider>
</ErrorBoundaryProvider>
</GlobalThemeProvider>
</DialogProvider>
<Toaster
position={"top-center"}
toastOptions={{
duration: 3000,
}}
/>
<NavigationEvents />
<DisableDarkReader />
</AnalyticsProvider>
</body>
</html>
);
@@ -590,7 +590,7 @@ function NameserverInput({
</div>
<Input
maxWidthClass={"min-w-[150px] max-w-[150px]"}
maxWidthClass={"max-w-[130px]"}
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.isValidDomain(value)) {
if (!validator.isValidDomainWithWildcard(value)) {
return "Please enter a valid domain, e.g. intra.example.com or *.example.com";
}
return ""; // Valid domain
+1 -4
View File
@@ -20,9 +20,6 @@ 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
@@ -43,7 +40,7 @@ export default function PeerNameCell({ peer, linkToPeer = true }: Props) {
}
>
<div className={"text-nb-gray-400 font-light truncate"}>
{displayUserEmailOrName || (displayUserId && `user: ${displayUserId}`)}
{userOfPeer?.email}
</div>
</ActiveInactiveRow>
</div>
+2 -11
View File
@@ -6,30 +6,21 @@ 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, os }: Props) {
export default function PeerVersionCell({ version }: 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);
}, [os, version, latestVersion]);
}, [version, latestVersion]);
const updateIcon = useMemo(() => {
return <ArrowUpCircleIcon size={15} className={"text-netbird"} />;
+1 -3
View File
@@ -154,9 +154,7 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
header: ({ column }) => {
return <DataTableHeader column={column}>Version</DataTableHeader>;
},
cell: ({ row }) => (
<PeerVersionCell version={row.original.version} os={row.original.os} />
),
cell: ({ row }) => <PeerVersionCell version={row.original.version} />,
},
{
id: "status",
@@ -3,12 +3,14 @@ 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";
@@ -23,14 +25,18 @@ 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] = useState<SortingState>([
{
id: "name",
desc: true,
},
]);
const [sorting, setSorting] = useLocalStorage<SortingState>(
"netbird-table-sort" + path,
[
{
id: "name",
desc: true,
},
],
);
const [selectedRows, setSelectedRows] = useState<RowSelectionState>({});
@@ -41,7 +47,6 @@ export default function PostureCheckBrowseTable({ onAdd }: Readonly<Props>) {
rowSelection={selectedRows}
setRowSelection={setSelectedRows}
isLoading={isLoading}
keepStateInLocalStorage={false}
text={"Posture Check"}
sorting={sorting}
wrapperClassName={""}
+27 -117
View File
@@ -1,18 +1,10 @@
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 { validator } from "@utils/helpers";
import { isNetBirdHosted } from "@utils/netbird";
import { ExternalLinkIcon, GlobeIcon, NetworkIcon } from "lucide-react";
import React, { useMemo, useState } from "react";
import { GlobeIcon, NetworkIcon } from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import SettingsIcon from "@/assets/icons/SettingsIcon";
import { Account } from "@/interfaces/Account";
@@ -21,23 +13,18 @@ type Props = {
account: Account;
};
export default function NetworkSettingsTab({ account }: Readonly<Props>) {
export default function NetworkSettingsTab({ account }: Props) {
const { mutate } = useSWRConfig();
const saveRequest = useApiCall<Account>("/accounts/" + account.id, true);
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
const [routingPeerDNSSetting, setRoutingPeerDNSSetting] = useState(
account.settings.routing_peer_dns_resolution_enabled,
);
const [customDNSDomain, setCustomDNSDomain] = useState(
account.settings.dns_domain || "",
);
const toggleNetworkDNSSetting = async (toggle: boolean) => {
const toggleSetting = async (toggle: boolean) => {
notify({
title: "DNS Wildcard Routing",
description: `DNS Wildcard Routing successfully ${
toggle ? "enabled" : "disabled"
}.`,
title: "Save Network Settings",
description: "Network settings successfully saved.",
promise: saveRequest
.put({
id: account.id,
@@ -50,43 +37,10 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
setRoutingPeerDNSSetting(toggle);
mutate("/accounts");
}),
loadingMessage: "Updating DNS wildcard setting...",
loadingMessage: "Saving the network settings...",
});
};
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"}>
@@ -97,80 +51,36 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
icon={<SettingsIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/settings?tab=networks"}
label={"Networks"}
href={"/settings#network"}
label={"Network"}
icon={<NetworkIcon size={14} />}
active
/>
</Breadcrumbs>
<div className={"flex items-start justify-between"}>
<div>
<h1>Networks</h1>
</div>
<Button
variant={"primary"}
disabled={!hasChanges}
onClick={saveChanges}
>
Save Changes
</Button>
<h1>Networks</h1>
</div>
<div className={"flex flex-col gap-6 w-full mt-8"}>
<div>
<div
className={
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
<FancyToggleSwitch
value={routingPeerDNSSetting}
onChange={toggleSetting}
label={
<>
<GlobeIcon size={15} />
Enable DNS Wildcard Routing
</>
}
>
<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>
helpText={
<>
Allow routing using DNS wildcards. This requires NetBird
client v0.35 or higher. Changes will only take effect after
restarting the clients.
</>
}
/>
</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>
+35 -28
View File
@@ -41,38 +41,45 @@ export const sleep = (ms: number) => {
};
export const validator = {
isValidDomain: (
domain: string,
options?: { allowWildcard?: boolean; allowOnlyTld?: boolean },
) => {
const { allowWildcard = true, allowOnlyTld = true } = options || {
allowWildcard: true,
allowOnlyTld: true,
};
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 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;
}
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) {
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;
}
// Handle wildcard
if (domain.includes("*")) {
if (!domain.startsWith("*.") || domain.indexOf("*", 1) !== -1) {
return false;
}
domain = "sub" + domain.slice(1); // Replace * with valid subdomain for testing
}
// 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;
try {