Compare commits

..
Author SHA1 Message Date
mlsmaycon 8174c8e134 add docs link for routing peer clarification in ResourcesTable 2026-04-08 16:10:31 +02:00
35 changed files with 254 additions and 587 deletions

No files matched your search

+57 -33
View File
@@ -60,7 +60,7 @@
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lucide-react": "^0.566.0",
"next": "16.1.7",
"next": "^16.1.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^19.2.4",
@@ -3244,6 +3244,32 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -3972,13 +3998,10 @@
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
@@ -4002,15 +4025,13 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
@@ -4299,6 +4320,12 @@
"node": ">= 10"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -6862,9 +6889,9 @@
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -6946,18 +6973,15 @@
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"license": "BlueOak-1.0.0",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^5.0.5"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"node": "*"
}
},
"node_modules/minimist": {
@@ -7388,9 +7412,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -8753,9 +8777,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
+1 -4
View File
@@ -68,7 +68,7 @@
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lucide-react": "^0.566.0",
"next": "16.1.7",
"next": "^16.1.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^19.2.4",
@@ -89,9 +89,6 @@
"timescape": "^0.7.1",
"typescript": "^5"
},
"overrides": {
"minimatch": ">=10.2.1"
},
"devDependencies": {
"@faker-js/faker": "^9.5.1",
"@types/chroma-js": "^3.1.1",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

-2
View File
@@ -23,7 +23,6 @@ const variants = cva("", {
purple: ["bg-purple-950/50 border-purple-500 border text-purple-500"],
yellow: ["bg-yellow-950 border-yellow-500 border text-yellow-400"],
gray: ["bg-nb-gray-930/60 border-nb-gray-800/40 text-nb-gray-300 border"],
lightGray: ["bg-nb-gray-910 text-nb-gray-200 border border-nb-gray-900"],
grayer: [
"bg-nb-gray-900/40 border-nb-gray-800/40 text-nb-gray-300 border",
],
@@ -46,7 +45,6 @@ const variants = cva("", {
"blue-darker": ["hover:bg-sky-800"],
red: ["hover:bg-red-950/40"],
gray: ["hover:bg-nb-gray-900"],
lightGray: ["hover:bg-nb-gray-900"],
grayer: ["hover:bg-nb-gray-900"],
"gray-ghost": ["hover:bg-nb-gray-800 cursor-pointer"],
green: ["hover:bg-green-950/50"],
+3 -7
View File
@@ -8,7 +8,7 @@ import React from "react";
export const fancyToggleSwitchVariants = cva([], {
variants: {
variant: {
default: ["px-5 py-4 border rounded-md"],
default: ["px-6 py-4 border rounded-md"],
blank: null,
},
state: {
@@ -45,8 +45,6 @@ interface Props extends FancyToggleSwitchVariants {
disabled?: boolean;
dataCy?: string;
className?: string;
labelClassName?: string;
textWrapperClassName?: string;
}
export default function FancyToggleSwitch({
@@ -59,8 +57,6 @@ export default function FancyToggleSwitch({
dataCy,
className,
variant = "default",
labelClassName,
textWrapperClassName = "max-w-sm",
}: Readonly<Props>) {
const handleToggle = () => {
if (disabled) return;
@@ -91,8 +87,8 @@ export default function FancyToggleSwitch({
)}
>
<div className={"flex justify-between gap-10"}>
<div className={cn(textWrapperClassName)}>
<Label className={labelClassName}>{label}</Label>
<div className={"max-w-sm"}>
<Label>{label}</Label>
<HelpText margin={false}>{helpText}</HelpText>
</div>
<div className={"mt-2 pr-1"}>
-17
View File
@@ -1,17 +0,0 @@
import { useAccount } from "@/modules/account/useAccount";
import { SSOIdentityProvider } from "@/interfaces/IdentityProvider";
import useFetchApi from "@utils/api";
export function useEmbeddedIdentityProviders() {
const account = useAccount();
const isEmbeddedIdPEnabled = !!account?.settings?.embedded_idp_enabled;
const { data: providers } = useFetchApi<SSOIdentityProvider[]>(
"/identity-providers",
true,
true,
isEmbeddedIdPEnabled,
);
return { providers, isEmbeddedIdPEnabled };
}
+15 -11
View File
@@ -1,33 +1,37 @@
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useMemo } from "react";
import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
export default function useUrlTab(
validTabs: string[],
defaultTab: string,
paramName: string = "tab",
): [string, (value: string) => void] {
const searchParams = useSearchParams();
const router = useRouter();
const getTab = useCallback(
(params: URLSearchParams) => {
const tabParam = params.get(paramName);
const tabParam = params.get("tab");
if (tabParam && validTabs.includes(tabParam)) return tabParam;
return defaultTab;
},
[validTabs, defaultTab, paramName],
[validTabs, defaultTab],
);
const tab = useMemo(() => getTab(searchParams), [searchParams, getTab]);
const [tab, setTabState] = useState(() => getTab(searchParams));
useEffect(() => {
const newTab = getTab(searchParams);
setTabState(newTab);
}, [searchParams, getTab]);
const setTab = useCallback(
(value: string) => {
const nextTab = validTabs.includes(value) ? value : defaultTab;
const params = new URLSearchParams(searchParams.toString());
params.set(paramName, nextTab);
router.replace(`?${params.toString()}`, { scroll: false });
setTabState(nextTab);
const params = new URLSearchParams(window.location.search);
params.set("tab", nextTab);
window.history.replaceState(null, "", `?${params.toString()}`);
},
[searchParams, router, validTabs, defaultTab, paramName],
[validTabs, defaultTab],
);
return [tab, setTab];
+5 -9
View File
@@ -1,22 +1,20 @@
export interface GoogleWorkspaceIntegration {
id: string;
customer_id: string;
sync_interval: number;
customerId: string;
syncInterval: number;
enabled: boolean;
group_prefixes: string[];
user_group_prefixes: string[];
connector_id?: string;
}
export interface AzureADIntegration {
id: string;
client_id: string;
tenant_id: string;
sync_interval: number;
clientId: string;
tenantId: string;
syncInterval: number;
enabled: boolean;
group_prefixes: string[];
user_group_prefixes: string[];
connector_id?: string;
}
export interface OktaIntegration {
@@ -25,8 +23,6 @@ export interface OktaIntegration {
group_prefixes: string[];
user_group_prefixes: string[];
auth_token: string;
connection_name?: string;
connector_id?: string;
}
export interface IdentityProviderLog {
-11
View File
@@ -22,20 +22,11 @@ export interface ReverseProxy {
meta?: ReverseProxyMeta;
}
export const CrowdSecMode = {
OFF: "off",
ENFORCE: "enforce",
OBSERVE: "observe",
} as const;
export type CrowdSecMode = (typeof CrowdSecMode)[keyof typeof CrowdSecMode];
export interface AccessRestrictions {
allowed_cidrs?: string[];
blocked_cidrs?: string[];
allowed_countries?: string[];
blocked_countries?: string[];
crowdsec_mode?: CrowdSecMode;
}
export interface ReverseProxyMeta {
@@ -111,7 +102,6 @@ export interface ReverseProxyDomain {
target_cluster?: string;
supports_custom_ports?: boolean;
require_subdomain?: boolean;
supports_crowdsec?: boolean;
}
export enum ReverseProxyDomainType {
@@ -159,7 +149,6 @@ export interface ReverseProxyEvent {
bytes_upload: number;
bytes_download: number;
protocol?: EventProtocol;
metadata?: Record<string, string>;
}
export function isL4Event(event: ReverseProxyEvent): boolean {
@@ -821,36 +821,6 @@ export default function ActivityDescription({ event }: Props) {
</div>
);
/**
* Reverse Proxy
*/
if (event.activity_code == "service.create")
return (
<div className={"inline"}>
Service <Value>{m.domain}</Value> in cluster{" "}
<Value>{m.proxy_cluster}</Value> was created with authentication{" "}
<Value>{m.auth ? "Enabled" : "Disabled"}</Value>
</div>
);
if (event.activity_code == "service.update")
return (
<div className={"inline"}>
Service <Value>{m.domain}</Value> in cluster{" "}
<Value>{m.proxy_cluster}</Value> was updated with authentication{" "}
<Value>{m.auth === "true" ? "Enabled" : "Disabled"}</Value>
</div>
);
if (event.activity_code == "service.delete")
return (
<div className={"inline"}>
Service <Value>{m.domain}</Value> in cluster{" "}
<Value>{m.proxy_cluster}</Value> was deleted
</div>
);
return (
<div className={"flex gap-2.5 items-center"}>
<span className={"mb-[1px]"}>{event.activity}</span>
@@ -49,9 +49,7 @@ export function ActivityEventCodeSelector({
return {
activity_code: event.activity_code,
activity: event.activity,
group: event.activity_code.startsWith("service.user")
? "Service User"
: event.activity_code.split(".")[0],
group: event.activity_code.split(".")[0],
};
});
return items.reduce((acc, item) => {
+9 -12
View File
@@ -11,6 +11,7 @@ import {
KeyRound,
Layers3Icon,
LogIn,
type LucideIcon,
MonitorSmartphoneIcon,
NetworkIcon,
RefreshCcw,
@@ -20,7 +21,6 @@ import {
User,
} from "lucide-react";
import React from "react";
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
type Props = {
code: string;
@@ -46,7 +46,7 @@ const ActivityTypeMappings = {
dashboard: LogIn,
integration: Blocks,
personal: User,
"service.user": Cog,
service: Cog,
billing: CreditCardIcon,
integrated: ShieldCheck,
posture: ShieldCheck,
@@ -54,24 +54,21 @@ const ActivityTypeMappings = {
resource: Layers3Icon,
network: NetworkIcon,
identityprovider: FingerprintIcon,
service: ReverseProxyIcon,
} as const;
} as const satisfies Record<string, LucideIcon>;
export default function ActivityTypeIcon({
code,
size = 18,
className,
}: Props) {
const parts = code?.split(".") || [];
const twoPartKey = parts.slice(0, 2).join(".").toLowerCase();
const onePartKey = (parts[0] || "").toLowerCase();
const key = (
twoPartKey in ActivityTypeMappings ? twoPartKey : onePartKey
) as ActivityTypeKey;
const prefixParts = code?.split(".") || [];
const prefix = (prefixParts[0] || "").toLowerCase();
// Check if prefix is a valid key, otherwise use fallback
const Icon =
key in ActivityTypeMappings ? ActivityTypeMappings[key] : HelpCircleIcon;
prefix in ActivityTypeMappings
? ActivityTypeMappings[prefix as ActivityTypeKey]
: HelpCircleIcon;
return <Icon size={size} className={cn(DEFAULT_CLASSES, className)} />;
}
@@ -7,7 +7,8 @@ import NoResults from "@components/ui/NoResults";
import { IconCirclePlus } from "@tabler/icons-react";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import { removeAllSpaces } from "@utils/helpers";
import { ArrowUpRightIcon, Layers3Icon } from "lucide-react";
import { ArrowUpRightIcon, ExternalLinkIcon, Layers3Icon } from "lucide-react";
import InlineLink from "@components/InlineLink";
import { useRouter, useSearchParams } from "next/navigation";
import * as React from "react";
import { useState } from "react";
@@ -210,10 +211,24 @@ export default function ResourcesTable({
}
>
{(table) => (
<DataTableRowsPerPage
table={table}
disabled={!resources || resources?.length == 0}
/>
<>
<DataTableRowsPerPage
table={table}
disabled={!resources || resources?.length == 0}
/>
{!isGroupPage && (
<InlineLink
href={
"https://docs.netbird.io/manage/networks#manage-access-to-routing-peer"
}
target={"_blank"}
className={"text-xs whitespace-nowrap"}
>
Trying to access resources on the routing peer?
<ExternalLinkIcon size={12} />
</InlineLink>
)}
</>
)}
</DataTable>
);
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useReducer, useRef, useState } from "react";
import { useEffect, useMemo, useReducer, useRef } from "react";
import { Label } from "@components/Label";
import HelpText from "@components/HelpText";
import Button from "@components/Button";
@@ -18,8 +18,7 @@ import {
SelectOption,
} from "@components/select/SelectDropdown";
import { CountrySelector } from "@/components/ui/CountrySelector";
import { AccessRestrictions, CrowdSecMode } from "@/interfaces/ReverseProxy";
import { ReverseProxyCrowdSecIPReputation } from "@/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation";
import { AccessRestrictions } from "@/interfaces/ReverseProxy";
type AccessAction = "allow" | "block";
type AccessRuleType = "country" | "ip" | "cidr";
@@ -94,41 +93,30 @@ function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] {
}
}
function pushCidrRules(
rules: AccessRule[],
values: string[] | undefined,
action: AccessAction,
) {
values?.forEach((v) => {
const isIp = v.includes(":") ? v.endsWith("/128") : v.endsWith("/32");
rules.push({
id: nextId(),
action,
type: isIp ? "ip" : "cidr",
value: isIp ? v.replace(/\/(32|128)$/, "") : v,
});
});
}
function restrictionsToRules(
restrictions: AccessRestrictions | undefined,
): AccessRule[] {
if (!restrictions) return [];
const rules: AccessRule[] = [];
pushCidrRules(rules, restrictions.blocked_cidrs, "block");
restrictions.blocked_countries?.forEach((v) =>
rules.push({ id: nextId(), action: "block", type: "country", value: v }),
);
pushCidrRules(rules, restrictions.allowed_cidrs, "allow");
restrictions.allowed_countries?.forEach((v) =>
rules.push({ id: nextId(), action: "allow", type: "country", value: v }),
);
restrictions.blocked_countries?.forEach((v) =>
rules.push({ id: nextId(), action: "block", type: "country", value: v }),
);
restrictions.allowed_cidrs?.forEach((v) => {
const isIp = v.endsWith("/32");
rules.push({ id: nextId(), action: "allow", type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/32$/, "") : v });
});
restrictions.blocked_cidrs?.forEach((v) => {
const isIp = v.endsWith("/32");
rules.push({ id: nextId(), action: "block", type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/32$/, "") : v });
});
return rules;
}
function rulesToRestrictions(
rules: AccessRule[],
crowdsecMode?: CrowdSecMode,
): AccessRestrictions | undefined {
const allowed_countries: string[] = [];
const blocked_countries: string[] = [];
@@ -141,23 +129,17 @@ function rulesToRestrictions(
if (rule.action === "allow") allowed_countries.push(rule.value);
else blocked_countries.push(rule.value);
} else {
const suffix = rule.value.includes(":") ? "/128" : "/32";
const value =
rule.type === "ip" && !rule.value.includes("/")
? `${rule.value}${suffix}`
: rule.value;
const value = rule.type === "ip" && !rule.value.includes("/") ? `${rule.value}/32` : rule.value;
if (rule.action === "allow") allowed_cidrs.push(value);
else blocked_cidrs.push(value);
}
}
const hasCrowdSec = crowdsecMode != null && crowdsecMode !== CrowdSecMode.OFF;
const hasAny =
allowed_countries.length > 0 ||
blocked_countries.length > 0 ||
allowed_cidrs.length > 0 ||
blocked_cidrs.length > 0 ||
hasCrowdSec;
blocked_cidrs.length > 0;
if (!hasAny) return undefined;
@@ -166,7 +148,6 @@ function rulesToRestrictions(
...(blocked_countries.length > 0 && { blocked_countries }),
...(allowed_cidrs.length > 0 && { allowed_cidrs }),
...(blocked_cidrs.length > 0 && { blocked_cidrs }),
...(hasCrowdSec && { crowdsec_mode: crowdsecMode }),
};
}
@@ -174,7 +155,6 @@ type Props = {
value: AccessRestrictions | undefined;
onChange: (value: AccessRestrictions | undefined) => void;
onValidationChange?: (hasErrors: boolean) => void;
supportsCrowdSec?: boolean;
};
function validateRule(rule: AccessRule): string {
@@ -192,22 +172,13 @@ function validateRule(rule: AccessRule): string {
return "";
}
export const ReverseProxyAccessControlRules = ({
value,
onChange,
onValidationChange,
supportsCrowdSec,
}: Props) => {
export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationChange }: Props) => {
const [rules, dispatch] = useReducer(
rulesReducer,
value,
restrictionsToRules,
);
const [crowdsecMode, setCrowdsecMode] = useState<CrowdSecMode>(
value?.crowdsec_mode ?? CrowdSecMode.OFF,
);
const errors = useMemo(
() => Object.fromEntries(rules.map((r) => [r.id, validateRule(r)])),
[rules],
@@ -225,14 +196,8 @@ export const ReverseProxyAccessControlRules = ({
onValidationChangeRef.current = onValidationChange;
useEffect(() => {
if (!supportsCrowdSec) {
setCrowdsecMode(CrowdSecMode.OFF);
}
}, [supportsCrowdSec]);
useEffect(() => {
onChangeRef.current(rulesToRestrictions(rules, crowdsecMode));
}, [rules, crowdsecMode]);
onChangeRef.current(rulesToRestrictions(rules));
}, [rules]);
useEffect(() => {
onValidationChangeRef.current?.(hasErrors);
@@ -240,13 +205,6 @@ export const ReverseProxyAccessControlRules = ({
return (
<div className={"flex-col flex"}>
{supportsCrowdSec && (
<ReverseProxyCrowdSecIPReputation
value={crowdsecMode}
onChange={setCrowdsecMode}
/>
)}
<div>
<Label>Access Control Rules</Label>
<HelpText>
@@ -343,17 +301,15 @@ export const ReverseProxyAccessControlRules = ({
))}
</div>
)}
<div className="flex gap-2">
<Button
variant="dotted"
className="flex-1"
size="sm"
onClick={() => dispatch({ type: "add" })}
>
<PlusIcon size={14} />
Add Rule
</Button>
</div>
<Button
variant="dotted"
className="w-full"
size="sm"
onClick={() => dispatch({ type: "add" })}
>
<PlusIcon size={14} />
Add Rule
</Button>
</div>
);
};
@@ -1,109 +0,0 @@
import * as React from "react";
import { ReactNode } from "react";
import { Label } from "@components/Label";
import HelpText from "@components/HelpText";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@components/Select";
import { EyeIcon, PowerOffIcon, ShieldCheckIcon } from "lucide-react";
import { HelpTooltip } from "@components/HelpTooltip";
import { CrowdSecMode } from "@/interfaces/ReverseProxy";
import Image from "next/image";
import CrowdSecIconImage from "@/assets/integrations/crowdsec.png";
type Props = {
value: CrowdSecMode;
onChange: (value: CrowdSecMode) => void;
};
type CrowdSecOption = {
label: string;
description?: string;
icon: ReactNode;
};
const CROWDSEC_OPTIONS: Record<CrowdSecMode, CrowdSecOption> = {
[CrowdSecMode.OFF]: {
label: "Disabled",
icon: <PowerOffIcon size={14} />,
},
[CrowdSecMode.ENFORCE]: {
label: "Enforce",
description:
"Blocked IPs are denied immediately. If the bouncer is not yet synced, connections are denied (fail-closed).",
icon: <ShieldCheckIcon size={14} />,
},
[CrowdSecMode.OBSERVE]: {
label: "Observe",
description:
"Blocked IPs are logged but not denied. Use this to evaluate CrowdSec before enforcing.",
icon: <EyeIcon size={14} />,
},
};
export const ReverseProxyCrowdSecIPReputation = ({
value,
onChange,
}: Props) => {
const selected = CROWDSEC_OPTIONS[value];
return (
<div className="flex items-center gap-0 justify-between mb-6">
<div className="flex gap-4">
<div
className={
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70 shrink-0 relative"
}
>
<Image
src={CrowdSecIconImage}
alt={"CrowdSec"}
className={"rounded-[4px]"}
/>
</div>
<div>
<Label>CrowdSec IP Reputation</Label>
<HelpText>
Detect malicious IPs with CrowdSec.{" "}
<b className={"text-white"}>Enforce</b> to block them or{" "}
<b className={"text-white"}>Observe</b> to only log without
blocking.
</HelpText>
</div>
</div>
<Select value={value} onValueChange={(v) => onChange(v as CrowdSecMode)}>
<SelectTrigger className="w-[260px]">
<div className="flex items-center gap-2 whitespace-nowrap">
{selected.icon}
<SelectValue />
</div>
</SelectTrigger>
<SelectContent>
{Object.entries(CROWDSEC_OPTIONS).map(([mode, config]) => (
<SelectItem
key={mode}
value={mode}
extra={
config.description ? (
<HelpTooltip
triggerClassName="ml-[0.01rem]"
align="center"
side="right"
content={<>{config.description}</>}
/>
) : undefined
}
>
<span className="whitespace-nowrap">{config.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
@@ -46,7 +46,7 @@ export default function ReverseProxyHTTPTargets({
}: Readonly<Props>) {
return (
<div>
<Label>HTTPS Targets</Label>
<Label>HTTP/S Targets</Label>
<HelpText>
Add one or more devices running your service or resources to make it
publicly accessible.
@@ -93,7 +93,10 @@ export default function ReverseProxyHTTPTargets({
/>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="default-outline" className="!px-3">
<Button
variant="default-outline"
className="!px-3"
>
<MoreVertical size={16} className="shrink-0" />
</Button>
</DropdownMenuTrigger>
@@ -101,7 +104,9 @@ export default function ReverseProxyHTTPTargets({
className="w-auto min-w-[200px]"
align="end"
>
<DropdownMenuItem onClick={() => onEditTarget(index)}>
<DropdownMenuItem
onClick={() => onEditTarget(index)}
>
<div className="flex gap-3 items-center">
<Edit size={14} className="shrink-0" />
Edit Target
@@ -112,7 +117,10 @@ export default function ReverseProxyHTTPTargets({
onClick={() => onRemoveTarget(index)}
>
<div className="flex gap-3 items-center">
<MinusCircleIcon size={14} className="shrink-0" />
<MinusCircleIcon
size={14}
className="shrink-0"
/>
Remove Target
</div>
</DropdownMenuItem>
@@ -143,7 +151,10 @@ export default function ReverseProxyHTTPTargets({
variant="warning"
className="mt-3"
icon={
<AlertTriangle size={14} className="shrink-0 relative top-[3px]" />
<AlertTriangle
size={14}
className="shrink-0 relative top-[3px]"
/>
}
>
There are currently no resources in your network{" "}
@@ -66,7 +66,7 @@ export default function ReverseProxyLayer4Content({
<Label>
Listen Port
<HelpTooltip
className={isListenPortSupported ? "max-w-sm" : "max-w-xs"}
className={"max-w-sm"}
content={
isListenPortSupported
? "Enter the public listen port this service will be reachable on."
@@ -590,7 +590,6 @@ export default function ReverseProxyModal({
value={accessRestrictions}
onChange={setAccessRestrictions}
onValidationChange={setAccessControlHasErrors}
supportsCrowdSec={selectedDomain?.supports_crowdsec}
/>
</div>
</TabsContent>
@@ -31,7 +31,7 @@ type ServiceModeConfig = {
export const SERVICE_MODES: Record<ServiceMode, ServiceModeConfig> = {
[ServiceMode.HTTP]: {
label: "HTTPS Service",
label: "HTTP/S Service",
description:
"Reverse proxy with path routing and built-in authentication (SSO, PIN, password). Typically used for web applications and APIs.",
icon: <Globe size={14} />,
@@ -64,7 +64,7 @@ export const ReverseProxyServiceModeSelector = ({
}: Props) => {
const selected = value ?? ServiceMode.HTTP;
const selectedMode = SERVICE_MODES[selected];
const isL4Supported = domain?.supports_custom_ports !== undefined;
const isL4Supported = domain?.supports_custom_ports === true;
// Reset to HTTP if the current L4 mode becomes unsupported (e.g. domain changed)
useEffect(() => {
@@ -7,8 +7,6 @@ import {
Mail,
Network,
RectangleEllipsis,
ShieldAlert,
ShieldOff,
Users,
} from "lucide-react";
import * as React from "react";
@@ -71,26 +69,6 @@ export const ReverseProxyEventsAuthMethodCell = ({ event }: Props) => {
icon: <GlobeOff size={12} />,
label: "Geo Unavailable",
};
case "crowdsec_ban":
return {
icon: <ShieldAlert size={12} />,
label: "CrowdSec Ban",
};
case "crowdsec_captcha":
return {
icon: <ShieldAlert size={12} />,
label: "CrowdSec Captcha",
};
case "crowdsec_throttle":
return {
icon: <ShieldAlert size={12} />,
label: "CrowdSec Throttle",
};
case "crowdsec_unavailable":
return {
icon: <ShieldOff size={12} />,
label: "CrowdSec Unavailable",
};
default:
return {
icon: null,
Loaded 20 of 35 files, more files were not shown because too many files have changed in this diff. Show more