Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b79c6615b4 | ||
|
|
5d4e491611 | ||
|
|
9b1f920863 |
142
package-lock.json
generated
142
package-lock.json
generated
@@ -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={
|
||||
|
||||
115
src/app/error/page.tsx
Normal file
115
src/app/error/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
53
src/modules/routes/RouteAutoApplyCell.tsx
Normal file
53
src/modules/routes/RouteAutoApplyCell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -6,6 +6,7 @@ import DataTableHeader from "@components/table/DataTableHeader";
|
||||
import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
|
||||
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
|
||||
import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import { NotificationCountBadge } from "@components/ui/NotificationCountBadge";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
|
||||
@@ -88,6 +89,12 @@ export const UsersTableColumns: ColumnDef<User>[] = [
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "approval_required",
|
||||
accessorKey: "approval_required",
|
||||
sortingFn: "basic",
|
||||
accessorFn: (u) => u?.pending_approval,
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
@@ -127,6 +134,8 @@ export default function UsersTable({
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const pendingApprovalCount =
|
||||
users?.filter((u) => u.pending_approval).length || 0;
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
@@ -139,6 +148,7 @@ export default function UsersTable({
|
||||
data={users}
|
||||
columnVisibility={{
|
||||
is_current: false,
|
||||
approval_required: false,
|
||||
}}
|
||||
onRowClick={(row) => {
|
||||
router.push(`/team/user?id=${row.original.id}`);
|
||||
@@ -185,18 +195,56 @@ export default function UsersTable({
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(table) => (
|
||||
<>
|
||||
<DataTableRowsPerPage table={table} disabled={users?.length == 0} />
|
||||
<DataTableRefreshButton
|
||||
isDisabled={users?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/users?service_user=false");
|
||||
mutate("/groups");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(table) => {
|
||||
if (
|
||||
pendingApprovalCount == 0 &&
|
||||
table.getColumn("approval_required")?.getFilterValue() === true
|
||||
) {
|
||||
table.setColumnFilters([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{pendingApprovalCount > 0 && (
|
||||
<Button
|
||||
disabled={users?.length == 0}
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
let current =
|
||||
table.getColumn("approval_required")?.getFilterValue() ===
|
||||
undefined
|
||||
? true
|
||||
: undefined;
|
||||
|
||||
table.setColumnFilters([
|
||||
{
|
||||
id: "approval_required",
|
||||
value: current,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
variant={
|
||||
table.getColumn("approval_required")?.getFilterValue() ===
|
||||
true
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
Pending Approvals
|
||||
<NotificationCountBadge count={pendingApprovalCount} />
|
||||
</Button>
|
||||
)}
|
||||
<DataTableRowsPerPage table={table} disabled={users?.length == 0} />
|
||||
<DataTableRefreshButton
|
||||
isDisabled={users?.length == 0}
|
||||
onClick={() => {
|
||||
mutate("/users?service_user=false");
|
||||
mutate("/groups");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Button from "@components/Button";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { isNetBirdHosted } from "@utils/netbird";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Trash2, XCircle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -36,6 +36,42 @@ export default function UserActionCell({
|
||||
});
|
||||
};
|
||||
|
||||
const approveUser = async () => {
|
||||
const name = user.name || "User";
|
||||
notify({
|
||||
title: `'${name}' approved`,
|
||||
description: "User was successfully approved.",
|
||||
promise: userRequest.post({}, `/${user.id}/approve`).then(() => {
|
||||
mutate(`/users?service_user=${serviceUser}`);
|
||||
}),
|
||||
loadingMessage: "Approving the user...",
|
||||
});
|
||||
};
|
||||
|
||||
const rejectUser = async () => {
|
||||
const name = user.name || "User";
|
||||
const choice = await confirm({
|
||||
title: `Reject '${name}'?`,
|
||||
description:
|
||||
"Rejecting this user will remove them from the account permanently. This action cannot be undone.",
|
||||
confirmText: "Reject",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
maxWidthClass: "max-w-md",
|
||||
});
|
||||
if (!choice) return;
|
||||
|
||||
notify({
|
||||
title: `'${name}' rejected`,
|
||||
description: "User was successfully rejected and removed.",
|
||||
promise: userRequest.del("", `/${user.id}/reject`).then(() => {
|
||||
mutate(`/users?service_user=${serviceUser}`);
|
||||
}),
|
||||
|
||||
loadingMessage: "Rejecting the user...",
|
||||
});
|
||||
};
|
||||
|
||||
const openConfirm = async () => {
|
||||
const name = user.name || "User";
|
||||
const choice = await confirm({
|
||||
@@ -44,6 +80,7 @@ export default function UserActionCell({
|
||||
"Deleting this user will remove their devices and remove dashboard access. This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
maxWidthClass: "max-w-md",
|
||||
type: "danger",
|
||||
});
|
||||
if (!choice) return;
|
||||
@@ -55,21 +92,50 @@ export default function UserActionCell({
|
||||
return user.is_current;
|
||||
}, [permission.users.delete, user.is_current]);
|
||||
|
||||
const isPendingApproval = user.pending_approval;
|
||||
const canManageUsers = permission.users.update;
|
||||
|
||||
return (
|
||||
<div className={"flex justify-end pr-4 items-center gap-4"}>
|
||||
{!serviceUser && isNetBirdHosted() && (
|
||||
<div className={"flex justify-end pr-4 items-center gap-2"}>
|
||||
{!serviceUser && isNetBirdHosted() && !isPendingApproval && (
|
||||
<UserResendInviteButton user={user} />
|
||||
)}
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={openConfirm}
|
||||
data-cy={"delete-user"}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{isPendingApproval && canManageUsers && (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
onClick={approveUser}
|
||||
data-cy={"approve-user"}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"xs"}
|
||||
className={"!px-3"}
|
||||
onClick={rejectUser}
|
||||
data-cy={"reject-user"}
|
||||
>
|
||||
<XCircle size={14} />
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isPendingApproval && (
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={openConfirm}
|
||||
data-cy={"delete-user"}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ export default function UserBlockCell({ user, isUserPage = false }: Props) {
|
||||
});
|
||||
};
|
||||
|
||||
if (user?.pending_approval) return;
|
||||
|
||||
return !disabled ? (
|
||||
<div className={"flex"}>
|
||||
<ToggleSwitch
|
||||
|
||||
@@ -10,6 +10,27 @@ export default function UserNameCell({ user }: Readonly<Props>) {
|
||||
const status = user.status;
|
||||
const isCurrent = user.is_current;
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (user?.pending_approval) {
|
||||
return {
|
||||
color: "bg-netbird text-netbird-950",
|
||||
icon: <Clock size={12} />,
|
||||
};
|
||||
}
|
||||
if (status === "blocked") {
|
||||
return { color: "bg-red-500 text-red-100", icon: <Ban size={12} /> };
|
||||
}
|
||||
if (status === "invited") {
|
||||
return {
|
||||
color: "bg-yellow-400 text-yellow-900",
|
||||
icon: <Clock size={12} />,
|
||||
};
|
||||
}
|
||||
return { color: "bg-gray-400", icon: <Clock size={12} /> };
|
||||
};
|
||||
|
||||
const { color, icon } = getStatusIcon();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex gap-4 px-2 py-1 items-center")}
|
||||
@@ -29,12 +50,10 @@ export default function UserNameCell({ user }: Readonly<Props>) {
|
||||
<div
|
||||
className={cn(
|
||||
"w-5 h-5 absolute -right-1 -bottom-1 bg-nb-gray-930 rounded-full flex items-center justify-center border-2 border-nb-gray-950",
|
||||
status == "invited" && "bg-yellow-400 text-yellow-900",
|
||||
status == "blocked" && "bg-red-500 text-red-100",
|
||||
color,
|
||||
)}
|
||||
>
|
||||
{status == "invited" && <Clock size={12} />}
|
||||
{status == "blocked" && <Ban size={12} />}
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { ExternalLinkIcon, HelpCircle } from "lucide-react";
|
||||
import React from "react";
|
||||
import { User } from "@/interfaces/User";
|
||||
|
||||
@@ -8,23 +11,73 @@ type Props = {
|
||||
|
||||
export default function UserStatusCell({ user }: Readonly<Props>) {
|
||||
const status = user.status;
|
||||
const isPendingApproval = user.pending_approval;
|
||||
|
||||
const getStatusDisplay = () => {
|
||||
if (isPendingApproval) {
|
||||
return { text: "Pending Approval", color: "bg-netbird" };
|
||||
}
|
||||
if (status === "blocked") {
|
||||
return { text: "Blocked", color: "bg-red-500" };
|
||||
}
|
||||
if (status === "invited") {
|
||||
return { text: "Pending", color: "bg-yellow-400" };
|
||||
}
|
||||
if (status === "active") {
|
||||
return { text: "Active", color: "bg-green-500" };
|
||||
}
|
||||
return { text: status || "Unknown", color: "bg-gray-400" };
|
||||
};
|
||||
|
||||
const { text, color } = getStatusDisplay();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex gap-2.5 items-center text-nb-gray-300 text-sm")}
|
||||
data-cy={"user-status-cell"}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
status == "invited" && "bg-yellow-400",
|
||||
status == "blocked" && "bg-red-500",
|
||||
status == "active" && "bg-green-500",
|
||||
)}
|
||||
></span>
|
||||
{status == "invited" && "Pending"}
|
||||
{status == "blocked" && "Blocked"}
|
||||
{status == "active" && "Active"}
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"max-w-xs text-xs flex flex-col gap-2"}>
|
||||
<div>
|
||||
This user needs to be approved by an administrator before it can
|
||||
join your organization.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
If you want to disable approval for new users, go to{" "}
|
||||
<InlineLink href={"/settings?tab=authentication"}>
|
||||
Settings
|
||||
</InlineLink>{" "}
|
||||
and disable{" "}
|
||||
<span className={"font-medium text-white"}>
|
||||
{"'User Approval Required'"}
|
||||
</span>
|
||||
.
|
||||
</div>
|
||||
<div>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/approve-users"}
|
||||
target={"_blank"}
|
||||
>
|
||||
User Approval <ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
interactive={true}
|
||||
side="right"
|
||||
disabled={!isPendingApproval}
|
||||
>
|
||||
<div
|
||||
className={cn("flex gap-2.5 items-center text-nb-gray-300 text-sm")}
|
||||
data-cy={"user-status-cell"}
|
||||
>
|
||||
<span className={cn("h-2 w-2 rounded-full", color)}></span>
|
||||
{text}
|
||||
{isPendingApproval && (
|
||||
<HelpCircle size={14} className="text-netbird cursor-help" />
|
||||
)}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ export function useApiErrorHandling(ignoreError = false) {
|
||||
const { login } = useOidc();
|
||||
const currentPath = usePathname();
|
||||
const { setError } = useErrorBoundary();
|
||||
|
||||
if (ignoreError)
|
||||
return (err: ErrorResponse) => {
|
||||
console.log(err);
|
||||
@@ -231,6 +232,21 @@ export function useApiErrorHandling(ignoreError = false) {
|
||||
if (err.code == 401 && err.message == "token invalid") {
|
||||
setError(err);
|
||||
}
|
||||
|
||||
// Handle user blocked/pending approval responses
|
||||
if (err.code == 403 && (
|
||||
err.message?.toLowerCase().includes("blocked") ||
|
||||
err.message?.toLowerCase().includes("pending")
|
||||
)) {
|
||||
const params = new URLSearchParams({
|
||||
code: err.code.toString(),
|
||||
message: encodeURIComponent(err.message),
|
||||
type: "user-status"
|
||||
});
|
||||
window.location.href = `/error?${params.toString()}`;
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
if (err.code == 500 && err.message == "internal server error") {
|
||||
setError(err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user