mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38e14a6c64 | ||
|
|
b79c6615b4 | ||
|
|
5d4e491611 | ||
|
|
9b1f920863 |
No files matched your search
Generated
+74
-68
@@ -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": {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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");
|
||||
|
||||
@@ -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 />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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;
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
Reference in new issue
Block a user