* Replace the pending-approval error page with a dedicated screen A user waiting for an administrator to approve their account was shown the generic /error page: an error icon, a raw response_message dump and "if you believe this is an error, please contact your administrator". Waiting for approval is an expected part of signing up, so it now gets a screen of its own, following the onboarding layout of logo, centered card and a stepper placing the user between "Account Created" and "Join Account". The owner to contact is read out of the refusal management sends, which names a masked address, and the copy asks for the owner without naming one when the address is absent. Steps.Step gains an optional status and size, so a step can render as complete, current or upcoming at a larger scale. Steps that pass neither keep the look used by the instruction-list steppers. * Detect the pending-approval screen by test id in e2e The three places that recognise a pending user matched on "User Approval Pending", the heading the old error page rendered. The screen that replaced it has no heading, so the approval branch of the login races could never resolve and every leg timed out instead: both fixtures treat approval as a legitimate outcome, not a failure. They now key off the data-testid on the screen's root, which does not move when the copy does. * Point the pending-approval footer at the docs and support pages A mailbox asks the reader to compose something; the approval docs explain what they are waiting for, and the support page routes them the way every other NetBird surface does. The support address constant went with it. * Render the pending-approval screen from the caller-identity response Every call a pending user makes is refused, but only /users/current names the owner who can approve them. Redirecting to /error on whichever 403 landed first therefore threw that name away about as often as it kept it, and which screen appeared came down to a race. Pending users are no longer routed to /error at all. The provider that already waits on /users/current renders the screen from that response, which also ends the wait it could never finish: the caller identity it blocks on is exactly what such a user is refused. * Decide the refused-user screen from the responses, not the first 403 Routing on whichever refused call landed first meant the screen depended on a race, and once only /users/current could name the owner it meant the name was usually thrown away. Management older than that reports a pending user as merely blocked there, so it also meant a pending user landed on the blocked screen, which is what the e2e run caught. UserProfileProvider now decides from the responses it already waits on: /users/current first, since it alone names the owner, then the user list, which reports pending on every management version. Blocked keeps the error page, decided after pending so the older wording cannot win. * Wait for both refusal responses before choosing a screen Concluding blocked the moment /users/current said so meant concluding it while the user list was still in flight — and on management that reports a pending user as merely blocked there, that list is the one response that would have said otherwise. The screen it was meant to prevent was reachable again, just through a narrower window. The decision moves into resolveRefusedUser, which holds off while a better-informed response is still coming: the named one before the list, the list before blocked. Navigating to the error page becomes an effect rather than a call made while rendering. * Move the refused-user decision next to the provider that makes it It sat in utils only because two callers needed it, but the second one — the guard keeping these errors off the error boundary — needs nothing more than to recognise them, which is a line of its own. So the decision moves beside the provider that acts on it, and reads an error into a RefusalReason rather than assembling a discriminated union. The cost is that both files now spell out management's wording. The one here is the tested reading; the one in @utils/api only has to be no narrower than it. * Assert the refusal kind the resolver actually returns The result property is kind, not reason. toEqual types its argument loosely, so the stale expectations type-checked and only failed when run.
NetBird Dashboard
This project is the UI for NetBird's Management service.
Hosted version: https://app.netbird.io/
See NetBird repo
Why?
The purpose of this project is simple - make it easy to manage VPN built with NetBird. The dashboard makes it possible to:
- track the status of your peers
- remove peers
- manage Setup Keys (to authenticate new peers)
- list users
- define access controls
Some Screenshots
Technologies Used
- NextJS
- ReactJS
- Tailwind CSS
- React Flow for the Control Center
- Auth0
- Nginx
- Docker
- Let's Encrypt
How to run
Disclaimer. We believe that proper user management system is not a trivial task and requires quite some effort to make it right. Therefore we decided to use Auth0 service that covers all our needs (user management, social login, JWT for the management API). Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
-
Install Docker
-
Register Auth0 account
-
Running NetBird UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
AUTH0_DOMAINAUTH0_CLIENT_IDAUTH0_AUDIENCETo obtain these, please use Auth0 React SDK Guide up until "Configure Allowed Web Origins"
-
NetBird UI Dashboard uses NetBird's Management Service HTTP API, so setting
NETBIRD_MGMT_API_ENDPOINTis required. Most likely it will behttp://localhost:33071if you are hosting Management API on the same server. -
Run docker container without SSL (Let's Encrypt):
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 MANAGEMENT API URL> \ netbirdio/dashboard:main -
Run docker container with SSL (Let's Encrypt):
docker run -d --name netbird-dashboard \ --rm -p 80:80 -p 443:443 \ -e NGINX_SSL_PORT=443 \ -e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> \ -e LETSENCRYPT_EMAIL=<YOUR EMAIL> \ -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \ -e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> \ -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \ -e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMENT API URL> \ netbirdio/dashboard:main
How to run local development
- Install Node
- Create and update the
.local-config.jsonfile. This file should contain values to be replaced fromconfig.json - Run
npm installto install dependencies - Run
npm run devto start the development server
Open 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.
- Stop the dashboard container
docker compose down dashboard - Replace the docker image name in your
docker-compose.ymlwithnetbirdio/dashboard:main - Recreate the dashboard container
docker compose up -d --force-recreate dashboard