mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42763b23c0 | ||
|
|
a918465c7e | ||
|
|
d7c5f7e183 | ||
|
|
ebbe865ce0 | ||
|
|
6c0ab88488 | ||
|
|
a50576d851 | ||
|
|
150a9cecc3 | ||
|
|
676250266c | ||
|
|
042c65a652 |
No files matched your search
Generated
+3845
-2777
File diff suppressed because it is too large.
Load diff
+4
-4
@@ -48,7 +48,6 @@
|
||||
"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",
|
||||
@@ -57,12 +56,12 @@
|
||||
"ip-cidr": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.460.0",
|
||||
"next": "13.5.7",
|
||||
"next": "^14.2.28",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^18",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "^18",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-hotjar": "^6.2.0",
|
||||
@@ -78,6 +77,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^13.13.0",
|
||||
"eslint-config-next": "^14.2.28",
|
||||
"postcss": "^8",
|
||||
"prettier": "3.0.3",
|
||||
"tailwindcss": "^3"
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
+26
-23
@@ -3,12 +3,13 @@
|
||||
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/dist/lib/metadata/types/extra-types";
|
||||
import { Viewport } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import React from "react";
|
||||
import React, { Suspense } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import OIDCProvider from "@/auth/OIDCProvider";
|
||||
import AnalyticsProvider from "@/contexts/AnalyticsProvider";
|
||||
@@ -34,27 +35,29 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={cn(inter.className, "dark:bg-nb-gray bg-gray-50")}>
|
||||
<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 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>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"} />;
|
||||
|
||||
@@ -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={""}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+26
-36
@@ -41,47 +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
|
||||
}
|
||||
|
||||
// Split and validate each part
|
||||
const parts = domain.split(".");
|
||||
if (parts.length < 2) {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user