Compare commits

...

62 Commits

Author SHA1 Message Date
Eduard Gert
a04e3afccb Show "Never" when a user never logged in instead of a date (#335)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-16 12:15:32 +01:00
Eduard Gert
bca327e4cf Add better search for network-routes by group name (#336) 2024-02-16 12:15:14 +01:00
Maycon Santos
6c74506316 Add templates for bugs and for feature request (#333) 2024-02-14 13:43:27 +01:00
Eduard Gert
663d7ea58c Add check to call initial users only once in dev mode (#332)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-13 15:11:37 +01:00
Eduard Gert
b701783dca Update ephemeral_peers to ephemeral (#331)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-13 14:12:31 +01:00
Eduard Gert
fc9a9dfa3e Block application and show loading until users are fetched (#330)
* Add option to ignore errors

* Block application and show loading until users are fetched
2024-02-13 14:08:43 +01:00
Eduard Gert
093efc08b3 Fix an issue of creating duplicate groups in the access control and network routes modal when group does not exist (#328)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-12 14:12:57 +01:00
Eduard Gert
dfa41a48e3 Hide the user invite button for selfhosted users (#327) 2024-02-12 14:08:10 +01:00
Eduard Gert
2cf366a5f8 Fix access control to show the correct modal (#326)
* Rename Access Control "Rule" to Access Control "Policy"

* Show the correct modal for Access Control
2024-02-12 14:07:53 +01:00
Eduard Gert
f91788faef Fix iOS detection and modal scrolling on Safari mobile (#325)
* Add better iOS detection

* Fix scrolling for Safari browser
2024-02-12 14:07:31 +01:00
Eduard Gert
ec7bb76f1e Fix closing of tab when creating setup-key (#324) 2024-02-12 14:06:59 +01:00
Eduard Gert
15bab2cef4 Merge pull request #322
* Add unique key for nameservers
2024-02-12 14:05:26 +01:00
Eduard Gert
4fa3482c74 Merge pull request #318
* Fix redirect link to event streaming docs
2024-02-12 14:04:42 +01:00
Eduard Gert
f5059f485c Fix invalid token error message (#321) 2024-02-09 16:07:09 +01:00
Eduard Gert
3c60de4169 Add a fallback in case the user has no name (#320)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* Fix redirect link to event streaming docs

* Fallback to a user id in case user has no name
2024-02-05 16:48:25 +01:00
Eduard Gert
2267cecf46 Change fetch() to support idToken source (#319)
* Fix redirect link to event streaming docs

* Change fetch logic to support okta oidc
2024-02-05 11:08:36 +01:00
Eduard Gert
2b222e082a Init Dashboard V2 (#316)
* Init Dashboard V2

* Update README.md

* use dedicated vars and prevent docker push on PRs

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-01-30 13:34:42 +01:00
Eduard Gert
4612f6c49a Add "New dashboard" Banner (#315)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* Add new dashboard Banner.tsx

* display on localdev and add todo note

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-01-24 18:36:20 +01:00
Maycon Santos
a3a0e6315f Use Approve for approve peer modal (#312)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-01-02 20:07:10 +01:00
Misha Bragin
fa7bc205ba Add reference to the Apple Store on the Add Peer window (#311)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-29 18:03:50 +01:00
pascal-fischer
87ff65f1a7 adding peers approval flag to non user bound devices (#310)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-25 13:01:54 +01:00
Bethuel Mmbaga
748596f710 Add JWT allow groups support in account settings (#309)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-18 12:14:03 +01:00
Misha Bragin
b06cb0ec3d Add a settings page for custom IdP and MFA (#308)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-12 13:42:14 +01:00
Misha Bragin
0c924f7ded Correct peer approval tooltip (#307) 2023-12-12 13:33:11 +01:00
Maycon Santos
f29c915fc2 fix settings update blank page
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-11 18:07:05 +01:00
Maycon Santos
5f8579bfda Fix peer expiration interval init on validation (#306)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-11 17:46:32 +01:00
pascal-fischer
a71389aa29 not sending extra settings on selfhosted deployments (#305)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-11 16:52:29 +01:00
Maycon Santos
c3ba026452 Init domains array for custom nameserver group (#304)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-08 11:07:10 +01:00
Maycon Santos
193f8a7bdf Fix missing text update from #302 (#303) 2023-12-07 18:05:44 +01:00
Fabio Fantoni
f9814e1169 add codespell github workflow and fix some typo (#296)
* add codespell github workflow

* fix some typo
2023-12-07 18:05:31 +01:00
Maycon Santos
2f800bf912 Add delete account support (#302)
* Add delete account support

* add peer approval form item

* block menu for non owners
2023-12-07 15:28:07 +01:00
pascal-fischer
0199ea81f3 Add support for peer approval (#299)
* add support for peer approval

* use gold color for tag

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-12-05 13:17:40 +01:00
Maycon Santos
a20894092b Add owner role support (#300)
Update checks for admin to include owner role

Updated role list to include role for users

Updated activity with new event type handler
2023-12-01 16:57:18 +01:00
Maycon Santos
f4d6ce5770 Fix Windows server version lowercase
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-11-24 18:29:43 +01:00
Maycon Santos
dd67ab6dcb Fix Windows server version (#298)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-11-24 17:42:31 +01:00
pascal-fischer
2613948cdf Fix OS display format for iOS (#297)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* fix OS format function to properly show iOS

* bring back windows check
2023-11-13 22:30:50 +08:00
pascal-fischer
dc95d8bfd1 disallow routing through ios devices (#295) 2023-11-11 13:20:39 +01:00
Bethuel Mmbaga
f49c28f550 Add integration activity events target to Activity view as system setting (#293) 2023-11-08 13:51:57 +03:00
Maycon Santos
2fc7b73ea0 Fix/blank page on empty policy groups (#292)
validate null sources and destinations fields
2023-11-01 11:56:12 +01:00
pascal-fischer
a9354d3c87 removed disabled option from setup key filter switch (#291) 2023-10-27 20:06:42 +02:00
Sarooj bukhari
e0ae7d068a add search domains enabled to namespace (#288)
Some checks failed
build and push / build_n_push (push) Has been cancelled
---------

Co-authored-by: braginini <bangvalo@gmail.com>
2023-10-20 09:33:44 +02:00
Sarooj bukhari
ddd812e9a0 Add a refresh button to the table views (#287)
Co-authored-by: braginini <bangvalo@gmail.com>
2023-10-15 19:04:05 +02:00
Sarooj bukhari
2d55d0736f Add referesh button to the peers table (#286)
Some checks failed
build and push / build_n_push (push) Has been cancelled
Co-authored-by: braginini <bangvalo@gmail.com>
2023-10-12 18:38:59 +02:00
Sarooj bukhari
8febc26f1f fix update distribution groups bug (#284)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-10-04 20:07:02 +02:00
Sarooj bukhari
3f854b01a0 Add support to network routes with peer group (#275)
Some checks failed
build and push / build_n_push (push) Has been cancelled
support to routes with peer group
updated the view logic to support the new type
updated peer view as well
for now, we are supporting a single group, but that can be extended
2023-10-04 11:37:23 +02:00
Misha Bragin
303d51eff8 Display service username in the activity (#282) 2023-10-03 16:06:50 +02:00
braginini
21e69e642a Disable user deletion since the API isn't ready
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-09-29 16:05:26 +02:00
pascal-fischer
835bb37ab9 Update jwt group sync visibility and update description (#281)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-09-29 13:58:46 +02:00
dependabot[bot]
a944dc8ab0 Bump @adobe/css-tools from 4.0.1 to 4.3.1 (#265)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.0.1 to 4.3.1.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-29 00:39:19 +05:00
Sarooj bukhari
b2c51533fb remove invalid key check (#280) 2023-09-28 21:26:11 +02:00
pascal-fischer
fd24536926 add checks to hide last login for non netbird hosted deployments (#277) 2023-09-27 18:00:33 +02:00
Misha Bragin
8e8484cd45 Fix packages (#276)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-09-27 12:25:55 +02:00
Sarooj bukhari
6c87f53195 Update the /Activity view to use email addresses from the /api/events response (#273)
Some checks failed
build and push / build_n_push (push) Has been cancelled
uses the events api response information to map user name and email
2023-09-23 10:45:27 +02:00
pascal-fischer
9bbbff7dc0 Fix last login for regular entries in users overview (#264)
* add last login to user overview for regular entries without tag

* add backward compatability to properly show with older version of management

* Merge main and update delete user popup messages

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-09-23 10:40:22 +02:00
Sarooj bukhari
04a20fa31f add new fields on setting pag (#258) 2023-09-22 16:32:40 +02:00
Misha Bragin
3797db93f0 Hide network routes card for non-linux peers (#269) 2023-09-20 10:58:26 +02:00
Sarooj bukhari
2e81765e85 add delete user functionality (#272) 2023-09-19 15:13:31 +02:00
Yoann N
cb9f76c0fc typo in ac creation (#266) 2023-09-18 23:22:52 +02:00
Sarooj bukhari
54accb665c setup ui (#268)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* setup ui

* capital letter

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-09-07 20:26:34 +02:00
Maycon Santos
cfea3bd489 Add ephemeral peers views and subviews (#267)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-09-06 13:36:23 +02:00
Zoltan Papp
a44a1c5424 Feature/ephemeral peers (#263)
* Add ephemeral peer switch for SetupKeyNew component

* Add "sys" handling in activity view
2023-09-04 13:49:11 +02:00
Sarooj bukhari
c2c044421f Peer search bug (#262) 2023-08-25 12:25:58 +02:00
574 changed files with 31219 additions and 51983 deletions

13
.eslintrc.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": ["next/core-web-vitals","prettier"],
"plugins": ["simple-import-sort"],
"rules": {
"simple-import-sort/imports": [
"warn",
{
"groups": [["^\\u0000", "^@?\\w", "^[^.]", "^\\."]]
}
],
"simple-import-sort/exports": "warn"
}
}

View File

@@ -0,0 +1,44 @@
---
name: Bug/Issue report
about: Create a report to help us improve
title: ''
labels: ['needs-triage']
assignees: ''
---
**Describe the problem**
A clear and concise description of what the problem is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Are you using NetBird Cloud?**
Please specify whether you use NetBird Cloud or self-host NetBird's control plane.
**NetBird version**
`netbird version`
**NetBird status -d output:**
If applicable, add the `netbird status -d' command output.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ['feature-request','needs-triage']
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -7,6 +7,9 @@ on:
- "**"
pull_request:
env:
IMAGE_NAME: netbirdio/dashboard
jobs:
build_n_push:
runs-on: ubuntu-latest
@@ -16,15 +19,16 @@ jobs:
- name: setup-node
uses: actions/setup-node@v3
with:
node-version: '16'
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm install
- run: echo '{}' > .local-config.json
- name: Build
# skiping fail on warning for now
run: CI=false npm run build
run: npm run build
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
@@ -36,14 +40,14 @@ jobs:
id: meta
uses: docker/metadata-action@v4
with:
images: wiretrustee/dashboard
images: ${{ env.IMAGE_NAME }}
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
username: ${{ secrets.NB_DOCKER_USER }}
password: ${{ secrets.NB_DOCKER_TOKEN }}
-
name: Docker build and push
uses: docker/build-push-action@v3
@@ -53,4 +57,4 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}

15
.github/workflows/codespell.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Codespell
on: [pull_request]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: codespell
uses: codespell-project/actions-codespell@v2
with:
only_warn: 1
skip: package-lock.json,*.svg

View File

@@ -1,42 +0,0 @@
name: run e2e tests
on:
push:
branches:
- main
pull_request:
jobs:
e2e_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: setup-node
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: install playwright
run: npx playwright install
- name: install playwright deps
run: npx playwright install-deps
- name: create test environment
run: bash ./e2e-tests/create-test-env.sh
- name: run e2e tests
run: npx playwright test --workers 2
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 3

46
.gitignore vendored
View File

@@ -2,42 +2,42 @@
# dependencies
/node_modules
/node_modules.bkp
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
/out
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
src/auth_config.json
.idea
.eslintcache
src/.local-config*.json
/public/OidcServiceWorker.js
/public/OidcTrustedDomains.js
/e2e-tests/node_modules/
/e2e-tests/playwright-report/
/e2e-tests/test-results/
/test-results/
/playwright-report/
.env
Caddyfile
docker-compose.yml
machinekey/
management.json
turnserver.conf
zitadel.env
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# config
.local-config.json
.configs/.local-config.zitadel.json
.configs/.staging-config.json
.configs/.temp-config.json
.configs

View File

@@ -1,2 +1,3 @@
Mikhail Bragin (https://github.com/braginini)
Maycon Santos (https://github.com/mlsmaycon)
Wiretrustee UG (haftungsbeschränkt)

View File

@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2021 Wiretrustee AUTHORS
Copyright (c) 2024 Wiretrustee UG (haftungsbeschränkt) & AUTHORS
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

View File

@@ -1,4 +1,4 @@
# NetBird dashboard
# NetBird Dashboard
This project is the UI for NetBird's Management service.
@@ -17,15 +17,15 @@ The dashboard makes it possible to:
- define access controls
## Some Screenshots
<img src="./media/auth.png" alt="auth"/>
<img src="./media/peers.png" alt="peers"/>
<img src="./media/add-peer.png" alt="add-peer"/>
<img src="./src/assets/screenshots/peers.png" alt="peers"/>
<img src="./src/assets/screenshots/add-peer.png" alt="add-peer"/>
## Technologies Used
- NextJS
- ReactJS
- AntD UI framework
- Tailwind CSS
- Auth0
- Nginx
- Docker
@@ -38,28 +38,28 @@ Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
1. Install [Docker](https://docs.docker.com/get-docker/)
2. Register [Auth0](https://auth0.com/) account
3. Running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
3. Running NetBird UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
`AUTH0_DOMAIN` `AUTH0_CLIENT_ID` `AUTH0_AUDIENCE`
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Configure Allowed Web Origins"
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
4. NetBird UI Dashboard uses NetBirds Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
5. Run docker container without SSL (Let's Encrypt):
```shell
docker run -d --name wiretrustee-dashboard \
docker run -d --name netbird-dashboard \
--rm -p 80:80 -p 443:443 \
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
wiretrustee/dashboard:main
netbirdio/dashboard:main
```
6. Run docker container with SSL (Let's Encrypt):
```shell
docker run -d --name wiretrustee-dashboard \
docker run -d --name netbird-dashboard \
--rm -p 80:80 -p 443:443 \
-e NGINX_SSL_PORT=443 \
-e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> \
@@ -68,11 +68,26 @@ Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
-e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> \
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
wiretrustee/dashboard:main
netbirdio/dashboard:main
```
## How to run local development
1. Install node 16
2. create and update the `src/.local-config.json` file. This file should contain values to be replaced from `src/config.json`
3. run `npm install`
4. run `npm run start dev`
1. Install [Node](https://nodejs.org/)
2. Create and update the `.local-config.json` file. This file should contain values to be replaced from `config.json`
3. Run `npm install` to install dependencies
4. Run `npm run dev` to start the development server
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing by modifying the code inside `src/..`
The page auto-updates as you edit the file.
## How to migrate from old dashboard (v1)
The new dashboard comes with a new docker image `netbirdio/dashboard:main`.
To migrate from the old dashboard (v1) `wiretrustee/dashboard:main` to the new one, please follow the steps below.
1. Stop the dashboard container `docker compose down dashboard`
2. Replace the docker image name in your `docker-compose.yml` with `netbirdio/dashboard:main`
3. Recreate the dashboard container `docker compose up -d --force-recreate dashboard`

16
components.json Normal file
View File

@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": false
},
"aliases": {
"components": "@/components",
"utils": "@/utils/helpers"
}
}

View File

@@ -5,12 +5,12 @@
"authClientSecret": "$AUTH_CLIENT_SECRET",
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
"authAudience": "$AUTH_AUDIENCE",
"apiOrigin": "$NETBIRD_MGMT_API_ENDPOINT",
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"redirectURI": "$AUTH_REDIRECT_URI",
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS"
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID"
}

15
cypress.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
},
component: {
devServer: {
framework: "next",
bundler: "webpack",
},
},
viewportWidth: 1920,
viewportHeight: 1080,
});

13
cypress/e2e/test.cy.ts Normal file
View File

@@ -0,0 +1,13 @@
describe("Click all tabs in peer modal", () => {
it("passes", () => {
cy.visit("/install");
cy.get("div").contains("Linux").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Windows").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Android").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Docker").click();
cy.get("[data-cy=copy-to-clipboard]").click();
});
});

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

20
cypress/support/e2e.ts Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

9
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"baseUrl": "http://localhost:3000",
"types": ["cypress", "node"],
},
"include": ["**/*.ts"]
}

View File

@@ -21,4 +21,4 @@ RUN chmod +x /usr/local/init_react_envs.sh
# configure supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
# copy build files
COPY build/ /usr/share/nginx/html/
COPY out/ /usr/share/nginx/html/

View File

@@ -1,20 +1,20 @@
# Wiretrustee Dashboard
Wiretrustee Dashboard is a the Wiretrustee Managemenet server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
# NetBird Dashboard
NetBird Dashboard is NetBirds Management server UI. It allows users to signin, view setup keys and manage peers. This image is **not ready** for production use.
## Tags
```latest``` ```vX.X.X``` not available yet.
```main``` builded on every PR being merged to the repository
```main``` built on every PR being merged to the repository
## How to use this image
HTTP run:
```shell
docker run -d --rm -p 80:80 wiretrustee/dashboard:main
```
Using SSL certificate from Let's Encript®:
Using SSL certificate from Let's Encrypt®:
```shell
docker run -d --rm -p 80:80 -p 443:443 \
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
-e LETSENCRYPT_EMAIL=hello@mydomain.com \
wiretrustee/dashboard:main
netbirdio/dashboard:main
```
> For SSL generation, you need to run this image in a server with proper public IP and a domain name pointing to it.
## Environment variables

View File

@@ -6,11 +6,11 @@ server {
root /usr/share/nginx/html;
location / {
try_files $uri /index.html;
}
# You may need this to prevent return 404 recursion.
location = /404.html {
internal;
}
try_files $uri $uri.html $uri/ =404;
}
error_page 404 /404.html;
location = /404.html {
internal;
}
}

View File

@@ -57,17 +57,19 @@ export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api o
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
export NETBIRD_GOOGLE_ANALYTICS_ID=${NETBIRD_GOOGLE_ANALYTICS_ID}
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# replace ENVs in the config
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS"
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS"
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
cp "$MAIN_JS" "$MAIN_JS".copy
envsubst "$ENV_STR" < "$MAIN_JS".copy > "$MAIN_JS"
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
rm "$MAIN_JS".copy
for f in $(grep -R -l AUTH_SUPPORTED_SCOPES /usr/share/nginx/html); do
cp "$f" "$f".copy
envsubst "$ENV_STR" < "$f".copy > "$f"
rm "$f".copy
done

View File

@@ -1,3 +0,0 @@
#!/bin/bash
docker-compose down --volumes
rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json

View File

@@ -1,697 +0,0 @@
#!/bin/bash
set -e
handle_request_command_status() {
PARSED_RESPONSE=$1
FUNCTION_NAME=$2
RESPONSE=$3
if [[ $PARSED_RESPONSE -ne 0 ]]; then
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
exit 1
fi
}
handle_zitadel_request_response() {
PARSED_RESPONSE=$1
FUNCTION_NAME=$2
RESPONSE=$3
if [[ $PARSED_RESPONSE == "null" ]]; then
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
exit 1
fi
sleep 1
}
check_docker_compose() {
if command -v docker-compose &> /dev/null
then
echo "docker-compose"
return
fi
if docker compose --help &> /dev/null
then
echo "docker compose"
return
fi
echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr
exit 1
}
check_jq() {
if ! command -v jq &> /dev/null
then
echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr
exit 1
fi
}
wait_crdb() {
set +e
while true; do
if $DOCKER_COMPOSE_COMMAND exec -T crdb curl -sf -o /dev/null 'http://localhost:8080/health?ready=1'; then
break
fi
echo -n " ."
sleep 5
done
echo " done"
set -e
}
init_crdb() {
echo -e "\nInitializing Zitadel's CockroachDB\n\n"
$DOCKER_COMPOSE_COMMAND up -d crdb
echo ""
# shellcheck disable=SC2028
echo -n "Waiting cockroachDB to become ready "
wait_crdb
$DOCKER_COMPOSE_COMMAND exec -T crdb /bin/bash -c "cp /cockroach/certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown -R 1000:1000 /zitadel-certs/"
handle_request_command_status $? "init_crdb failed" ""
}
get_main_ip_address() {
if [[ "$OSTYPE" == "darwin"* ]]; then
interface=$(route -n get default | grep 'interface:' | awk '{print $2}')
ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}')
else
interface=$(ip route | grep default | awk '{print $5}' | head -n 1)
ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1)
fi
echo "$ip_address"
}
wait_pat() {
PAT_PATH=$1
set +e
while true; do
if [[ -f "$PAT_PATH" ]]; then
break
fi
echo -n " ."
sleep 1
done
echo " done"
set -e
}
wait_api() {
INSTANCE_URL=$1
PAT=$2
set +e
while true; do
curl -s --fail -o /dev/null "$INSTANCE_URL/auth/v1/users/me" -H "Authorization: Bearer $PAT"
if [[ $? -eq 0 ]]; then
break
fi
echo -n " ."
sleep 1
done
echo " done"
set -e
}
create_new_project() {
INSTANCE_URL=$1
PAT=$2
PROJECT_NAME="NETBIRD"
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/projects" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{"name": "'"$PROJECT_NAME"'"}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.id')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_project" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_new_application() {
INSTANCE_URL=$1
PAT=$2
APPLICATION_NAME=$3
BASE_REDIRECT_URL1=$4
BASE_REDIRECT_URL2=$5
LOGOUT_URL=$6
ZITADEL_DEV_MODE=$7
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"name": "'"$APPLICATION_NAME"'",
"redirectUris": [
"'"$BASE_REDIRECT_URL1"'",
"'"$BASE_REDIRECT_URL2"'"
],
"postLogoutRedirectUris": [
"'"$LOGOUT_URL"'"
],
"RESPONSETypes": [
"OIDC_RESPONSE_TYPE_CODE"
],
"grantTypes": [
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
],
"appType": "OIDC_APP_TYPE_USER_AGENT",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
"version": "OIDC_VERSION_1_0",
"devMode": '"$ZITADEL_DEV_MODE"',
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
"accessTokenRoleAssertion": true,
"skipNativeAppSuccessPage": true
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.clientId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_application" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_service_user() {
INSTANCE_URL=$1
PAT=$2
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/users/machine" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userName": "netbird-service-account",
"name": "Netbird Service Account",
"description": "Netbird Service Account for IDP management",
"accessTokenType": "ACCESS_TOKEN_TYPE_JWT"
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_service_user" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_service_user_secret() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X PUT "$INSTANCE_URL/management/v1/users/$USER_ID/secret" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{}'
)
SERVICE_USER_CLIENT_ID=$(echo "$RESPONSE" | jq -r '.clientId')
handle_zitadel_request_response "$SERVICE_USER_CLIENT_ID" "create_service_user_secret_id" "$RESPONSE"
SERVICE_USER_CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.clientSecret')
handle_zitadel_request_response "$SERVICE_USER_CLIENT_SECRET" "create_service_user_secret" "$RESPONSE"
}
add_organization_user_manager() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/orgs/me/members" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userId": "'"$USER_ID"'",
"roles": [
"ORG_USER_MANAGER"
]
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "add_organization_user_manager" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_admin_user() {
INSTANCE_URL=$1
PAT=$2
USERNAME=$3
PASSWORD=$4
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/users/human/_import" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userName": "'"$USERNAME"'",
"profile": {
"firstName": "Zitadel",
"lastName": "Admin"
},
"email": {
"email": "'"$USERNAME"'",
"isEmailVerified": true
},
"password": "'"$PASSWORD"'",
"passwordChangeRequired": false
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_admin_user" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
add_instance_admin() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/admin/v1/members" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userId": "'"$USER_ID"'",
"roles": [
"IAM_OWNER"
]
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "add_instance_admin" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
delete_auto_service_user() {
INSTANCE_URL=$1
PAT=$2
RESPONSE=$(
curl -sS -X GET "$INSTANCE_URL/auth/v1/users/me" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
USER_ID=$(echo "$RESPONSE" | jq -r '.user.id')
handle_zitadel_request_response "$USER_ID" "delete_auto_service_user_get_user" "$RESPONSE"
RESPONSE=$(
curl -sS -X DELETE "$INSTANCE_URL/admin/v1/members/$USER_ID" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_instance_permissions" "$RESPONSE"
RESPONSE=$(
curl -sS -X DELETE "$INSTANCE_URL/management/v1/orgs/me/members/$USER_ID" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_org_permissions" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
init_zitadel() {
echo -e "\nInitializing Zitadel with NetBird's applications\n"
INSTANCE_URL="$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
TOKEN_PATH=./machinekey/zitadel-admin-sa.token
echo -n "Waiting for Zitadel's PAT to be created "
wait_pat "$TOKEN_PATH"
echo "Reading Zitadel PAT"
PAT=$(cat $TOKEN_PATH)
if [ "$PAT" = "null" ]; then
echo "Failed requesting getting Zitadel PAT"
exit 1
fi
echo -n "Waiting for Zitadel to become ready "
wait_api "$INSTANCE_URL" "$PAT"
# create the zitadel project
echo "Creating new zitadel project"
PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT")
ZITADEL_DEV_MODE=false
BASE_REDIRECT_URL=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
if [[ $NETBIRD_HTTP_PROTOCOL == "http" ]]; then
ZITADEL_DEV_MODE=true
fi
# create zitadel spa applications
echo "Creating new Zitadel SPA Dashboard application"
DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Dashboard" "http://localhost:3000/nb-auth" "http://localhost:3000/nb-silent-auth" "http://localhost:3000/" "true")
echo "Creating new Zitadel SPA Cli application"
CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true")
MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT")
SERVICE_USER_CLIENT_ID="null"
SERVICE_USER_CLIENT_SECRET="null"
create_service_user_secret "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID"
DATE=$(add_organization_user_manager "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID")
ZITADEL_ADMIN_USERNAME="admin@localhost"
ZITADEL_ADMIN_PASSWORD="testMe123@"
HUMAN_USER_ID=$(create_admin_user "$INSTANCE_URL" "$PAT" "$ZITADEL_ADMIN_USERNAME" "$ZITADEL_ADMIN_PASSWORD")
DATE="null"
DATE=$(add_instance_admin "$INSTANCE_URL" "$PAT" "$HUMAN_USER_ID")
DATE="null"
DATE=$(delete_auto_service_user "$INSTANCE_URL" "$PAT")
if [ "$DATE" = "null" ]; then
echo "Failed deleting auto service user"
echo "Please remove it manually"
fi
export NETBIRD_AUTH_CLIENT_ID=$DASHBOARD_APPLICATION_CLIENT_ID
export NETBIRD_AUTH_CLIENT_ID_CLI=$CLI_APPLICATION_CLIENT_ID
export NETBIRD_IDP_MGMT_CLIENT_ID=$SERVICE_USER_CLIENT_ID
export NETBIRD_IDP_MGMT_CLIENT_SECRET=$SERVICE_USER_CLIENT_SECRET
export ZITADEL_ADMIN_USERNAME
export ZITADEL_ADMIN_PASSWORD
}
check_nb_domain() {
DOMAIN=$1
if [ "$DOMAIN-x" == "-x" ]; then
echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr
return 1
fi
if [ "$DOMAIN" == "netbird.example.com" ]; then
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
retrun 1
fi
return 0
}
read_nb_domain() {
READ_NETBIRD_DOMAIN=""
echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr
read -r READ_NETBIRD_DOMAIN < /dev/tty
if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then
read_nb_domain
fi
echo "$READ_NETBIRD_DOMAIN"
}
initEnvironment() {
CADDY_SECURE_DOMAIN=""
ZITADEL_EXTERNALSECURE="false"
ZITADEL_TLS_MODE="disabled"
ZITADEL_MASTERKEY="$(openssl rand -base64 32 | head -c 32)"
NETBIRD_PORT=80
NETBIRD_HTTP_PROTOCOL="http"
TURN_USER="self"
TURN_PASSWORD=$(openssl rand -base64 32 | sed 's/=//g')
TURN_MIN_PORT=49152
TURN_MAX_PORT=65535
NETBIRD_DOMAIN=$(get_main_ip_address)
if [[ "$OSTYPE" == "darwin"* ]]; then
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
else
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ")
fi
check_jq
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
if [ -f zitadel.env ]; then
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
echo "You can use the following commands:"
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
echo " rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json"
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
exit 1
fi
echo Rendering initial files...
renderDockerCompose > docker-compose.yml
renderCaddyfile > Caddyfile
renderZitadelEnv > zitadel.env
echo "" > turnserver.conf
echo "" > management.json
mkdir -p machinekey
chmod 777 machinekey
init_crdb
echo -e "\nStarting Zidatel IDP for user management\n\n"
$DOCKER_COMPOSE_COMMAND up -d caddy zitadel
init_zitadel
echo -e "\nRendering NetBird files...\n"
renderTurnServerConf > turnserver.conf
renderManagementJson > management.json
renderDashboardEnv > src/.local-config.json
echo -e "\nStarting NetBird services\n"
$DOCKER_COMPOSE_COMMAND up -d
echo -e "\nDone!\n"
echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
echo "Login with the following credentials:"
echo "Username: $ZITADEL_ADMIN_USERNAME" | tee .env
echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
}
renderCaddyfile() {
cat <<EOF
{
debug
servers :80,:443 {
protocols h1 h2c
}
}
:80${CADDY_SECURE_DOMAIN} {
# Signal
reverse_proxy /signalexchange.SignalExchange/* h2c://signal:10000
# Management
reverse_proxy /api/* management:80
reverse_proxy /management.ManagementService/* h2c://management:80
# Zitadel
reverse_proxy /zitadel.admin.v1.AdminService/* h2c://zitadel:8080
reverse_proxy /admin/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.auth.v1.AuthService/* h2c://zitadel:8080
reverse_proxy /auth/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.management.v1.ManagementService/* h2c://zitadel:8080
reverse_proxy /management/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.system.v1.SystemService/* h2c://zitadel:8080
reverse_proxy /system/v1/* h2c://zitadel:8080
reverse_proxy /assets/v1/* h2c://zitadel:8080
reverse_proxy /ui/* h2c://zitadel:8080
reverse_proxy /oidc/v1/* h2c://zitadel:8080
reverse_proxy /saml/v2/* h2c://zitadel:8080
reverse_proxy /oauth/v2/* h2c://zitadel:8080
reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080
reverse_proxy /openapi/* h2c://zitadel:8080
reverse_proxy /debug/* h2c://zitadel:8080
# Dashboard
reverse_proxy /* dashboard:80
}
EOF
}
renderTurnServerConf() {
cat <<EOF
listening-port=3478
tls-listening-port=5349
min-port=$TURN_MIN_PORT
max-port=$TURN_MAX_PORT
fingerprint
lt-cred-mech
user=$TURN_USER:$TURN_PASSWORD
realm=wiretrustee.com
cert=/etc/coturn/certs/cert.pem
pkey=/etc/coturn/private/privkey.pem
log-file=stdout
no-software-attribute
pidfile="/var/tmp/turnserver.pid"
no-cli
EOF
}
renderManagementJson() {
cat <<EOF
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:$NETBIRD_DOMAIN:3478"
}
],
"TURNConfig": {
"Turns": [
{
"Proto": "udp",
"URI": "turn:$NETBIRD_DOMAIN:3478",
"Username": "$TURN_USER",
"Password": "$TURN_PASSWORD"
}
],
"TimeBasedCredentials": false
},
"Signal": {
"Proto": "$NETBIRD_HTTP_PROTOCOL",
"URI": "$NETBIRD_DOMAIN:$NETBIRD_PORT"
},
"HttpConfig": {
"AuthIssuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN",
"AuthAudience": "$NETBIRD_AUTH_CLIENT_ID",
"OIDCConfigEndpoint":"$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/.well-known/openid-configuration"
},
"IdpManagerConfig": {
"ManagerType": "zitadel",
"ClientConfig": {
"Issuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"TokenEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/oauth/v2/token",
"ClientID": "$NETBIRD_IDP_MGMT_CLIENT_ID",
"ClientSecret": "$NETBIRD_IDP_MGMT_CLIENT_SECRET",
"GrantType": "client_credentials"
},
"ExtraConfig": {
"ManagementEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/management/v1"
}
},
"PKCEAuthorizationFlow": {
"ProviderConfig": {
"Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",
"ClientID": "$NETBIRD_AUTH_CLIENT_ID_CLI",
"Scope": "openid profile email offline_access",
"RedirectURLs": ["http://localhost:53000/","http://localhost:54000/"]
}
}
}
EOF
}
renderDashboardEnv() {
cat <<EOF
{
"auth0Auth": "false",
"authAuthority": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"authClientId": "$NETBIRD_AUTH_CLIENT_ID",
"authScopesSupported": "openid profile email offline_access",
"authAudience": "$NETBIRD_AUTH_CLIENT_ID",
"apiOrigin": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"grpcApiOrigin": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"redirectURI": "/nb-auth",
"silentRedirectURI": "/nb-silent-auth"
}
EOF
}
renderZitadelEnv() {
cat <<EOF
ZITADEL_LOG_LEVEL=debug
ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY
ZITADEL_DATABASE_COCKROACH_HOST=crdb
ZITADEL_DATABASE_COCKROACH_USER_USERNAME=zitadel_user
ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE=verify-full
ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT="/crdb-certs/ca.crt"
ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT="/crdb-certs/client.zitadel_user.crt"
ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY="/crdb-certs/client.zitadel_user.key"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_MODE=verify-full
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_ROOTCERT="/crdb-certs/ca.crt"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_CERT="/crdb-certs/client.root.crt"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_KEY="/crdb-certs/client.root.key"
ZITADEL_EXTERNALSECURE=$ZITADEL_EXTERNALSECURE
ZITADEL_TLS_ENABLED="false"
ZITADEL_EXTERNALPORT=$NETBIRD_PORT
ZITADEL_EXTERNALDOMAIN=$NETBIRD_DOMAIN
ZITADEL_FIRSTINSTANCE_PATPATH=/machinekey/zitadel-admin-sa.token
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_SCOPES=openid
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE=$ZIDATE_TOKEN_EXPIRATION_DATE
EOF
}
renderDockerCompose() {
cat <<EOF
version: "3.4"
services:
# Caddy reverse proxy
caddy:
image: caddy
restart: unless-stopped
networks: [ netbird ]
ports:
- '443:443'
- '80:80'
- '8080:8080'
volumes:
- netbird_caddy_data:/data
- ./Caddyfile:/etc/caddy/Caddyfile
# Management
management:
image: netbirdio/management:latest
restart: unless-stopped
networks: [netbird]
volumes:
- netbird_management:/var/lib/netbird
- ./management.json:/etc/netbird/management.json
command: [
"--port", "80",
"--log-file", "console",
"--log-level", "info",
"--disable-anonymous-metrics=false",
"--single-account-mode-domain=netbird.selfhosted",
"--dns-domain=netbird.selfhosted",
"--idp-sign-key-refresh-enabled",
]
# Zitadel - identity provider
zitadel:
restart: 'always'
networks: [netbird]
image: 'ghcr.io/zitadel/zitadel:v2.31.3'
command: 'start-from-init --masterkeyFromEnv --tlsMode $ZITADEL_TLS_MODE'
env_file:
- ./zitadel.env
depends_on:
crdb:
condition: 'service_healthy'
volumes:
- ./machinekey:/machinekey
- netbird_zitadel_certs:/crdb-certs:ro
# CockroachDB for zitadel
crdb:
restart: 'always'
networks: [netbird]
image: 'cockroachdb/cockroach:v22.2.2'
command: 'start-single-node --advertise-addr crdb'
volumes:
- netbird_crdb_data:/cockroach/cockroach-data
- netbird_crdb_certs:/cockroach/certs
- netbird_zitadel_certs:/zitadel-certs
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ]
interval: '10s'
timeout: '30s'
retries: 5
start_period: '20s'
volumes:
netbird_management:
netbird_caddy_data:
netbird_crdb_data:
netbird_crdb_certs:
netbird_zitadel_certs:
networks:
netbird:
EOF
}
initEnvironment

View File

@@ -1,56 +0,0 @@
import { Page, test, expect } from "@playwright/test";
export class AccessControlPage {
private readonly accessControlUrl = 'http://localhost:3000/acls'
private readonly defaulAccessControl = this.page.getByRole('cell', { name: 'Default' })
private readonly deleteButton = this.page.getByRole('button', { name: 'Delete' })
private readonly deleteModal = this.page.getByTestId('confirm-delete-modal-title')
private readonly confirmButton = this.page.getByRole('button', { name: 'OK' })
private readonly addRulesButton = this.page.getByTestId('add-rule-empty-state-button')
constructor(private readonly page: Page) {}
async openAccessControlPage() {
await test.step('Open Access Control page', async () => {
await this.page.goto(this.accessControlUrl);
})
}
async assertDefaultAccessCotrolIsCreated() {
await test.step('Assert that default cotrol access is created', async () => {
await expect(this.defaulAccessControl).toBeVisible();
})
}
async pressDeleteButton() {
await test.step('Press delete button', async () => {
await this.deleteButton.click();
})
}
async assertDeleteModalIsVisibile() {
await test.step('Assert access control deletion modal is visible', async () => {
await expect(this.deleteModal).toBeVisible();
})
}
async pressConfirmButton() {
await test.step('Press confirm button on access control deletion modal', async () => {
await this.confirmButton.click();
})
}
async assertDefaultAccessCotrolIsDeleted() {
await test.step('Assert default access control should be deleted', async () => {
await expect(this.defaulAccessControl).not.toBeVisible();
})
}
async assertAddRuleButtonIsVisile() {
await test.step('Assert Add Rules button is visible', async () => {
await expect(this.addRulesButton).toBeVisible();
})
}
}
export default AccessControlPage;

View File

@@ -1,34 +0,0 @@
import { Page, test, expect} from "@playwright/test";
export class LoginPage {
private readonly localUrl = 'http://localhost:3000/'
private readonly usernameField = this.page.getByPlaceholder('username@domain')
private readonly nextButton = this.page.getByRole('button', { name: 'next' })
private readonly passwordField = this.page.getByLabel('Password')
private readonly skipButton = this.page.getByRole('button', { name: 'skip' });
private readonly netBirdLogo = this.page.getByRole('link', { name: 'logo' })
constructor(private readonly page: Page) {}
async doLogin() {
await test.step('Login to local enviroment', async () => {
await this.page.goto(this.localUrl);
await this.usernameField.fill('admin@localhost');
await this.pressNextButton();
await this.passwordField.fill('testMe123@');
await this.pressNextButton();
if (await this.skipButton.isVisible({ timeout: 300 })) {
await this.skipButton.click();
}
await expect(this.netBirdLogo).toBeVisible();
})
}
async pressNextButton() {
await test.step('Press next button', async () => {
await this.nextButton.click();
})
}
}
export default LoginPage;

View File

@@ -1,111 +0,0 @@
import { Page, test, expect } from "@playwright/test";
export class AddPeerModal {
private readonly addPeerModal = this.page.getByTestId('add-peer-modal').locator('div').nth(2)
private readonly linuxTab = this.page.getByTestId('add-peer-modal-linux-tab')
private readonly windowsTab = this.page.getByTestId('add-peer-modal-windows-tab')
private readonly macTab = this.page.getByTestId('add-peer-modal-mac-tab')
private readonly androidTab = this.page.getByTestId('add-peer-modal-android-tab')
private readonly dockerTab = this.page.getByTestId('add-peer-modal-docker-tab')
private readonly linuxTabText = this.page.locator('pre').filter({ hasText: 'curl -fsSL https://pkgs.netbird.io/install.sh | sh' })
private readonly windowsDownloadButton = this.page.getByTestId('download-windows-button')
private readonly intelDownloadButton = this.page.getByTestId('download-intel-button')
private readonly m1M2DownloadButton = this.page.getByTestId('download-m1-m2-button')
private readonly androidDownloadButton = this.page.getByTestId('download-android-button')
private readonly dockerDownloadButton = this.page.getByTestId('download-docker-button')
private readonly closeButton = this.page.getByLabel('Close', { exact: true })
constructor(private readonly page: Page) {}
async assertPeerModalIsVisible() {
await test.step('Assert that add peer modal is visible', async () => {
await expect(this.addPeerModal).toBeVisible();
})
}
async assertPeerModalIsNotVisible() {
await test.step('Assert that add peer modal is not visible', async () => {
await expect(this.addPeerModal).not.toBeVisible();
})
}
async openLinuxTab() {
await test.step('Open Linux tab on add peer modal', async () => {
await this.linuxTab.click();
})
}
async openWindowsTab() {
await test.step('Open Windows tab on add peer modal', async () => {
await this.windowsTab.click();
})
}
async openMacTab() {
await test.step('Open MacOS tab on add peer modal', async () => {
await this.macTab.click();
})
}
async openAndroidTab() {
await test.step('Open Android tab on add peer modal', async () => {
await this.androidTab.click();
})
}
async openDockerTab() {
await test.step('Open Docker tab on add peer modal', async () => {
await this.dockerTab.click();
})
}
async assertLinuxTabHasCorrectText() {
await test.step('Assert Linux tab has correct installation text', async () => {
await expect(this.linuxTabText).toBeVisible();
})
}
async assertWindowsDownloadButtonHasCorrectLink() {
await test.step('Assert Windows download button has a correct link', async () => {
await expect(this.windowsDownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/windows/x64');
})
}
async assertIntelDownloadButtonHasCorrectLink() {
await test.step('Assert Intel download button has a correct link', async () => {
await expect(this.intelDownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/macos/amd64');
})
}
async assertM1M2DownloadButtonHasCorrectLink() {
await test.step('Assert M1 & M2 download button has a correct link', async () => {
await expect(this.m1M2DownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/macos/arm64');
})
}
async assertAndroidDownloadButtonHasCorrectLink() {
await test.step('Assert Android download button has a correct link', async () => {
await expect(this.androidDownloadButton).toHaveAttribute('href', 'https://play.google.com/store/apps/details?id=io.netbird.client');
})
}
async assertDockerDownloadButtonHasCorrectLink() {
await test.step('Assert Docker download button has a correct link', async () => {
await expect(this.dockerDownloadButton).toHaveAttribute('href', 'https://docs.docker.com/engine/install/');
})
}
async closeAddPeerModal() {
await test.step('Close Add peer modal', async () => {
await this.closeButton.click();
})
}
}
export default AddPeerModal;

View File

@@ -1,15 +0,0 @@
import { Page, test } from "@playwright/test";
export class PeersPage {
private readonly addNewPeerButton = this.page.getByTestId('add-new-peer-button')
constructor(private readonly page: Page) {}
async clickOnAddNewPeerButton() {
await test.step('Click on Add new peer Button to open Add peer modal', async () => {
await this.addNewPeerButton.click();
})
}
}
export default PeersPage;

View File

@@ -1,15 +0,0 @@
import { Page, test} from "@playwright/test";
export class TopMenu {
private readonly accessControlButton = this.page.getByTestId('access-control-page')
constructor(private readonly page: Page) {}
async clickOnAccessControlOnTopMenu() {
await test.step('Click on Access Control page on a top menu', async () => {
await this.accessControlButton.click();
})
}
}
export default TopMenu;

View File

@@ -1,22 +0,0 @@
import { test } from '@playwright/test'
import {LoginPage} from '../pages/login-page'
import {AccessControlPage} from '../pages/access-control-page'
let loginPage: LoginPage
let accessControlPage: AccessControlPage
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.doLogin();
});
test('Confirm that new user has Default access', async ({ page }) => {
accessControlPage = new AccessControlPage(page);
await accessControlPage.openAccessControlPage();
await accessControlPage.assertDefaultAccessCotrolIsCreated();
await accessControlPage.pressDeleteButton();
await accessControlPage.assertDeleteModalIsVisibile();
await accessControlPage.pressConfirmButton();
await accessControlPage.assertDefaultAccessCotrolIsDeleted();
await accessControlPage.assertAddRuleButtonIsVisile();
});

View File

@@ -1,49 +0,0 @@
import { test } from '@playwright/test'
import {AddPeerModal} from '../pages/modals/add-peer-modal'
import {PeersPage} from '../pages/peers-page'
import {LoginPage} from '../pages/login-page'
let addPeerModal: AddPeerModal
let peersPage: PeersPage
let loginPage: LoginPage
test.beforeEach(async ({ page }) => {
addPeerModal = new AddPeerModal(page);
loginPage = new LoginPage(page);
await loginPage.doLogin();
await addPeerModal.assertPeerModalIsVisible();
});
test('Test Linux tab on a first access add peer modal / @bc', async function () {
await addPeerModal.openLinuxTab();
await addPeerModal.assertLinuxTabHasCorrectText();
});
test('Test Windows tab on a first access add peer modal / @bc', async () => {
await addPeerModal.openWindowsTab();
await addPeerModal.assertWindowsDownloadButtonHasCorrectLink();
});
test('Test MacOS tab on a first access add peer modal / @bc', async () => {
await addPeerModal.openMacTab();
await addPeerModal.assertIntelDownloadButtonHasCorrectLink();
await addPeerModal.assertM1M2DownloadButtonHasCorrectLink();
});
test('Test Android tab on a first access add peer modal', async () => {
await addPeerModal.openAndroidTab();
await addPeerModal.assertAndroidDownloadButtonHasCorrectLink();
});
test('Test Docker tab on a first access add peer modal', async () => {
await addPeerModal.openDockerTab();
await addPeerModal.assertDockerDownloadButtonHasCorrectLink();
});
test('Close and open Add peer modal', async ({ page }) => {
peersPage = new PeersPage(page);
await addPeerModal.closeAddPeerModal();
await addPeerModal.assertPeerModalIsNotVisible();
await peersPage.clickOnAddNewPeerButton();
await addPeerModal.assertPeerModalIsVisible();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

9
next.config.js Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
images: {
unoptimized: true,
},
};
module.exports = nextConfig;

30427
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +1,78 @@
{
"name": "wiretrustee-dashboard",
"version": "0.1.0",
"name": "netbird-dashboard",
"version": "2.0.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@axa-fr/react-oidc": "^5.14.0",
"@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.4",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^27.5.1",
"@types/lodash": "^4.14.182",
"@types/node": "^17.0.35",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.5",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/styled-components": "^5.1.25",
"antd": "^5.3.1",
"autoprefixer": "^10.4.4",
"axios": "^0.27.2",
"cidr-regex": "^3.1.1",
"copyfiles": "^2.4.1",
"heroicons": "^1.0.6",
"highlight.js": "^11.2.0",
"history": "^5.0.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"postcss": "^8.4.12",
"prop-types": "^15.7.2",
"punycode": "^2.1.1",
"rc-overflow": "^1.2.8",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.1.0",
"react-hotjar": "^5.1.0",
"react-redux": "^8.0.2",
"react-router-dom": "^5.3.3",
"react-scripts": "^5.0.1",
"react-select": "^5.7.3",
"react-syntax-highlighter": "^15.5.0",
"react-table": "^7.7.0",
"redux": "^4.2.0",
"redux-devtools-extension": "^2.13.9",
"redux-saga": "^1.1.3",
"styled-components": "^5.3.5",
"tailwindcss": "^3.0.23",
"ts-md5": "^1.3.1",
"typesafe-actions": "^5.1.0",
"typescript": "^4.6.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"copy": "copyfiles -f ./node_modules/@axa-fr/react-oidc/dist/OidcServiceWorker.js ./public",
"copytrusted": "copyfiles -f ./public/local/OidcTrustedDomains.js ./public",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"dev": "next dev -p 3000",
"turbo": "next dev -p 3000 --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
"cypress:open": "cypress open"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"dependencies": {
"@axa-fr/react-oidc": "^5.14.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@tabler/icons-react": "^2.39.0",
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.7",
"@types/lodash": "^4.14.200",
"@types/node": "20.10.6",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"eslint": "^8",
"eslint-config-next": "13.5.5",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"flowbite": "^1.8.1",
"flowbite-react": "^0.6.4",
"framer-motion": "^10.16.4",
"ip-cidr": "^3.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.287.0",
"next": "13.5.5",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18",
"react-day-picker": "^8.9.1",
"react-dom": "^18",
"react-ga4": "^2.1.0",
"react-hot-toast": "^2.4.1",
"react-hotjar": "^6.2.0",
"react-hotkeys-hook": "^4.4.1",
"react-jwt": "^1.2.0",
"react-loading-skeleton": "^3.3.1",
"react-responsive": "^9.0.2",
"swr": "^2.2.4",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5"
},
"devDependencies": {
"@types/react-syntax-highlighter": "^15.5.3",
"@playwright/test": "^1.36.2"
"cypress": "^13.3.3",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"
}
}

View File

@@ -1,80 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e-tests/tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { channel: 'chrome', },
},
// {
// name: 'firefox',
// use: { browserName: 'firefox', },
// },
//
// {
// name: 'webkit',
// use: { browserName: 'webkit', },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
timeout: 180 * 1000,
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="robots" content="noindex">
<meta
name="description"
content="NetBird Management Dashboard"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>NetBird</title>
</head>
<body>
<noscript>NetBird Management Dashboard.</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -1,15 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow: /

View File

@@ -1,18 +0,0 @@
#!/bin/bash
MGMT_PORT=$1
npm run build
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
docker rm -f netbird-dashboard
docker run -d --name netbird-dashboard \
-p 3000:80 -p 443:443 \
-e AUTH_AUDIENCE=netbird-client \
-e AUTH_AUTHORITY=http://localhost:8080/realms/netbird \
-e AUTH_CLIENT_ID=netbird-client \
-e USE_AUTH0=false \
-e AUTH_SUPPORTED_SCOPES='openid profile email api offline_access' \
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
netbird/dashboard-local:latest

View File

@@ -1,16 +0,0 @@
#!/bin/bash
MGMT_PORT=$1
npm run build
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
docker rm -f netbird-dashboard
docker run -d --name netbird-dashboard \
-p 3000:80 -p 443:443 \
-e AUTH0_AUDIENCE=http://localhost:3000/ \
-e AUTH0_DOMAIN=netbird-localdev.eu.auth0.com \
-e AUTH0_CLIENT_ID=kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt \
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
netbird/dashboard-local:latest

View File

@@ -1,18 +0,0 @@
#!/bin/bash
MGMT_PORT=$1
npm run build
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
docker rm -f netbird-dashboard
docker run -d --name netbird-dashboard \
-p 3000:80 -p 443:443 \
-e AUTH_AUDIENCE=http://localhost:3000/ \
-e AUTH_AUTHORITY=https://netbird-localdev.eu.auth0.com \
-e AUTH_CLIENT_ID=kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt \
-e USE_AUTH0=true \
-e AUTH_SUPPORTED_SCOPES='openid profile email api offline_access email_verified' \
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
netbird/dashboard-local:latest

View File

@@ -1,11 +0,0 @@
{
"auth0Auth": "true",
"authAuthority": "https://netbird-localdev.eu.auth0.com",
"authClientId": "kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt",
"authScopesSupported": "openid profile email api offline_access email_verified",
"authAudience": "http://localhost:3000/",
"apiOrigin": "http://0.0.0.0",
"grpcApiOrigin": "http://0.0.0.0:80",
"latestVersion": "v0.6.3"
}

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,122 +0,0 @@
import React, {useEffect, useRef, useState} from 'react';
import {Provider} from "react-redux";
import {apiClient, store} from "./store";
import {hotjar} from 'react-hotjar';
import {getConfig} from "./config";
import Banner from "./components/Banner";
import {Col, ConfigProvider, Layout, Row} from "antd";
import {Container} from "./components/Container";
import Navbar from "./components/Navbar";
import {Redirect, Route, Switch} from "react-router-dom";
import {withOidcSecure} from "@axa-fr/react-oidc";
import Peers from "./views/Peers";
import Routes from "./views/Routes";
import AddPeer from "./views/AddPeer";
import SetupKeys from "./views/SetupKeys";
import AccessControl from "./views/AccessControl";
import Users from "./views/Users";
import FooterComponent from "./components/FooterComponent";
import {useGetTokenSilently, useTokenSource} from "./utils/token";
import {User} from "./store/user/types";
import {SecureLoading} from "./components/Loading";
import DNS from "./views/DNS";
import Activity from "./views/Activity";
import Settings from "./views/Settings";
const {Header, Content} = Layout;
function App() {
const run = useRef(false)
const [show, setShow] = useState(false)
const {hotjarTrackID,tokenSource} = getConfig();
useTokenSource(tokenSource)
const {getTokenSilently} = useGetTokenSilently();
// @ts-ignore
if (hotjarTrackID && window._DATADOG_SYNTHETICS_BROWSER === undefined) {
hotjar.initialize(hotjarTrackID, 6);
}
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const hideMenu = () => {
if (window.innerWidth > 768 && isOpen) {
setIsOpen(false);
}
};
window.addEventListener('resize', hideMenu);
return () => {
window.removeEventListener('resize', hideMenu);
};
}, []);
useEffect(() => {
if (!run.current) {
run.current = true
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getTokenSilently})
.then(() => {
setShow(true)
})
.catch(e => {
setShow(true)
console.log(e)
})
}
}, [getTokenSilently])
return (
<>
<Provider store={store}>
{!show && <SecureLoading padding="3em" width={50} height={50}/>}
{show &&
<Layout>
<Banner/>
<Header className="header" style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
alignContent: "center"
}}>
<Row justify="space-around" align="middle">
<Col span={24}>
<Container>
<Navbar/>
</Container>
</Col>
</Row>
</Header>
<Content style={{minHeight: "100vh"}}>
<Switch>
<Route
exact
path="/"
render={() => {
return (
<Redirect to="/peers"/>
)
}}
/>
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
<Route path="/routes" component={withOidcSecure(Routes)}/>
<Route path="/users" component={withOidcSecure(Users)}/>
<Route path="/dns" component={withOidcSecure(DNS)}/>
<Route path="/activity" component={withOidcSecure(Activity)}/>
<Route path="/settings" component={withOidcSecure(Settings)}/>
</Switch>
</Content>
<FooterComponent/>
</Layout>
}
</Provider>
</>
)
}
export default App;

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Access Control - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,66 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
import GroupsProvider from "@/contexts/GroupsProvider";
import PoliciesProvider from "@/contexts/PoliciesProvider";
import { Policy } from "@/interfaces/Policy";
import PageContainer from "@/layouts/PageContainer";
const AccessControlTable = lazy(
() => import("@/modules/access-control/table/AccessControlTable"),
);
export default function AccessControlPage() {
const { data: policies, isLoading } = useFetchApi<Policy[]>("/policies");
return (
<PageContainer>
<GroupsProvider>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/policies"}
label={"Access Control"}
icon={<AccessControlIcon size={13} />}
/>
</Breadcrumbs>
<h1>
{policies && policies.length > 1
? `${policies.length} Access Control Policies`
: "Access Control Policies"}
</h1>
<Paragraph>
Create rules to manage access in your network and define what peers
can connect.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={"https://docs.netbird.io/how-to/manage-network-access"}
target={"_blank"}
>
Access Controls
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess page={"Access Control"}>
<PoliciesProvider>
<Suspense fallback={<SkeletonTable />}>
<AccessControlTable isLoading={isLoading} policies={policies} />
</Suspense>
</PoliciesProvider>
</RestrictedAccess>
</GroupsProvider>
</PageContainer>
);
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Activity Events - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,58 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useFetchApi from "@utils/api";
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
import { ExternalLinkIcon } from "lucide-react";
import React from "react";
import ActivityIcon from "@/assets/icons/ActivityIcon";
import { ActivityEvent } from "@/interfaces/ActivityEvent";
import PageContainer from "@/layouts/PageContainer";
import ActivityTable from "@/modules/activity/ActivityTable";
import { EventStreamingCard } from "@/modules/integrations/event-streaming/EventStreamingCard";
export default function Activity() {
const { data: events, isLoading } = useFetchApi<ActivityEvent[]>("/events");
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/activity"}
label={"Activity"}
icon={<ActivityIcon size={13} />}
/>
</Breadcrumbs>
<h1>
{events && events.length > 1
? `${events.length} Activity Events`
: "Activity Events"}
</h1>
<Paragraph>
Here you can see all the account and network activity events.
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink
href={
"https://docs.netbird.io/how-to/monitor-system-and-network-activity"
}
target={"_blank"}
>
Activity Events
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess page={"Activity"}>
{(isLocalDev() || isNetBirdHosted()) && <EventStreamingCard />}
<ActivityTable events={events} isLoading={isLoading} />
</RestrictedAccess>
</PageContainer>
);
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Nameservers - DNS - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,70 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon, ServerIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import DNSIcon from "@/assets/icons/DNSIcon";
import { NameserverGroup } from "@/interfaces/Nameserver";
import PageContainer from "@/layouts/PageContainer";
const NameserverGroupTable = lazy(
() => import("@/modules/dns-nameservers/table/NameserverGroupTable"),
);
export default function NameServers() {
const { data: nameserverGroups, isLoading } =
useFetchApi<NameserverGroup[]>("/dns/nameservers");
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/dns"}
label={"DNS"}
icon={<DNSIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/dns/nameservers"}
label={"Nameservers"}
active
icon={<ServerIcon size={13} />}
/>
</Breadcrumbs>
<h1>
{nameserverGroups && nameserverGroups.length > 1
? `${nameserverGroups.length} Nameservers`
: "Nameservers"}
</h1>
<Paragraph>
Add nameservers for domain name resolution in your NetBird network.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={"https://docs.netbird.io/how-to/manage-dns-in-your-network"}
target={"_blank"}
>
DNS
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess page={"Nameservers"}>
<Suspense fallback={<SkeletonTable />}>
<NameserverGroupTable
nameserverGroups={nameserverGroups}
isLoading={isLoading}
/>
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}

View File

@@ -0,0 +1,15 @@
"use client";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function DNS() {
const router = useRouter();
useEffect(() => {
router.push("/dns/nameservers");
}, [router]);
return <FullScreenLoading height={"auto"} />;
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Settings - DNS - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,131 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import Card from "@components/Card";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Label } from "@components/Label";
import { notify } from "@components/Notification";
import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React from "react";
import { useSWRConfig } from "swr";
import DNSIcon from "@/assets/icons/DNSIcon";
import { useHasChanges } from "@/hooks/useHasChanges";
import { NameserverSettings } from "@/interfaces/NameserverSettings";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
export default function NameServerSettings() {
const { data: settings, isLoading } =
useFetchApi<NameserverSettings>("/dns/settings");
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/dns"}
label={"DNS"}
icon={<DNSIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/dns/settings"}
label={"DNS Settings"}
active
icon={<IconSettings2 size={15} />}
/>
</Breadcrumbs>
<h1>DNS Settings</h1>
<Paragraph>{"Manage your account's DNS settings."}</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={"https://docs.netbird.io/how-to/manage-dns-in-your-network"}
target={"_blank"}
>
DNS
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
<RestrictedAccess page={"DNS Settings"}>
{!isLoading && (
<SettingDisabledManagementGroups
initial={settings?.disabled_management_groups}
/>
)}
</RestrictedAccess>
</div>
</PageContainer>
);
}
const SettingDisabledManagementGroups = ({
initial,
}: {
initial: string[] | undefined;
}) => {
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
const { mutate } = useSWRConfig();
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initial || [],
});
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
selectedGroups,
]);
const saveSettings = async () => {
const savedGroups = await saveGroups();
notify({
title: "DNS Settings",
description: "Settings saved successfully.",
promise: settingRequest
.put({
disabled_management_groups: savedGroups.map((g) => g.id),
})
.then(() => {
mutate("/dns/settings");
updateChangesRef([selectedGroups]);
}),
loadingMessage: "Saving the settings...",
});
};
return (
<Card className={"mt-8 max-w-xl"}>
<div className={"px-8 py-8"}>
<Label>Disable DNS management for these groups</Label>
<HelpText>
Peers in these groups will require manual domain name resolution
</HelpText>
<PeerGroupSelector
onChange={setSelectedGroups}
values={selectedGroups}
/>
</div>
<div
className={
"flex justify-end bg-nb-gray-900/20 border-t border-nb-gray-900 px-8 py-5"
}
>
<Button
variant={"primary"}
size={"sm"}
onClick={saveSettings}
disabled={!hasChanges}
>
Save Changes
</Button>
</div>
</Card>
);
};

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Integrations - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,39 @@
"use client";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { VerticalTabs } from "@components/VerticalTabs";
import { FileText, FingerprintIcon } from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useState } from "react";
import PageContainer from "@/layouts/PageContainer";
import EventStreamingTab from "@/modules/integrations/event-streaming/EventStreamingTab";
import IdentityProviderTab from "@/modules/integrations/idp-sync/IdentityProviderTab";
export default function Integrations() {
const searchParams = useSearchParams();
const currentTab = searchParams.get("tab");
const [tab, setTab] = useState(currentTab || "event-streaming");
return (
<PageContainer>
<VerticalTabs value={tab} onChange={setTab}>
<VerticalTabs.List>
<VerticalTabs.Trigger value="event-streaming">
<FileText size={14} />
Event Streaming
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="identity-provider">
<FingerprintIcon size={14} />
Identity Provider
</VerticalTabs.Trigger>
</VerticalTabs.List>
<RestrictedAccess page={"Integrations"}>
<div className={"border-l border-nb-gray-930 w-full"}>
<EventStreamingTab />
<IdentityProviderTab />
</div>
</RestrictedAccess>
</VerticalTabs>
</PageContainer>
);
}

View File

@@ -0,0 +1,3 @@
import DashboardLayout from "@/layouts/DashboardLayout";
export default DashboardLayout;

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Network Routes - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,75 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import PeersProvider from "@/contexts/PeersProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { Route } from "@/interfaces/Route";
import PageContainer from "@/layouts/PageContainer";
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
const NetworkRoutesTable = lazy(
() => import("@/modules/route-group/NetworkRoutesTable"),
);
export default function NetworkRoutes() {
const { data: routes, isLoading } = useFetchApi<Route[]>("/routes");
const groupedRoutes = useGroupedRoutes({ routes });
return (
<PageContainer>
<RoutesProvider>
<PeersProvider>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/network-routes"}
label={"Network Routes"}
icon={<NetworkRoutesIcon size={13} />}
/>
</Breadcrumbs>
<h1>
{groupedRoutes && groupedRoutes.length > 1
? `${groupedRoutes.length} Network Routes`
: "Network Routes"}
</h1>
<Paragraph>
Network routes allow you to access other networks like LANs and
VPCs without installing NetBird on every resource.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
}
target={"_blank"}
>
Network Routes
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess>
<Suspense fallback={<SkeletonTable />}>
<NetworkRoutesTable
isLoading={isLoading}
groupedRoutes={groupedRoutes}
routes={routes}
/>
</Suspense>
</RestrictedAccess>
</PeersProvider>
</RoutesProvider>
</PageContainer>
);
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Peer - Peers - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,454 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import Card from "@components/Card";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import FullTooltip from "@components/FullTooltip";
import HelpText from "@components/HelpText";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import {
Modal,
ModalClose,
ModalContent,
ModalFooter,
ModalTrigger,
} from "@components/modal/Modal";
import ModalHeader from "@components/modal/ModalHeader";
import { notify } from "@components/Notification";
import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import dayjs from "dayjs";
import { trim } from "lodash";
import {
Cpu,
Globe,
History,
MapPin,
MonitorSmartphoneIcon,
PencilIcon,
TerminalSquare,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { toASCII } from "punycode";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import CircleIcon from "@/assets/icons/CircleIcon";
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import type { Peer } from "@/interfaces/Peer";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton";
import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
export default function PeerPage() {
const queryParameter = useSearchParams();
const peerId = queryParameter.get("id");
const { data: peer } = useFetchApi<Peer>("/peers/" + peerId);
return peer ? (
<PeerProvider peer={peer}>
<PeerOverview />
</PeerProvider>
) : (
<FullScreenLoading />
);
}
function PeerOverview() {
const router = useRouter();
const { mutate } = useSWRConfig();
const { peer, user, peerGroups, openSSHDialog, update } = usePeer();
const [ssh, setSsh] = useState(peer.ssh_enabled);
const [name, setName] = useState(peer.name);
const [showEditNameModal, setShowEditNameModal] = useState(false);
const [loginExpiration, setLoginExpiration] = useState(
peer.login_expiration_enabled,
);
const [selectedGroups, setSelectedGroups, { getAllGroupCalls }] =
useGroupHelper({
initial: peerGroups,
peer,
});
/**
* Check the operating system of the peer, if it is linux, then show the routes table, otherwise hide it.
*/
const isLinux = useMemo(() => {
const operatingSystem = getOperatingSystem(peer.os);
return operatingSystem == OperatingSystem.LINUX;
}, [peer.os]);
/**
* Detect if there are changes in the peer information, if there are changes, then enable the save button.
*/
const { hasChanges, updateRef: updateHasChangedRef } = useHasChanges([
name,
ssh,
selectedGroups,
loginExpiration,
]);
const updatePeer = async () => {
const updateRequest = update(name, ssh, loginExpiration);
const groupCalls = getAllGroupCalls();
const batchCall = groupCalls
? [...groupCalls, updateRequest]
: [updateRequest];
notify({
title: name,
description: "Peer was successfully saved",
promise: Promise.all(batchCall).then(() => {
mutate("/peers/" + peer.id);
mutate("/groups");
updateHasChangedRef([name, ssh, selectedGroups, loginExpiration]);
}),
loadingMessage: "Saving the peer...",
});
};
return (
<PageContainer>
<RoutesProvider>
<div className={"p-default py-6 mb-4"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/peers"}
label={"Peers"}
icon={<PeerIcon size={13} />}
/>
<Breadcrumbs.Item label={peer.ip} active />
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl items-start"}>
<div>
<div className={"flex items-center gap-3"}>
<h1 className={"flex items-center gap-3"}>
<CircleIcon
active={peer.connected}
size={12}
className={"mb-[3px]"}
/>
<TextWithTooltip text={name} maxChars={30} />
<Modal
open={showEditNameModal}
onOpenChange={setShowEditNameModal}
>
<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"
}
>
<PencilIcon size={16} />
</div>
</ModalTrigger>
<EditNameModal
onSuccess={(newName) => {
setName(newName);
setShowEditNameModal(false);
}}
peer={peer}
initialName={name}
key={showEditNameModal ? 1 : 0}
/>
</Modal>
</h1>
<LoginExpiredBadge loginExpired={peer.login_expired} />
</div>
<div className={"flex items-center gap-8"}>
<Paragraph className={"flex items-center"}>
{user?.email}
</Paragraph>
</div>
</div>
<div className={"flex gap-4"}>
<Button
variant={"default"}
className={"w-full"}
onClick={() => router.push("/peers")}
>
Cancel
</Button>
<Button
variant={"primary"}
className={"w-full"}
onClick={() => updatePeer()}
disabled={!hasChanges}
>
Save Changes
</Button>
</div>
</div>
<div className={"flex gap-10 w-full mt-5 max-w-6xl"}>
<PeerInformationCard peer={peer} />
<div className={"flex flex-col gap-6 w-1/2"}>
<FullTooltip
content={
<div
className={
"flex gap-2 items-center !text-nb-gray-300 text-xs"
}
>
<IconInfoCircle size={14} />
<span>
Login expiration is disabled for all peers added with an
setup-key.
</span>
</div>
}
className={"w-full block"}
disabled={!!peer.user_id}
>
<FancyToggleSwitch
disabled={!peer.user_id}
value={loginExpiration}
onChange={setLoginExpiration}
label={
<>
<IconCloudLock size={16} />
Login Expiration
</>
}
helpText={
"Enable to require SSO login peers to re-authenticate when their login expires."
}
/>
</FullTooltip>
<FancyToggleSwitch
value={ssh}
onChange={(set) =>
!set
? setSsh(false)
: openSSHDialog().then((confirm) => setSsh(confirm))
}
label={
<>
<TerminalSquare size={16} />
SSH Access
</>
}
helpText={
"Enable the SSH server on this peer to access the machine via an secure shell."
}
/>
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<PeerGroupSelector
onChange={setSelectedGroups}
values={selectedGroups}
peer={peer}
/>
</div>
</div>
</div>
</div>
<Separator />
{isLinux ? (
<div className={"px-8 py-6"}>
<div className={"max-w-6xl"}>
<div className={"flex justify-between items-center"}>
<div>
<h2>Network Routes</h2>
<Paragraph>
Access other networks without installing NetBird on every
resource.
</Paragraph>
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div>
<AddRouteDropdownButton />
</div>
</div>
</div>
<PeerRoutesTable peer={peer} />
</div>
</div>
) : null}
</RoutesProvider>
</PageContainer>
);
}
function PeerInformationCard({ peer }: { peer: Peer }) {
return (
<Card>
<Card.List>
<Card.ListItem
label={
<>
<MapPin size={16} />
NetBird IP-Address
</>
}
value={peer.ip}
/>
<Card.ListItem
label={
<>
<Globe size={16} />
Domain Name
</>
}
value={peer.dns_label}
/>
<Card.ListItem
label={
<>
<MonitorSmartphoneIcon size={16} />
Hostname
</>
}
value={peer.hostname}
/>
<Card.ListItem
label={
<>
<Cpu size={16} />
Operating System
</>
}
value={peer.os}
/>
<Card.ListItem
label={
<>
<History size={16} />
Last seen
</>
}
value={
dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(peer.last_seen) +
")"
}
/>
<Card.ListItem
label={
<>
<NetBirdIcon size={16} />
Agent Version
</>
}
value={peer.version}
/>
<Card.ListItem
label={
<>
<NetBirdIcon size={16} />
UI Version
</>
}
value={peer.ui_version?.replace("netbird-desktop-ui/", "")}
/>
</Card.List>
</Card>
);
}
interface ModalProps {
onSuccess: (name: string) => void;
peer: Peer;
initialName: string;
}
function EditNameModal({ onSuccess, peer, initialName }: ModalProps) {
const [name, setName] = useState(initialName);
const isDisabled = useMemo(() => {
if (name === peer.name) return true;
const trimmedName = trim(name);
return trimmedName.length === 0;
}, [name, peer]);
const domainNamePreview = useMemo(() => {
let punyName = toASCII(name.toLowerCase());
punyName = punyName.replace(/[^a-z0-9]/g, "-");
let domain = "";
if (peer.dns_label) {
const labelList = peer.dns_label.split(".");
if (labelList.length > 1) {
labelList.splice(0, 1);
domain = "." + labelList.join(".");
}
}
return punyName + domain;
}, [name, peer]);
return (
<ModalContent maxWidthClass={"max-w-md"}>
<form>
<ModalHeader
title={"Edit Peer Name"}
description={"Set an easily identifiable name for your peer."}
color={"blue"}
/>
<div className={"p-default flex flex-col gap-4"}>
<div>
<Input
placeholder={"e.g., AWS Servers"}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<Card className={"w-full px-6 pt-5 pb-4"}>
<Label>
<Globe size={15} />
Domain Name Preview
</Label>
<HelpText className={"mt-2"}>
If the domain name already exists, we add an increment number
suffix to it.
</HelpText>
<div className={"text-netbird text-sm break-all whitespace-normal"}>
{domainNamePreview}
</div>
</Card>
</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(name)}
disabled={isDisabled}
type={"submit"}
>
Save
</Button>
</div>
</ModalFooter>
</form>
</ModalContent>
);
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Peers - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,61 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import PeerIcon from "@/assets/icons/PeerIcon";
import { useUsers } from "@/contexts/UsersProvider";
import { Peer } from "@/interfaces/Peer";
import PageContainer from "@/layouts/PageContainer";
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
export default function Peers() {
const { data: peers, isLoading } = useFetchApi<Peer[]>("/peers");
const { users } = useUsers();
const peersWithUser = peers?.map((peer) => {
if (!users) return peer;
return {
...peer,
user: users?.find((user) => user.id === peer.user_id),
};
});
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/peers"}
label={"Peers"}
icon={<PeerIcon size={13} />}
/>
</Breadcrumbs>
<h1>{peers && peers.length > 1 ? `${peers.length} Peers` : "Peers"}</h1>
<Paragraph>
A list of all machines and devices connected to your private network.
Use this view to manage peers.
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/add-machines-to-your-network"}
target={"_blank"}
>
Peers
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<Suspense fallback={<SkeletonTable />}>
<PeersTable isLoading={isLoading} peers={peersWithUser} />
</Suspense>
</PageContainer>
);
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Settings - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,46 @@
"use client";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { VerticalTabs } from "@components/VerticalTabs";
import { AlertOctagonIcon, FolderGit2Icon, ShieldIcon } from "lucide-react";
import React, { useState } from "react";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import PageContainer from "@/layouts/PageContainer";
import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
import GroupsTab from "@/modules/settings/GroupsTab";
export default function NetBirdSettings() {
const [tab, setTab] = useState("authentication");
const { isOwner } = useLoggedInUser();
const account = useAccount();
return (
<PageContainer>
<VerticalTabs value={tab} onChange={setTab}>
<VerticalTabs.List>
<VerticalTabs.Trigger value="authentication">
<ShieldIcon size={14} />
Authentication
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="groups">
<FolderGit2Icon size={14} />
Groups
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
<AlertOctagonIcon size={14} />
Danger zone
</VerticalTabs.Trigger>
</VerticalTabs.List>
<RestrictedAccess page={"Settings"}>
<div className={"border-l border-nb-gray-930 w-full"}>
{account && <AuthenticationTab account={account} />}
{account && <GroupsTab account={account} />}
{account && <DangerZoneTab account={account} />}
</div>
</RestrictedAccess>
</VerticalTabs>
</PageContainer>
);
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Setup Keys - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,79 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import { useGroups } from "@/contexts/GroupsProvider";
import { Group } from "@/interfaces/Group";
import { SetupKey } from "@/interfaces/SetupKey";
import PageContainer from "@/layouts/PageContainer";
const SetupKeysTable = lazy(
() => import("@/modules/setup-keys/SetupKeysTable"),
);
export default function SetupKeys() {
const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys");
const { groups } = useGroups();
const setupKeysWithGroups = setupKeys?.map((setupKey) => {
if (!setupKey.auto_groups) return setupKey;
if (!groups) return setupKey;
return {
...setupKey,
groups: setupKey.auto_groups.map((group) => {
return groups.find((g) => g.id === group) || undefined;
}) as Group[] | undefined,
};
});
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/setup-keys"}
label={"Setup Keys"}
icon={<SetupKeysIcon size={13} />}
/>
</Breadcrumbs>
<h1>
{setupKeys && setupKeys.length > 1
? `${setupKeys.length} Setup Keys`
: "Setup Keys"}
</h1>
<Paragraph>
Setup keys are pre-authentication keys that allow to register new
machines in your network.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
}
target={"_blank"}
>
Setup Keys
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess page={"Setup Keys"}>
<Suspense fallback={<SkeletonTable />}>
<SetupKeysTable
setupKeys={setupKeysWithGroups}
isLoading={isLoading}
/>
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}

View File

@@ -0,0 +1,15 @@
"use client";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Team() {
const router = useRouter();
useEffect(() => {
router.push("/team/users");
}, [router]);
return <FullScreenLoading height={"auto"} />;
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Service Users - Team - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,69 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { IconSettings2 } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import TeamIcon from "@/assets/icons/TeamIcon";
import { User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
const ServiceUsersTable = lazy(
() => import("@/modules/users/ServiceUsersTable"),
);
export default function ServiceUsers() {
const { data: users, isLoading } = useFetchApi<User[]>(
"/users?service_user=true",
);
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/team"}
label={"Team"}
icon={<TeamIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/team/service-users"}
label={"Service Users"}
active
icon={<IconSettings2 size={17} />}
/>
</Breadcrumbs>
<h1>
{users && users.length > 1
? `${users.length} Service Users`
: "Service Users"}
</h1>
<Paragraph>
Use service users to create API tokens and avoid losing automated
access.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={"https://docs.netbird.io/how-to/access-netbird-public-api"}
target={"_blank"}
>
Service Users
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess page={"Service Users"}>
<Suspense fallback={<SkeletonTable />}>
<ServiceUsersTable users={users} isLoading={isLoading} />
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `User - Team - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,326 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import Card from "@components/Card";
import HelpText from "@components/HelpText";
import { Label } from "@components/Label";
import { notify } from "@components/Notification";
import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api";
import { generateColorFromString } from "@utils/helpers";
import dayjs from "dayjs";
import { Ban, GalleryHorizontalEnd, History, Mail, User2 } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import TeamIcon from "@/assets/icons/TeamIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
import { Role, User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
import AccessTokensTable from "@/modules/access-tokens/AccessTokensTable";
import CreateAccessTokenModal from "@/modules/access-tokens/CreateAccessTokenModal";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
export default function UserPage() {
const queryParameter = useSearchParams();
const userId = queryParameter.get("id");
const isServiceUser = queryParameter.get("service_user") === "true";
const { data: users, isLoading } = useFetchApi<User[]>(
`/users?service_user=${isServiceUser}`,
);
const user = useMemo(() => {
return users?.find((u) => u.id === userId);
}, [users, userId]);
return !isLoading && user ? (
<UserOverview user={user} />
) : (
<FullScreenLoading />
);
}
type Props = {
user: User;
};
function UserOverview({ user }: Props) {
const router = useRouter();
const userRequest = useApiCall<User>("/users");
const { mutate } = useSWRConfig();
const { loggedInUser, isOwnerOrAdmin } = useLoggedInUser();
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
const initialGroups = user.auto_groups;
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initialGroups,
});
const [role, setRole] = useState(user.role || Role.User);
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
role,
selectedGroups,
]);
const save = async () => {
const groups = await saveGroups();
const groupIds = groups.map((group) => group.id) as string[];
notify({
title: user.name,
description: "Changes successfully saved.",
promise: userRequest
.put(
{
role: role,
auto_groups: groupIds,
is_blocked: user.is_blocked,
},
`/${user.id}`,
)
.then(() => {
mutate(`/users?service_user=${user.is_service_user}`);
updateChangesRef([role, selectedGroups]);
}),
loadingMessage: "Saving changes...",
});
};
return (
<PageContainer>
<div className={"p-default py-6 mb-4"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/team"}
label={"Team"}
icon={<TeamIcon size={13} />}
/>
{user.is_service_user ? (
<Breadcrumbs.Item
href={"/team/service-users"}
label={"Service Users"}
icon={<IconSettings2 size={17} />}
/>
) : (
<Breadcrumbs.Item
href={"/team/users"}
label={"Users"}
icon={<User2 size={16} />}
/>
)}
<Breadcrumbs.Item label={user.name || user.id} active />
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl"}>
<div>
<div className={"flex items-center gap-3"}>
<div
className={
"w-10 h-10 rounded-full relative flex items-center justify-center text-white uppercase text-md font-medium bg-nb-gray-900"
}
style={
user.is_service_user
? {
color: "white",
}
: {
color: user?.name
? generateColorFromString(
user?.name || user?.id || "System User",
)
: "#808080",
}
}
>
{user.is_service_user ? (
<IconSettings2 size={16} />
) : (
user?.name?.charAt(0) || user?.id?.charAt(0)
)}
</div>
<h1 className={"flex items-center gap-3"}>
{user.name || user.id}
</h1>
</div>
</div>
<div className={"flex gap-4"}>
<Button
variant={"default"}
className={"w-full"}
onClick={() => {
user.is_service_user
? router.push("/team/service-users")
: router.push("/team/users");
}}
>
Cancel
</Button>
<Button
variant={"primary"}
className={"w-full"}
disabled={!hasChanges}
onClick={save}
>
Save Changes
</Button>
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl"}>
<UserInformationCard user={user} />
<div className={"flex flex-col gap-8 w-1/2 "}>
{!user.is_service_user && (
<div>
<Label>Auto-assigned groups</Label>
<HelpText>
Groups will be assigned to peers added by this user.
</HelpText>
<PeerGroupSelector
onChange={setSelectedGroups}
values={selectedGroups}
/>
</div>
)}
<div className={"flex items-start"}>
<div className={"w-2/3"}>
<Label>User Role</Label>
<HelpText>
Set a role for the user to assign access permissions.
</HelpText>
</div>
<div className={"w-1/3"}>
<UserRoleSelector
value={role}
onChange={setRole}
disabled={
isLoggedInUser ||
!isOwnerOrAdmin ||
user.role === Role.Owner
}
/>
</div>
</div>
</div>
</div>
</div>
{(user.is_current || user.is_service_user) && (
<>
<Separator />
<div className={"px-8 py-6"}>
<div className={"max-w-6xl"}>
<div className={"flex justify-between items-center"}>
<div>
<h2>Access Tokens</h2>
<Paragraph>
Access tokens give access to NetBird API.
</Paragraph>
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div>
<CreateAccessTokenModal user={user}>
<Button variant={"primary"}>
<IconCirclePlus size={16} />
Create Access Token
</Button>
</CreateAccessTokenModal>
</div>
</div>
</div>
<AccessTokensTable user={user} />
</div>
</div>
</>
)}
</PageContainer>
);
}
function UserInformationCard({ user }: { user: User }) {
const isServiceUser = user.is_service_user || false;
const neverLoggedIn = dayjs(user.last_login).isBefore(
dayjs().subtract(1000, "years"),
);
return (
<Card>
<Card.List>
<Card.ListItem
label={
<>
<User2 size={16} />
{user.name ? "Name" : "User ID"}
</>
}
value={user.name || user.id}
/>
{!isServiceUser && (
<Card.ListItem
label={
<>
<Mail size={16} />
E-Mail
</>
}
value={user.email || "-"}
/>
)}
<Card.ListItem
label={
<>
<GalleryHorizontalEnd size={16} />
Status
</>
}
value={<UserStatusCell user={user} />}
/>
{!isServiceUser && (
<>
<Card.ListItem
label={
<>
<Ban size={16} />
Block User
</>
}
value={<UserBlockCell user={user} isUserPage={true} />}
/>
<Card.ListItem
label={
<>
<History size={16} />
Last login
</>
}
value={
neverLoggedIn
? "Never"
: dayjs(user.last_login).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(user.last_login) +
")"
}
/>
</>
)}
</Card.List>
</Card>
);
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Users - Team - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -0,0 +1,62 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon, User2 } from "lucide-react";
import React, { lazy, Suspense } from "react";
import TeamIcon from "@/assets/icons/TeamIcon";
import { User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
export default function TeamUsers() {
const { data: users, isLoading } = useFetchApi<User[]>(
"/users?service_user=false",
);
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/team"}
label={"Team"}
icon={<TeamIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/team/users"}
label={"Users"}
active
icon={<User2 size={16} />}
/>
</Breadcrumbs>
<h1>{users && users.length > 1 ? `${users.length} Users` : "Users"}</h1>
<Paragraph>
Manage users and their permissions. Same-domain email users are added
automatically on first sign-in.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={"https://docs.netbird.io/how-to/add-users-to-your-network"}
target={"_blank"}
>
Users
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess page={"Users"}>
<Suspense fallback={<SkeletonTable />}>
<UsersTable users={users} isLoading={isLoading} />
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}

BIN
src/app/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

67
src/app/globals.css Normal file
View File

@@ -0,0 +1,67 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
h1 {
@apply text-2xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
}
h2 {
@apply text-xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
}
p {
@apply font-light tracking-wide text-gray-700 dark:text-zinc-50 text-sm;
}
.p-default {
@apply px-4 sm:px-6 md:px-8;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
[placeholder]{
text-overflow:ellipsis;
}
.animated-gradient-bg{
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
height: 100%;
}
@keyframes gradient {
0% {
background-position: 0 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
.sticky {
position: sticky !important;
left: 0;
top: 0;
z-index: 1;
}
.table-fixed-scroll {
display: table;
position: relative;
width: 100%;
}

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Installation - ${globalMetaTitle}`,
};
export default BlankLayout;

19
src/app/install/page.tsx Normal file
View File

@@ -0,0 +1,19 @@
"use client";
import { Modal } from "@components/modal/Modal";
import { useEffect, useState } from "react";
import SetupModal from "@/modules/setup-netbird-modal/SetupModal";
export default function UnauthenticatedInstallModal() {
const [open, setOpen] = useState(false);
useEffect(() => {
setOpen(true);
}, []);
return (
<Modal onOpenChange={() => null} open={open}>
<SetupModal showClose={false} />
</Modal>
);
}

10
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import AppLayout from "@/layouts/AppLayout";
export const metadata: Metadata = {
title: `${globalMetaTitle}`,
description:
"NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single open-source platform",
};
export default AppLayout;

14
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,14 @@
"use client";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function NotFound() {
const router = useRouter();
useEffect(() => {
router.push("/peers");
});
return <FullScreenLoading />;
}

9
src/app/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
"use client";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { useRedirect } from "@hooks/useRedirect";
export default function Home() {
useRedirect("/peers");
return <FullScreenLoading />;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
src/assets/avatars/009.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
src/assets/avatars/030.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
src/assets/avatars/063.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
src/assets/avatars/086.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -1,52 +0,0 @@
<svg width="135" height="140" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill-opacity="0.8">
<rect y="10" width="15" height="120" rx="6" fill="#FF6600">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="30" y="10" width="15" height="120" rx="6" fill="#FF6600">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="60" width="15" height="140" rx="6" fill="#FF6600">
<animate attributeName="height"
begin="0s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="90" y="10" width="15" height="120" rx="6" fill="#FF6600">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="120" y="10" width="15" height="120" rx="6" fill="#FF6600">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,20 @@
import Image from "next/image";
import * as React from "react";
import euIcon from "@/assets/countries/eu.svg";
export const CountryEURounded = () => {
return (
<div
className={
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
}
>
<Image
src={euIcon}
alt={"eu"}
fill={true}
className={"object-cover object-center shrink-0"}
/>
</div>
);
};

View File

@@ -0,0 +1,20 @@
import Image from "next/image";
import * as React from "react";
import jpIcon from "@/assets/countries/jp.svg";
export const CountryJPRounded = () => {
return (
<div
className={
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
}
>
<Image
src={jpIcon}
alt={"eu"}
fill={true}
className={"object-cover object-center"}
/>
</div>
);
};

View File

@@ -0,0 +1,20 @@
import Image from "next/image";
import * as React from "react";
import usIcon from "@/assets/countries/us.svg";
export const CountryUSRounded = () => {
return (
<div
className={
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
}
>
<Image
src={usIcon}
alt={"us"}
fill={true}
className={"object-cover object-center"}
/>
</div>
);
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 810 540"><defs><g id="d"><g id="b"><path id="a" d="M0 0v1h.5z" transform="rotate(18 3.157 -.5)"/><use xlink:href="#a" transform="scale(-1 1)"/></g><g id="c"><use xlink:href="#b" transform="rotate(72)"/><use xlink:href="#b" transform="rotate(144)"/></g><use xlink:href="#c" transform="scale(-1 1)"/></g></defs><path fill="#039" d="M0 0h810v540H0z"/><g fill="#fc0" transform="matrix(30 0 0 30 405 270)"><use xlink:href="#d" y="-6"/><use xlink:href="#d" y="6"/><g id="e"><use xlink:href="#d" x="-6"/><use xlink:href="#d" transform="rotate(-144 -2.344 -2.11)"/><use xlink:href="#d" transform="rotate(144 -2.11 -2.344)"/><use xlink:href="#d" transform="rotate(72 -4.663 -2.076)"/><use xlink:href="#d" transform="rotate(72 -5.076 .534)"/></g><use xlink:href="#e" transform="scale(-1 1)"/></g></svg>

After

Width:  |  Height:  |  Size: 888 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600">
<rect fill="#fff" height="600" width="900"/>
<circle fill="#bc002d" cx="450" cy="300" r="180"/>
</svg>

After

Width:  |  Height:  |  Size: 166 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 7410 3900"><path fill="#b22234" d="M0 0h7410v3900H0z"/><path d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0" stroke="#fff" stroke-width="300"/><path fill="#3c3b6e" d="M0 0h2964v2100H0z"/><g fill="#fff"><g id="d"><g id="c"><g id="e"><g id="b"><path id="a" d="M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"/><use xlink:href="#a" y="420"/><use xlink:href="#a" y="840"/><use xlink:href="#a" y="1260"/></g><use xlink:href="#a" y="1680"/></g><use xlink:href="#b" x="247" y="210"/></g><use xlink:href="#c" x="494"/></g><use xlink:href="#d" x="988"/><use xlink:href="#c" x="1976"/><use xlink:href="#e" x="2470"/></g></svg>

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Some files were not shown because too many files have changed in this diff Show More