Compare commits

...
4 Commits
Author SHA1 Message Date
Eduard Gert 38e14a6c64 Allow delete groups issued by jwt (#487) 2025-09-10 14:41:19 +02:00
Maycon Santos b79c6615b4 Add user approval feature (#486)
implements a user approval feature that allows administrators to manually approve new users before they can access the system. The feature adds approval workflow controls and error handling for blocked/pending users.

Adds user approval toggle in authentication settings
Implements approve/reject actions for pending users in the users table
Creates error page for blocked/pending approval scenarios
2025-09-02 15:25:30 +02:00
hakansa 5d4e491611 Add skip_auto_apply feature to exit nodes and update related components (#484) 2025-08-20 13:11:45 +02:00
Eduard Gert 9b1f920863 Update dependencies (#483) 2025-08-19 17:32:22 +02:00
27 changed files with 672 additions and 154 deletions

No files matched your search

+74 -68
View File
@@ -513,9 +513,9 @@
"license": "MIT"
},
"node_modules/@next/env": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.28.tgz",
"integrity": "sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.32.tgz",
"integrity": "sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -529,9 +529,9 @@
}
},
"node_modules/@next/eslint-plugin-next/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"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": {
@@ -578,9 +578,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz",
"integrity": "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.32.tgz",
"integrity": "sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==",
"cpu": [
"arm64"
],
@@ -594,9 +594,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz",
"integrity": "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.32.tgz",
"integrity": "sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg==",
"cpu": [
"x64"
],
@@ -610,9 +610,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz",
"integrity": "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.32.tgz",
"integrity": "sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA==",
"cpu": [
"arm64"
],
@@ -626,9 +626,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz",
"integrity": "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.32.tgz",
"integrity": "sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ==",
"cpu": [
"arm64"
],
@@ -642,9 +642,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz",
"integrity": "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.32.tgz",
"integrity": "sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw==",
"cpu": [
"x64"
],
@@ -658,9 +658,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz",
"integrity": "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.32.tgz",
"integrity": "sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg==",
"cpu": [
"x64"
],
@@ -674,9 +674,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz",
"integrity": "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.32.tgz",
"integrity": "sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg==",
"cpu": [
"arm64"
],
@@ -690,9 +690,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz",
"integrity": "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.32.tgz",
"integrity": "sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA==",
"cpu": [
"ia32"
],
@@ -706,9 +706,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz",
"integrity": "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.32.tgz",
"integrity": "sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==",
"cpu": [
"x64"
],
@@ -2809,9 +2809,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"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": {
@@ -3396,9 +3396,10 @@
"dev": true
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"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": "^1.0.0",
"concat-map": "0.0.1"
@@ -4532,14 +4533,16 @@
}
},
"node_modules/es-set-tostringtag": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
"integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.2",
"has-tostringtag": "^1.0.0",
"hasown": "^2.0.0"
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -5310,14 +5313,16 @@
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -5683,12 +5688,13 @@
}
},
"node_modules/has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.2"
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -6783,12 +6789,12 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
"node_modules/next": {
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.28.tgz",
"integrity": "sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA==",
"version": "14.2.32",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.32.tgz",
"integrity": "sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg==",
"license": "MIT",
"dependencies": {
"@next/env": "14.2.28",
"@next/env": "14.2.32",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
@@ -6803,15 +6809,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.28",
"@next/swc-darwin-x64": "14.2.28",
"@next/swc-linux-arm64-gnu": "14.2.28",
"@next/swc-linux-arm64-musl": "14.2.28",
"@next/swc-linux-x64-gnu": "14.2.28",
"@next/swc-linux-x64-musl": "14.2.28",
"@next/swc-win32-arm64-msvc": "14.2.28",
"@next/swc-win32-ia32-msvc": "14.2.28",
"@next/swc-win32-x64-msvc": "14.2.28"
"@next/swc-darwin-arm64": "14.2.32",
"@next/swc-darwin-x64": "14.2.32",
"@next/swc-linux-arm64-gnu": "14.2.32",
"@next/swc-linux-arm64-musl": "14.2.32",
"@next/swc-linux-x64-gnu": "14.2.32",
"@next/swc-linux-x64-musl": "14.2.32",
"@next/swc-win32-arm64-msvc": "14.2.32",
"@next/swc-win32-ia32-msvc": "14.2.32",
"@next/swc-win32-x64-msvc": "14.2.32"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -8565,9 +8571,9 @@
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
+15 -12
View File
@@ -289,6 +289,7 @@ function UserInformationCard({ user }: Readonly<{ user: User }>) {
const neverLoggedIn = dayjs(user.last_login).isBefore(
dayjs().subtract(1000, "years"),
);
const isPendingApproval = user?.pending_approval;
return (
<Card>
@@ -328,18 +329,20 @@ function UserInformationCard({ user }: Readonly<{ user: User }>) {
{!isServiceUser && (
<>
{!user.is_current && user.role != Role.Owner && (
<Card.ListItem
tooltip={false}
label={
<>
<Ban size={16} />
Block User
</>
}
value={<UserBlockCell user={user} isUserPage={true} />}
/>
)}
{!user.is_current &&
user.role != Role.Owner &&
!isPendingApproval && (
<Card.ListItem
tooltip={false}
label={
<>
<Ban size={16} />
Block User
</>
}
value={<UserBlockCell user={user} isUserPage={true} />}
/>
)}
<Card.ListItem
label={
+115
View File
@@ -0,0 +1,115 @@
"use client";
import { useOidc } from "@axa-fr/react-oidc";
import Button from "@components/Button";
import Paragraph from "@components/Paragraph";
import loadConfig from "@utils/config";
import { ArrowRightIcon, RefreshCw } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
const config = loadConfig();
export default function ErrorPage() {
const { logout, isAuthenticated } = useOidc();
const router = useRouter();
const searchParams = useSearchParams();
const [error, setError] = useState<{
code: number;
message: string;
type: string;
} | null>(null);
useEffect(() => {
// Get error details from URL params
const code = searchParams.get("code");
const message = searchParams.get("message");
const type = searchParams.get("type");
if (code && message) {
setError({
code: parseInt(code),
message: decodeURIComponent(message),
type: type || "error",
});
}
}, [searchParams]);
const handleLogout = () => {
// Use the same logout pattern as OIDCError
logout("/", { client_id: config.clientId });
};
const handleRetry = () => {
router.push("/");
};
if (!isAuthenticated) {
// If not authenticated, redirect to home
router.push("/");
return null;
}
const isBlockedUser =
error?.code === 403 && error?.message?.toLowerCase().includes("blocked");
const isPendingApproval =
error?.code === 403 &&
error?.message?.toLowerCase().includes("pending approval");
const getTitle = () => {
if (isBlockedUser) return "User Account Blocked";
if (isPendingApproval) return "User Approval Pending";
return "Access Error";
};
const getDescription = () => {
if (isBlockedUser) {
return "Your access has been blocked by the NetBird account administrator, possibly due to new user approval requirements or security policies. Please contact your administrator to regain access.";
}
if (isPendingApproval) {
return "Your account is pending approval from an administrator. Please wait for approval before accessing the dashboard.";
}
return "An error occurred while trying to access the dashboard. Please try again or contact your administrator.";
};
return (
<div className="flex items-center justify-center flex-col h-screen max-w-xl mx-auto">
<div className="bg-nb-gray-930 mb-3 border border-nb-gray-900 h-12 w-12 rounded-md flex items-center justify-center">
<NetBirdIcon size={23} />
</div>
<h1 className="text-center mt-2">{getTitle()}</h1>
<Paragraph className="text-center mt-2 block">
{getDescription()}
</Paragraph>
{error && (
<div className="bg-nb-gray-930 border border-nb-gray-800 rounded-md p-4 mt-4 max-w-md font-mono mb-2">
<div className="text-center text-sm text-netbird">
<div>response_message: {error.message}</div>
</div>
</div>
)}
<Paragraph className="text-center mt-2 text-sm">
If you believe this is an error, please contact your administrator.
</Paragraph>
<div className="mt-5 space-y-3">
{!isBlockedUser && !isPendingApproval && (
<Button variant="default-outline" size="sm" onClick={handleRetry}>
<RefreshCw size={16} className="mr-2" />
Try Again
</Button>
)}
<Button variant="primary" size="sm" onClick={handleLogout}>
{isBlockedUser || isPendingApproval ? "Sign Out" : "Logout"}
<ArrowRightIcon size={16} />
</Button>
</div>
</div>
);
}
+3 -3
View File
@@ -8,12 +8,12 @@ export const NotificationCountBadge = ({ count = 0 }: Props) => {
return count ? (
<div
className={cn(
count <= 9 ? "w-5 h-5" : "py-2.5 px-2",
"relative bg-netbird flex items-center justify-center rounded-full text-white !leading-[0] text-xs font-semibold",
count <= 9 ? "w-4 h-4" : "py-2 px-1.5",
"relative bg-netbird flex items-center justify-center rounded-full text-white !leading-[0] text-[0.6rem] font-semibold",
)}
>
<span className="animate-ping absolute left-0 inline-flex h-full w-full rounded-full bg-netbird opacity-20"></span>
{count || 0}
<span className={"relative -left-[0.5px]"}>{count || 0}</span>
</div>
) : null;
};
+2
View File
@@ -61,6 +61,7 @@ export default function RoutesProvider({ children }: Readonly<Props>) {
: toUpdate.access_control_groups ??
route.access_control_groups ??
undefined,
skip_auto_apply: toUpdate.skip_auto_apply ?? route.skip_auto_apply ?? true,
},
`/${route.id}`,
)
@@ -94,6 +95,7 @@ export default function RoutesProvider({ children }: Readonly<Props>) {
masquerade: route.masquerade,
groups: route.groups || [],
access_control_groups: route?.access_control_groups || undefined,
skip_auto_apply: route.skip_auto_apply ?? true,
})
.then((route) => {
mutate("/routes");
+9 -3
View File
@@ -62,18 +62,24 @@ const UserProfileProvider = ({ children }: Props) => {
}
}, [user, error, users, isLoading, isAllUsersLoading]);
const data = useMemo(() => {
return {
loggedInUser,
};
}, [loggedInUser]);
return !isLoading && loggedInUser ? (
// Show loading only when we're still loading and don't have user data
if (isLoading || !loggedInUser) {
return <FullScreenLoading />;
}
// For blocked or pending approval users, we still need to provide the context
// so they can access their user data on the blocked page
return (
<UserProfileContext.Provider value={data}>
<PermissionsProvider user={loggedInUser}>{children}</PermissionsProvider>
</UserProfileContext.Provider>
) : (
<FullScreenLoading />
);
};
+1
View File
@@ -7,6 +7,7 @@ export interface Account {
settings: {
extra: {
peer_approval_enabled: boolean;
user_approval_required: boolean;
};
peer_login_expiration_enabled: boolean;
peer_login_expiration: number;
+1
View File
@@ -12,6 +12,7 @@ export interface Route {
groups: string[];
keep_route?: boolean;
access_control_groups?: string[];
skip_auto_apply?: boolean;
// Frontend only
peer_groups?: string[];
routesGroups?: string[];
+1
View File
@@ -10,6 +10,7 @@ export interface User {
is_current?: boolean;
is_service_user?: boolean;
is_blocked?: boolean;
pending_approval?: boolean;
last_login?: Date;
permissions: Permissions;
}
@@ -253,6 +253,22 @@ export default function ActivityDescription({ event }: Props) {
</div>
);
if (event.activity_code == "user.approve")
return (
<div className={"inline"}>
User <Value>{event.meta.username}</Value>{" "}
<Value>{event.meta.email}</Value> was approved
</div>
);
if (event.activity_code == "user.reject")
return (
<div className={"inline"}>
User <Value>{event.meta.username}</Value>{" "}
<Value>{event.meta.email}</Value> was rejected
</div>
);
/**
* Service User
*/
+1
View File
@@ -19,6 +19,7 @@ const ACTION_COLOR_MAPPING: Record<string, ActionStatus> = {
delete: ActionStatus.ERROR,
revoke: ActionStatus.ERROR,
block: ActionStatus.ERROR,
reject: ActionStatus.ERROR,
// Warning actions
overuse: ActionStatus.WARNING,
@@ -15,7 +15,7 @@ type Props = {
export const ExitNodeDropdownButton = ({ peer }: Props) => {
const [modal, setModal] = useState(false);
const hasExitNodes = useHasExitNodes(peer);
const exitNodeInfo = useHasExitNodes(peer);
const { permission } = usePermissions();
return (
@@ -25,7 +25,7 @@ export const ExitNodeDropdownButton = ({ peer }: Props) => {
disabled={!permission.routes.create}
>
<div className={"flex gap-3 items-center w-full"}>
{hasExitNodes ? (
{exitNodeInfo.hasExitNode ? (
<>
<IconCirclePlus size={14} className={"shrink-0"} />
<div className={"flex justify-between items-center w-full"}>
+16 -12
View File
@@ -8,18 +8,22 @@ type Props = {
peer: Peer;
};
export const ExitNodePeerIndicator = ({ peer }: Props) => {
const hasExitNode = useHasExitNodes(peer);
const exitNodeInfo = useHasExitNodes(peer);
return hasExitNode ? (
<FullTooltip
content={
<div className={"text-xs max-w-xs"}>
This peer is an exit node. Traffic from the configured distribution
groups will be routed through this peer.
</div>
}
>
<IconDirectionSign size={15} className={"text-yellow-400 shrink-0"} />
if (!exitNodeInfo.hasExitNode) {
return null;
}
const tooltipContent = exitNodeInfo.skipAutoApply === false
? "This peer is an auto-applied exit node. Traffic from the configured distribution groups will be routed through this peer."
: "This peer is an exit node. Traffic from the configured distribution groups will be routed through this peer.";
return (
<FullTooltip content={<div className={"text-xs max-w-xs"}>{tooltipContent}</div>}>
<IconDirectionSign
size={15}
className={`shrink-0 ${exitNodeInfo.skipAutoApply === false ? "text-green-400" : "text-yellow-400"}`}
/>
</FullTooltip>
) : null;
);
};
+19 -6
View File
@@ -3,7 +3,12 @@ import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Peer } from "@/interfaces/Peer";
import { Route } from "@/interfaces/Route";
export const useHasExitNodes = (peer?: Peer) => {
export interface ExitNodeInfo {
hasExitNode: boolean;
skipAutoApply?: boolean;
}
export const useHasExitNodes = (peer?: Peer): ExitNodeInfo => {
const { isOwnerOrAdmin } = useLoggedInUser();
const { data: routes } = useFetchApi<Route[]>(
`/routes`,
@@ -11,9 +16,17 @@ export const useHasExitNodes = (peer?: Peer) => {
true,
isOwnerOrAdmin,
);
return peer
? routes?.some(
(route) => route?.peer === peer.id && route?.network === "0.0.0.0/0",
) || false
: false;
if (!peer || !routes) {
return { hasExitNode: false };
}
const exitNodeRoute = routes.find(
(route) => route?.peer === peer.id && route?.network === "0.0.0.0/0",
);
return {
hasExitNode: !!exitNodeRoute,
skipAutoApply: exitNodeRoute?.skip_auto_apply,
};
};
@@ -17,7 +17,7 @@ type Props = {
export const PeerNetworkRoutesSection = ({ peer }: Props) => {
const { peerRoutes, isLoading } = usePeerRoutes({ peer });
const hasExitNodes = useHasExitNodes(peer);
const exitNodeInfo = useHasExitNodes(peer);
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
@@ -34,7 +34,7 @@ export const PeerNetworkRoutesSection = ({ peer }: Props) => {
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div className={"gap-4 flex"}>
<AddExitNodeButton peer={peer} firstTime={!hasExitNodes} />
<AddExitNodeButton peer={peer} firstTime={!exitNodeInfo.hasExitNode} />
<AddRouteDropdownButton />
</div>
</div>
+53
View File
@@ -0,0 +1,53 @@
import { ToggleSwitch } from "@components/ToggleSwitch";
import React, { useMemo } from "react";
import { useSWRConfig } from "swr";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { useRoutes } from "@/contexts/RoutesProvider";
import { Route } from "@/interfaces/Route";
type Props = {
route: Route;
};
export default function RouteAutoApplyCell({ route }: Readonly<Props>) {
const { permission } = usePermissions();
const { updateRoute } = useRoutes();
const { mutate } = useSWRConfig();
const isExitNode = useMemo(() => route.network === "0.0.0.0/0", [route]);
const isChecked = useMemo(() => {
// Checked means Auto Apply is ON, which maps to skip_auto_apply === false
return route.skip_auto_apply === false;
}, [route]);
const update = async (checked: boolean) => {
// When toggled ON (checked === true), we want skip_auto_apply = false
const nextSkipAutoApply = !checked;
updateRoute(
route,
{ skip_auto_apply: nextSkipAutoApply },
() => {
mutate("/routes");
},
checked
? "Auto Apply was enabled for the route"
: "Auto Apply was disabled for the route",
);
};
if (!isExitNode) return null;
return (
<div className={"flex items-center"}>
<ToggleSwitch
checked={isChecked}
size={"small"}
onClick={() => update(!isChecked)}
disabled={!permission.routes.update}
/>
</div>
);
}
+16
View File
@@ -227,6 +227,7 @@ export function RouteModalContent({
const [enabled, setEnabled] = useState<boolean>(true);
const [metric, setMetric] = useState("9999");
const [masquerade, setMasquerade] = useState<boolean>(true);
const [isForced, setIsForced] = useState<boolean>(true);
const isNonLinuxRoutingPeer = useMemo(() => {
if (!routingPeer) return false;
@@ -304,6 +305,7 @@ export function RouteModalContent({
masquerade: useSinglePeer && isNonLinuxRoutingPeer ? true : masquerade,
groups: groupIds,
access_control_groups: accessControlGroupIds || undefined,
skip_auto_apply: !isForced,
},
onSuccess,
);
@@ -718,6 +720,20 @@ export function RouteModalContent({
helpText={"Use this switch to enable or disable the route."}
/>
{exitNode && (
<FancyToggleSwitch
value={isForced}
onChange={setIsForced}
label={
<>
<IconDirectionSign size={15} />
Auto Apply Route
</>
}
helpText={"Automatically apply this exit node to your distribution groups. This requires NetBird client v0.55.0 or higher."}
/>
)}
{!exitNode && (
<RoutingPeerMasqueradeSwitch
value={masquerade}
+15
View File
@@ -7,6 +7,7 @@ import { GroupedRoute, Route } from "@/interfaces/Route";
import RouteAccessControlGroups from "@/modules/routes/RouteAccessControlGroups";
import RouteActionCell from "@/modules/routes/RouteActionCell";
import RouteActiveCell from "@/modules/routes/RouteActiveCell";
import RouteAutoApplyCell from "@/modules/routes/RouteAutoApplyCell";
import RouteDistributionGroupsCell from "@/modules/routes/RouteDistributionGroupsCell";
import RouteMetricCell from "@/modules/routes/RouteMetricCell";
import RoutePeerCell from "@/modules/routes/RoutePeerCell";
@@ -77,6 +78,15 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
},
cell: ({ row }) => <RouteAccessControlGroups route={row.original} />,
},
{
id: "skipAutoApply",
accessorKey: "skip_auto_apply",
header: ({ column }) => {
return <DataTableHeader column={column}>Auto Apply</DataTableHeader>;
},
cell: ({ row }) => <RouteAutoApplyCell route={row.original} />,
sortingFn: "basic",
},
{
id: "group_names",
accessorFn: (row) => {
@@ -104,6 +114,10 @@ export default function RouteTable({ row }: Props) {
desc: true,
},
]);
const hasAtLeastOneExitNode = useMemo(() => {
return row.routes?.some((route) => route.network === "0.0.0.0/0");
}, [row.routes]);
const data = useMemo(() => {
if (!row.routes) return [];
@@ -144,6 +158,7 @@ export default function RouteTable({ row }: Props) {
domains: false,
domain_search: false,
network: false,
skipAutoApply: !!hasAtLeastOneExitNode,
}}
setSorting={setSorting}
columns={RouteTableColumns}
+18
View File
@@ -20,6 +20,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import { Textarea } from "@components/Textarea";
import { DomainsTooltip } from "@components/ui/DomainListBadge";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { IconDirectionSign } from "@tabler/icons-react";
import { cn } from "@utils/helpers";
import { uniqBy } from "lodash";
import {
@@ -193,6 +194,7 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
const [masquerade, setMasquerade] = useState<boolean>(
route?.masquerade ?? true,
);
const [isForced, setIsForced] = useState<boolean>(route?.skip_auto_apply === false);
// Refs to manage focus on tab change
const networkRangeRef = useRef<HTMLInputElement>(null);
@@ -257,6 +259,7 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
masquerade: useSinglePeer && isNonLinuxRoutingPeer ? true : masquerade,
groups: groupIds,
access_control_groups: accessControlGroupIds || undefined,
skip_auto_apply: !isForced,
},
(r) => {
onSuccess && onSuccess(r);
@@ -456,6 +459,21 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
}
helpText={"Use this switch to enable or disable the route."}
/>
{isExitNode && (
<FancyToggleSwitch
value={isForced}
onChange={setIsForced}
label={
<>
<IconDirectionSign size={15} />
Auto Apply Route
</>
}
helpText={"Automatically apply this exit node to your distribution groups. This requires NetBird client v0.55.0 or higher."}
/>
)}
{!isExitNode && (
<RoutingPeerMasqueradeSwitch
value={masquerade}
@@ -23,6 +23,7 @@ import {
CalendarClock,
ExternalLinkIcon,
ShieldIcon,
ShieldUserIcon,
TimerResetIcon,
} from "lucide-react";
import React, { useState } from "react";
@@ -52,6 +53,19 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
}
});
/**
* User approval required
*/
const [userApprovalRequired, setUserApprovalRequired] = useState<boolean>(
() => {
try {
return account?.settings?.extra?.user_approval_required || false;
} catch (error) {
return false;
}
},
);
// Peer Expiration
const [
loginExpiration,
@@ -86,6 +100,7 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
const { hasChanges, updateRef } = useHasChanges([
peerApproval,
userApprovalRequired,
loginExpiration,
expiresIn,
expireInterval,
@@ -118,6 +133,7 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
extra: {
...account.settings?.extra,
peer_approval_enabled: peerApproval,
user_approval_required: userApprovalRequired,
},
},
} as Account)
@@ -125,6 +141,7 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
mutate("/accounts");
updateRef([
peerApproval,
userApprovalRequired,
loginExpiration,
expiresIn,
expireInterval,
@@ -181,6 +198,27 @@ export default function AuthenticationTab({ account }: Readonly<Props>) {
</div>
<div className={"flex flex-col gap-6 w-full mt-8 mb-3"}>
<div className={"flex flex-col"}>
<FancyToggleSwitch
value={userApprovalRequired}
onChange={setUserApprovalRequired}
dataCy={"user-approval-required"}
label={
<>
<ShieldUserIcon size={15} />
User Approval Required
</>
}
helpText={
<>
Require manual approval for new users joining via <br />
domain matching. Users will be blocked until approved.
</>
}
disabled={!permission.settings.update}
/>
</div>
<div className={"flex flex-col"}>
<FancyToggleSwitch
value={loginExpiration}
Loaded 20 of 27 files, more files were not shown because too many files have changed in this diff. Show more