mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9fb379abf | ||
|
|
831673d0d6 | ||
|
|
bc4aac10aa | ||
|
|
38e14a6c64 | ||
|
|
b79c6615b4 | ||
|
|
5d4e491611 | ||
|
|
9b1f920863 | ||
|
|
7c7f0a0f10 | ||
|
|
76541c701c |
No files matched your search
@@ -28,6 +28,33 @@ jobs:
|
||||
|
||||
- run: echo '{}' > .local-config.json
|
||||
|
||||
- name: Download IronRDP release TS files
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: netbirdio/IronRDP
|
||||
latest: true
|
||||
fileName: "*.ts"
|
||||
out-file-path: 'public/ironrdp-pkg'
|
||||
|
||||
- name: Download IronRDP release JS files
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: netbirdio/IronRDP
|
||||
latest: true
|
||||
fileName: "*.js"
|
||||
out-file-path: 'public/ironrdp-pkg'
|
||||
|
||||
- name: Download IronRDP release WASM file
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: netbirdio/IronRDP
|
||||
latest: true
|
||||
fileName: "ironrdp_web_bg.wasm"
|
||||
out-file-path: 'public/ironrdp-pkg'
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
-
|
||||
@@ -44,7 +71,6 @@ jobs:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.NB_DOCKER_USER }}
|
||||
@@ -55,7 +81,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -41,3 +41,8 @@ next-env.d.ts
|
||||
.configs/.staging-config.json
|
||||
.configs/.temp-config.json
|
||||
.configs
|
||||
|
||||
/public/ironrdp-pkg/
|
||||
/public/netbird.wasm
|
||||
.idea
|
||||
src/.local-config*
|
||||
@@ -3,6 +3,10 @@ server {
|
||||
listen [::]:80 default_server;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
location = /netbird.wasm {
|
||||
root /usr/share/nginx/html;
|
||||
default_type application/wasm;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri.html $uri/ =404;
|
||||
|
||||
@@ -101,6 +101,7 @@ http {
|
||||
application/rss+xml
|
||||
application/vnd.geo+json
|
||||
application/vnd.ms-fontobject
|
||||
application/wasm
|
||||
application/x-font-ttf
|
||||
application/x-web-app-manifest+json
|
||||
application/xhtml+xml
|
||||
|
||||
Generated
+91
-68
@@ -35,6 +35,8 @@
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"autoprefixer": "^10",
|
||||
"chart.js": "^4.4.8",
|
||||
"chroma-js": "^3.1.2",
|
||||
@@ -513,9 +515,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 +531,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 +580,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 +596,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 +612,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 +628,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 +644,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 +660,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 +676,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 +692,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 +708,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 +2811,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": {
|
||||
@@ -2894,6 +2896,21 @@
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
@@ -3396,9 +3413,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 +4550,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 +5330,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 +5705,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 +6806,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 +6826,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 +8588,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": {
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"autoprefixer": "^10",
|
||||
"chart.js": "^4.4.8",
|
||||
"chroma-js": "^3.1.2",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+278
-125
@@ -2,6 +2,7 @@
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import { Callout } from "@components/Callout";
|
||||
import Card from "@components/Card";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
@@ -32,6 +33,7 @@ import dayjs from "dayjs";
|
||||
import { isEmpty, trim } from "lodash";
|
||||
import {
|
||||
Barcode,
|
||||
CalendarDays,
|
||||
Cpu,
|
||||
FlagIcon,
|
||||
Globe,
|
||||
@@ -64,6 +66,8 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
|
||||
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
|
||||
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
|
||||
export default function PeerPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
@@ -219,7 +223,7 @@ const PeerGeneralInformation = () => {
|
||||
<ModalTrigger>
|
||||
<div
|
||||
className={
|
||||
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
@@ -324,7 +328,7 @@ const PeerGeneralInformation = () => {
|
||||
}
|
||||
interactive={false}
|
||||
className={"w-full block"}
|
||||
disabled={!permission.peers.update}
|
||||
disabled={permission.peers.update}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
value={ssh}
|
||||
@@ -346,6 +350,16 @@ const PeerGeneralInformation = () => {
|
||||
/>
|
||||
</FullTooltip>
|
||||
|
||||
{/* Remote Access Buttons */}
|
||||
<div>
|
||||
<Label>Remote Access</Label>
|
||||
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
|
||||
<div className="flex gap-3">
|
||||
<SSHButton peer={peer} />
|
||||
<RDPButton peer={peer} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{permission.groups.read && (
|
||||
<div>
|
||||
<Label>Assigned Groups</Label>
|
||||
@@ -415,158 +429,217 @@ const PeerOverviewTabs = () => {
|
||||
|
||||
function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
const { isLoading, getRegionByPeer } = useCountries();
|
||||
const { update } = usePeer();
|
||||
const { mutate } = useSWRConfig();
|
||||
const [showEditIPModal, setShowEditIPModal] = useState(false);
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
return getRegionByPeer(peer);
|
||||
}, [getRegionByPeer, peer]);
|
||||
|
||||
return (
|
||||
<Card className={"w-full xl:w-1/2"}>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"NetBird IP-Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} />
|
||||
NetBird IP-Address
|
||||
</>
|
||||
}
|
||||
value={peer.ip}
|
||||
<>
|
||||
<Modal open={showEditIPModal} onOpenChange={setShowEditIPModal}>
|
||||
<EditIPModal
|
||||
onSuccess={(newIP) => {
|
||||
notify({
|
||||
title: peer.name,
|
||||
description: "Peer IP was successfully updated",
|
||||
promise: update({ ip: newIP }).then(() => {
|
||||
mutate("/peers/" + peer.id);
|
||||
setShowEditIPModal(false);
|
||||
}),
|
||||
loadingMessage: "Updating peer IP...",
|
||||
});
|
||||
}}
|
||||
peer={peer}
|
||||
key={showEditIPModal ? 1 : 0}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Public IP-Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
Public IP-Address
|
||||
</>
|
||||
}
|
||||
value={peer.connection_ip}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"DNS label"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
className={
|
||||
peer?.extra_dns_labels && peer.extra_dns_labels.length > 0
|
||||
? "items-start"
|
||||
: ""
|
||||
}
|
||||
value={peer.dns_label}
|
||||
extraText={peer?.extra_dns_labels}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
value={peer.hostname}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<FlagIcon size={16} />
|
||||
Region
|
||||
</>
|
||||
}
|
||||
tooltip={false}
|
||||
value={
|
||||
isEmpty(peer.country_code) ? (
|
||||
"Unknown"
|
||||
) : (
|
||||
</Modal>
|
||||
<Card className={"w-full xl:w-1/2"}>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
copy
|
||||
tooltip={false}
|
||||
copyText={"NetBird IP Address"}
|
||||
label={
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Skeleton width={140} />
|
||||
) : (
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<div className={"border-0 border-nb-gray-800 rounded-full"}>
|
||||
<RoundedFlag country={peer.country_code} size={12} />
|
||||
</div>
|
||||
{countryText}
|
||||
</div>
|
||||
)}
|
||||
<MapPin size={16} />
|
||||
NetBird IP Address
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
valueToCopy={peer.ip}
|
||||
value={
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{peer.ip}</span>
|
||||
{permission.peers.update && (
|
||||
<button
|
||||
className="flex w-7 h-7 items-center justify-center gap-2 text-nb-gray-400 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowEditIPModal(true);
|
||||
}}
|
||||
>
|
||||
<PencilIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Cpu size={16} />
|
||||
Operating System
|
||||
</>
|
||||
}
|
||||
value={peer.os}
|
||||
/>
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Public IP Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
Public IP Address
|
||||
</>
|
||||
}
|
||||
value={peer.connection_ip}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"DNS label"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
className={
|
||||
peer?.extra_dns_labels && peer.extra_dns_labels.length > 0
|
||||
? "items-start"
|
||||
: ""
|
||||
}
|
||||
value={peer.dns_label}
|
||||
extraText={peer?.extra_dns_labels}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
value={peer.hostname}
|
||||
/>
|
||||
|
||||
{peer.serial_number && peer.serial_number !== "" && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Barcode size={16} />
|
||||
Serial Number
|
||||
<FlagIcon size={16} />
|
||||
Region
|
||||
</>
|
||||
}
|
||||
value={peer.serial_number}
|
||||
tooltip={false}
|
||||
value={
|
||||
isEmpty(peer.country_code) ? (
|
||||
"Unknown"
|
||||
) : (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Skeleton width={140} />
|
||||
) : (
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<div
|
||||
className={"border-0 border-nb-gray-800 rounded-full"}
|
||||
>
|
||||
<RoundedFlag country={peer.country_code} size={12} />
|
||||
</div>
|
||||
{countryText}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} />
|
||||
Last seen
|
||||
</>
|
||||
}
|
||||
value={
|
||||
peer.connected
|
||||
? "just now"
|
||||
: dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Cpu size={16} />
|
||||
Operating System
|
||||
</>
|
||||
}
|
||||
value={peer.os}
|
||||
/>
|
||||
|
||||
{peer.serial_number && peer.serial_number !== "" && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Barcode size={16} />
|
||||
Serial Number
|
||||
</>
|
||||
}
|
||||
value={peer.serial_number}
|
||||
/>
|
||||
)}
|
||||
|
||||
{peer.created_at && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<CalendarDays size={16} />
|
||||
Registered on
|
||||
</>
|
||||
}
|
||||
value={
|
||||
dayjs(peer.created_at).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(peer.last_seen) +
|
||||
dayjs().to(peer.created_at) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
Agent Version
|
||||
</>
|
||||
}
|
||||
value={peer.version}
|
||||
/>
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} />
|
||||
Last seen
|
||||
</>
|
||||
}
|
||||
value={
|
||||
peer.connected
|
||||
? "just now"
|
||||
: dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(peer.last_seen) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
|
||||
{peer.ui_version && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
UI Version
|
||||
Agent Version
|
||||
</>
|
||||
}
|
||||
value={peer.ui_version?.replace("netbird-desktop-ui/", "")}
|
||||
value={peer.version}
|
||||
/>
|
||||
)}
|
||||
</Card.List>
|
||||
</Card>
|
||||
|
||||
{peer.ui_version && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
UI Version
|
||||
</>
|
||||
}
|
||||
value={peer.ui_version?.replace("netbird-desktop-ui/", "")}
|
||||
/>
|
||||
)}
|
||||
</Card.List>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -654,3 +727,83 @@ function EditNameModal({ onSuccess, peer, initialName }: Readonly<ModalProps>) {
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditIPModalProps {
|
||||
onSuccess: (ip: string) => void;
|
||||
peer: Peer;
|
||||
}
|
||||
|
||||
function EditIPModal({ onSuccess, peer }: Readonly<EditIPModalProps>) {
|
||||
const [ip, setIP] = useState(peer.ip);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const validateIP = (ipAddress: string) => {
|
||||
const ipRegex =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
return ipRegex.test(ipAddress);
|
||||
};
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
if (ip === peer.ip) return true;
|
||||
const trimmedIP = trim(ip);
|
||||
return trimmedIP.length === 0 || !validateIP(ip);
|
||||
}, [ip, peer.ip]);
|
||||
|
||||
React.useEffect(() => {
|
||||
switch (true) {
|
||||
case ip === peer.ip:
|
||||
setError("");
|
||||
break;
|
||||
case !validateIP(ip):
|
||||
setError("Please enter a valid IP, e.g., 100.64.0.15");
|
||||
break;
|
||||
default:
|
||||
setError("");
|
||||
break;
|
||||
}
|
||||
}, [ip, peer.ip]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-md"}>
|
||||
<form>
|
||||
<ModalHeader
|
||||
title={"Edit Peer IP Address"}
|
||||
description={"Update the NetBird IP address for this peer."}
|
||||
color={"blue"}
|
||||
/>
|
||||
|
||||
<div className={"p-default flex flex-col gap-4"}>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={"e.g., 100.64.0.15"}
|
||||
value={ip}
|
||||
onChange={(e) => setIP(e.target.value)}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout>Changes take effect when the peer reconnects.</Callout>
|
||||
</div>
|
||||
|
||||
<ModalFooter className={"items-center"} separator={false}>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"} className={"w-full"}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
onClick={() => onSuccess(ip)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
@@ -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,9 @@
|
||||
"use client";
|
||||
|
||||
import UsersProvider from "@/contexts/UsersProvider";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<UsersProvider>{children}</UsersProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
"use client";
|
||||
|
||||
import { notify } from "@components/Notification";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { IconCircleX } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import { RDPCertificateModal } from "@/modules/remote-access/rdp/RDPCertificateModal";
|
||||
import { RDPCredentialsModal } from "@/modules/remote-access/rdp/RDPCredentialsModal";
|
||||
import { useRDPQueryParams } from "@/modules/remote-access/rdp/useRDPQueryParams";
|
||||
import {
|
||||
RDPCredentials,
|
||||
RDPStatus,
|
||||
useRemoteDesktop,
|
||||
} from "@/modules/remote-access/rdp/useRemoteDesktop";
|
||||
import {
|
||||
NetBirdStatus,
|
||||
useNetBirdClient,
|
||||
} from "@/modules/remote-access/useNetBirdClient";
|
||||
import { cn } from "@utils/helpers";
|
||||
|
||||
export default function RDPPage() {
|
||||
const { peerId } = useRDPQueryParams();
|
||||
|
||||
const {
|
||||
data: peer,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchApi<Peer>(`/peers/${peerId}`, true, false, !!peerId);
|
||||
|
||||
return (
|
||||
<div className={"w-screen h-screen overflow-hidden"}>
|
||||
{peerId && peer && !isLoading ? (
|
||||
<RDPSession key={peer.id} peer={peer} />
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
|
||||
function RDPSession({ peer }: Props) {
|
||||
const client = useNetBirdClient();
|
||||
const [isNetBirdConnecting, setIsNetBirdConnecting] = useState(false);
|
||||
const rdp = useRemoteDesktop(client);
|
||||
const [credentialsModal, setCredentialsModal] = useState(true);
|
||||
const [credentials, setCredentials] = useState<RDPCredentials | null>(null);
|
||||
const connected = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${peer.name} - ${peer.ip} - RDP`;
|
||||
}, []);
|
||||
|
||||
const sendErrorNotification = (title: string, message: string) => {
|
||||
notify({
|
||||
title: title,
|
||||
description: message,
|
||||
icon: <IconCircleX size={24} />,
|
||||
backgroundColor: "bg-red-500",
|
||||
duration: 10000,
|
||||
});
|
||||
};
|
||||
|
||||
const reset = useCallback(async () => {
|
||||
setCredentials(null);
|
||||
connected.current = false;
|
||||
setCredentialsModal(true);
|
||||
rdp.session?.disconnect();
|
||||
await client.disconnect();
|
||||
}, [client, rdp]);
|
||||
|
||||
/**
|
||||
* Establishes a connection to the peer
|
||||
*/
|
||||
const connect = async (rdpCredentials: RDPCredentials) => {
|
||||
if (!peer?.id) return;
|
||||
if (client.status === NetBirdStatus.DISCONNECTED) {
|
||||
try {
|
||||
setCredentials(rdpCredentials);
|
||||
setIsNetBirdConnecting(true);
|
||||
await client.connectTemporary(peer.id, [`tcp/${rdpCredentials.port}`]);
|
||||
setIsNetBirdConnecting(false);
|
||||
} catch (error) {
|
||||
sendErrorNotification(
|
||||
"NetBird Connection Error",
|
||||
(error as Error).message,
|
||||
);
|
||||
setIsNetBirdConnecting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startSession = useCallback(async () => {
|
||||
if (!credentials) return;
|
||||
try {
|
||||
const result = await rdp.connect({
|
||||
hostname: peer.ip,
|
||||
port: credentials.port,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
if (result === RDPStatus.CONNECTED) {
|
||||
connected.current = true;
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
sendErrorNotification("RDP Connection Error", (error as Error).message);
|
||||
setCredentialsModal(true);
|
||||
await reset();
|
||||
}
|
||||
}, [credentials, peer.ip, rdp, reset]);
|
||||
|
||||
/**
|
||||
* Establish RDP session when NetBird connection is ready
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
client.status === NetBirdStatus.CONNECTED &&
|
||||
rdp.status === RDPStatus.DISCONNECTED &&
|
||||
credentials &&
|
||||
!connected.current &&
|
||||
!isNetBirdConnecting
|
||||
) {
|
||||
startSession().catch(console.error);
|
||||
}
|
||||
}, [
|
||||
client.status,
|
||||
credentials,
|
||||
peer.ip,
|
||||
rdp,
|
||||
startSession,
|
||||
isNetBirdConnecting,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Display notifications for RDP and NetBird client errors
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (rdp.error) {
|
||||
sendErrorNotification("RDP Error", rdp.error);
|
||||
}
|
||||
if (client.error) {
|
||||
sendErrorNotification("NetBird Client Error", client.error);
|
||||
}
|
||||
}, [rdp, client]);
|
||||
|
||||
/**
|
||||
* Close credentials modal when RDP is connected
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (rdp.status === RDPStatus.CONNECTED) {
|
||||
setCredentialsModal(false);
|
||||
}
|
||||
}, [rdp.status]);
|
||||
|
||||
const isLoading =
|
||||
client.status === NetBirdStatus.CONNECTING ||
|
||||
rdp.status === RDPStatus.CONNECTING ||
|
||||
rdp.isResizing ||
|
||||
isNetBirdConnecting;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Credentials Modal */}
|
||||
<RDPCredentialsModal
|
||||
open={credentialsModal}
|
||||
peer={peer}
|
||||
onConnect={connect}
|
||||
loading={isLoading}
|
||||
/>
|
||||
|
||||
{/* Certificate Modal */}
|
||||
<RDPCertificateModal
|
||||
open={!!rdp.pendingCertificate}
|
||||
certificateInfo={rdp.pendingCertificate}
|
||||
onAccept={rdp.acceptCertificatePrompt}
|
||||
onReject={async () => {
|
||||
rdp.rejectCertificatePrompt();
|
||||
await reset();
|
||||
}}
|
||||
/>
|
||||
|
||||
{rdp.isResizing && (
|
||||
<div
|
||||
className={
|
||||
"fixed w-screen h-screen z-50 backdrop-blur bg-black/50 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Loader2Icon size={20} className={"animate-spin"} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RDP Canvas */}
|
||||
<canvas
|
||||
ref={rdp.canvasRef}
|
||||
className={cn(
|
||||
rdp.status === RDPStatus.CONNECTED ? "block" : "hidden",
|
||||
"w-full h-full select-none bg-nb-gray-950",
|
||||
)}
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
"use client";
|
||||
|
||||
import { PageNotFound } from "@components/ui/PageNotFound";
|
||||
import useFetchApi, { ErrorResponse } from "@utils/api";
|
||||
import { CircleXIcon, InfoIcon, Loader2Icon } from "lucide-react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import { Terminal } from "@/modules/remote-access/ssh/Terminal";
|
||||
import { SSHStatus, useSSH } from "@/modules/remote-access/ssh/useSSH";
|
||||
import { useSSHQueryParams } from "@/modules/remote-access/ssh/useSSHQueryParams";
|
||||
import {
|
||||
NetBirdStatus,
|
||||
useNetBirdClient,
|
||||
} from "@/modules/remote-access/useNetBirdClient";
|
||||
|
||||
export default function SSHPage() {
|
||||
const { peerId, username, port } = useSSHQueryParams();
|
||||
|
||||
const {
|
||||
data: peer,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchApi<Peer>(`/peers/${peerId}`, true, false, !!peerId);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={"w-screen h-screen overflow-hidden"}>
|
||||
<ErrorMessage
|
||||
error={{
|
||||
message:
|
||||
"This peer may have been deleted, or you may not have permission to view it.",
|
||||
code: error.code,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"w-screen h-screen overflow-hidden"}>
|
||||
{peerId && peer && !isLoading && username && port ? (
|
||||
<SSHTerminal
|
||||
key={peer.id}
|
||||
peer={peer}
|
||||
username={username}
|
||||
port={port}
|
||||
/>
|
||||
) : (
|
||||
<LoadingMessage message={"Starting ssh session..."} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
username: string;
|
||||
port: string;
|
||||
peer: Peer;
|
||||
};
|
||||
|
||||
function SSHTerminal({ username, port, peer }: Props) {
|
||||
const client = useNetBirdClient();
|
||||
const connected = useRef(false);
|
||||
const sshConnectedOnce = useRef(false);
|
||||
|
||||
const {
|
||||
connect: ssh,
|
||||
disconnect,
|
||||
status,
|
||||
session,
|
||||
error: sshError,
|
||||
} = useSSH(client);
|
||||
|
||||
const isSSHConnecting = status === SSHStatus.CONNECTING;
|
||||
const isSSHConnected = status === SSHStatus.CONNECTED;
|
||||
const isSSHDisconnected = status === SSHStatus.DISCONNECTED;
|
||||
const isClientDisconnected = client.status === NetBirdStatus.DISCONNECTED;
|
||||
const isClientConnecting = client.status === NetBirdStatus.CONNECTING;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${username}@${peer.ip} - ${peer.hostname}`;
|
||||
}, [username, peer, client]);
|
||||
|
||||
const handleReconnect = async () => {
|
||||
if (!peer?.id) return;
|
||||
if (isSSHConnected || isSSHConnecting) return;
|
||||
connected.current = false;
|
||||
try {
|
||||
const rules = [`tcp/${port}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
await ssh({
|
||||
hostname: peer.ip,
|
||||
port: Number(port),
|
||||
username,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Reconnection failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSSHConnected || isSSHConnecting) return;
|
||||
if (isClientConnecting || client.status === NetBirdStatus.CONNECTED) return;
|
||||
|
||||
const connect = async () => {
|
||||
if (!peer.id) return;
|
||||
if (connected.current) return;
|
||||
connected.current = true;
|
||||
try {
|
||||
const rules = [`tcp/${port}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
const res = await ssh({
|
||||
hostname: peer.ip,
|
||||
port: Number(port),
|
||||
username,
|
||||
});
|
||||
if (res === SSHStatus.CONNECTED) {
|
||||
sshConnectedOnce.current = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Connection failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (isClientDisconnected) connect().catch(console.error);
|
||||
}, [
|
||||
isClientDisconnected,
|
||||
isSSHConnected,
|
||||
isSSHConnecting,
|
||||
isClientConnecting,
|
||||
peer.id,
|
||||
port,
|
||||
ssh,
|
||||
username,
|
||||
client.connectTemporary,
|
||||
client.status,
|
||||
]);
|
||||
|
||||
if (client.error) {
|
||||
return <ErrorMessage error={{ message: client.error, code: 0 }} />;
|
||||
}
|
||||
|
||||
if (sshError) {
|
||||
return <ErrorMessage error={{ message: sshError, code: 0 }} />;
|
||||
}
|
||||
|
||||
if (isSSHDisconnected && sshConnectedOnce.current) {
|
||||
return (
|
||||
<DisconnectedMessage
|
||||
username={username}
|
||||
peerIp={peer.ip}
|
||||
onReconnect={handleReconnect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{session && <Terminal session={session} onClose={disconnect} />}
|
||||
{!isSSHConnected && (
|
||||
<LoadingMessage message={`Connecting to ${username}@${peer.ip}...`} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type MessageProps = {
|
||||
message?: string;
|
||||
error?: ErrorResponse;
|
||||
};
|
||||
|
||||
const LoadingMessage = ({ message }: MessageProps) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-full h-full flex items-center justify-center flex-col text-center"
|
||||
}
|
||||
>
|
||||
<div className="text-nb-gray-200 font-normal text-base flex gap-2 items-center justify-center">
|
||||
<Loader2Icon size={16} className={"animate-spin shrink-0"} />
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorMessage = ({ error }: MessageProps) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-full h-full flex items-center justify-center flex-col text-center"
|
||||
}
|
||||
>
|
||||
<div className="text-nb-gray-200 font-normal text-base flex gap-2 items-center justify-center">
|
||||
<CircleXIcon size={16} className={"shrink-0 text-red-500"} />
|
||||
{error?.message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type DisconnectedMessageProps = {
|
||||
username: string;
|
||||
peerIp: string;
|
||||
onReconnect: () => void;
|
||||
};
|
||||
|
||||
const DisconnectedMessage = ({
|
||||
username,
|
||||
peerIp,
|
||||
onReconnect,
|
||||
}: DisconnectedMessageProps) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-full h-full flex items-center justify-center flex-col text-center gap-4"
|
||||
}
|
||||
>
|
||||
<div className="text-nb-gray-200 font-normal text-base flex gap-2 items-center justify-center">
|
||||
<InfoIcon size={16} className={"shrink-0 text-nb-gray-200"} />
|
||||
Disconnected from {username}@{peerIp}
|
||||
<button
|
||||
className={
|
||||
"underline-offset-4 items-center transition-all duration-200 inline-flex texts-inherit gap-1 text-netbird hover:underline font-normal"
|
||||
}
|
||||
onClick={onReconnect}
|
||||
>
|
||||
Reconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -157,3 +157,14 @@ p {
|
||||
.animate-bg-scroll-faster {
|
||||
animation: bg-scroll 1.8s linear infinite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminal (xterm)
|
||||
*/
|
||||
.xterm {
|
||||
@apply m-0 p-1 box-border h-full w-full;
|
||||
}
|
||||
|
||||
.xterm-viewport {
|
||||
@apply m-0 p-0 box-border;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export default function NotFound() {
|
||||
}
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
useRedirect("/peers" + (queryParams && `?${queryParams}`));
|
||||
const params = queryParams && `?${queryParams}`;
|
||||
useRedirect(url == "/" ? `/peers${params}` : `${url}${params}`);
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
@@ -58,6 +58,10 @@ export default function OIDCProvider({ children }: Props) {
|
||||
"utm_content",
|
||||
"utm_campaign",
|
||||
"hs_id",
|
||||
"page",
|
||||
"page_size",
|
||||
"user",
|
||||
"port",
|
||||
];
|
||||
|
||||
try {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const buttonVariants = cva(
|
||||
"relative",
|
||||
"text-sm focus:z-10 focus:ring-2 font-medium focus:outline-none whitespace-nowrap shadow-sm",
|
||||
"inline-flex gap-2 items-center justify-center transition-colors focus:ring-offset-1",
|
||||
"disabled:opacity-20 disabled:cursor-not-allowed disabled:dark:text-nb-gray-300 dark:ring-offset-neutral-950/50",
|
||||
"disabled:opacity-40 disabled:cursor-not-allowed disabled:dark:text-nb-gray-300 dark:ring-offset-neutral-950/50",
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
@@ -34,7 +34,7 @@ export const buttonVariants = cva(
|
||||
secondary: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
|
||||
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
|
||||
"dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
|
||||
],
|
||||
secondaryLighter: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const calloutVariants = cva(
|
||||
|
||||
export const Callout = ({
|
||||
children,
|
||||
icon = <InfoIcon size={14} className={"shrink-0 relative top-[2px]"} />,
|
||||
icon = <InfoIcon size={14} className={"shrink-0 relative top-[3px]"} />,
|
||||
className,
|
||||
variant = "default",
|
||||
}: Props) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ function CardList({ children }: Props) {
|
||||
type CardListItemProps = {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
valueToCopy?: string;
|
||||
className?: string;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
@@ -39,6 +40,7 @@ type CardListItemProps = {
|
||||
function CardListItem({
|
||||
label,
|
||||
value,
|
||||
valueToCopy,
|
||||
className,
|
||||
copy = false,
|
||||
copyText,
|
||||
@@ -57,6 +59,7 @@ function CardListItem({
|
||||
<CardTextItem
|
||||
label={label}
|
||||
value={value}
|
||||
valueToCopy={valueToCopy}
|
||||
copy={copy}
|
||||
copyText={copyText}
|
||||
tooltip={tooltip}
|
||||
@@ -79,6 +82,7 @@ function CardListItem({
|
||||
type CardTextItemProps = {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
valueToCopy?: string;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
tooltip?: boolean;
|
||||
@@ -88,10 +92,11 @@ const CardTextItem = ({
|
||||
label,
|
||||
value,
|
||||
copy = false,
|
||||
valueToCopy,
|
||||
copyText,
|
||||
tooltip = true,
|
||||
}: CardTextItemProps) => {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(value as string);
|
||||
const [, copyToClipBoard] = useCopyToClipboard(valueToCopy ?? `${value}`);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
Loaded 20 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user