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>
13
.eslintrc.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
.github/workflows/build_and_push.yml
vendored
@@ -7,6 +7,9 @@ on:
|
|||||||
- "**"
|
- "**"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: netbirdio/dashboard
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_n_push:
|
build_n_push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -16,15 +19,16 @@ jobs:
|
|||||||
- name: setup-node
|
- name: setup-node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '18'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- run: echo '{}' > .local-config.json
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
# skiping fail on warning for now
|
run: npm run build
|
||||||
run: CI=false npm run build
|
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
@@ -36,14 +40,14 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: wiretrustee/dashboard
|
images: ${{ env.IMAGE_NAME }}
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.NB_DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.NB_DOCKER_TOKEN }}
|
||||||
-
|
-
|
||||||
name: Docker build and push
|
name: Docker build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
|
|||||||
42
.github/workflows/e2e-tests.yml
vendored
@@ -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
@@ -2,42 +2,42 @@
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/node_modules.bkp
|
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/out
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
*.pem
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
src/auth_config.json
|
# local env files
|
||||||
.idea
|
.env*.local
|
||||||
.eslintcache
|
|
||||||
src/.local-config*.json
|
# vercel
|
||||||
/public/OidcServiceWorker.js
|
.vercel
|
||||||
/public/OidcTrustedDomains.js
|
|
||||||
/e2e-tests/node_modules/
|
# typescript
|
||||||
/e2e-tests/playwright-report/
|
*.tsbuildinfo
|
||||||
/e2e-tests/test-results/
|
next-env.d.ts
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
# config
|
||||||
.env
|
.local-config.json
|
||||||
Caddyfile
|
.configs/.local-config.zitadel.json
|
||||||
docker-compose.yml
|
.configs/.staging-config.json
|
||||||
machinekey/
|
.configs/.temp-config.json
|
||||||
management.json
|
.configs
|
||||||
turnserver.conf
|
|
||||||
zitadel.env
|
|
||||||
|
|||||||
1
AUTHORS
@@ -1,2 +1,3 @@
|
|||||||
Mikhail Bragin (https://github.com/braginini)
|
Mikhail Bragin (https://github.com/braginini)
|
||||||
Maycon Santos (https://github.com/mlsmaycon)
|
Maycon Santos (https://github.com/mlsmaycon)
|
||||||
|
Wiretrustee UG (haftungsbeschränkt)
|
||||||
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
BSD 3-Clause License
|
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:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
|||||||
45
README.md
@@ -1,4 +1,4 @@
|
|||||||
# NetBird dashboard
|
# NetBird Dashboard
|
||||||
|
|
||||||
This project is the UI for NetBird's Management service.
|
This project is the UI for NetBird's Management service.
|
||||||
|
|
||||||
@@ -17,15 +17,15 @@ The dashboard makes it possible to:
|
|||||||
- define access controls
|
- define access controls
|
||||||
|
|
||||||
## Some Screenshots
|
## Some Screenshots
|
||||||
<img src="./media/auth.png" alt="auth"/>
|
<img src="./src/assets/screenshots/peers.png" alt="peers"/>
|
||||||
<img src="./media/peers.png" alt="peers"/>
|
<img src="./src/assets/screenshots/add-peer.png" alt="add-peer"/>
|
||||||
<img src="./media/add-peer.png" alt="add-peer"/>
|
|
||||||
|
|
||||||
|
|
||||||
## Technologies Used
|
## Technologies Used
|
||||||
|
|
||||||
|
- NextJS
|
||||||
- ReactJS
|
- ReactJS
|
||||||
- AntD UI framework
|
- Tailwind CSS
|
||||||
- Auth0
|
- Auth0
|
||||||
- Nginx
|
- Nginx
|
||||||
- Docker
|
- 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/)
|
1. Install [Docker](https://docs.docker.com/get-docker/)
|
||||||
2. Register [Auth0](https://auth0.com/) account
|
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`
|
`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"
|
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):
|
5. Run docker container without SSL (Let's Encrypt):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d --name wiretrustee-dashboard \
|
docker run -d --name netbird-dashboard \
|
||||||
--rm -p 80:80 -p 443:443 \
|
--rm -p 80:80 -p 443:443 \
|
||||||
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
|
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
|
||||||
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
|
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
|
||||||
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
||||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
-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):
|
6. Run docker container with SSL (Let's Encrypt):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d --name wiretrustee-dashboard \
|
docker run -d --name netbird-dashboard \
|
||||||
--rm -p 80:80 -p 443:443 \
|
--rm -p 80:80 -p 443:443 \
|
||||||
-e NGINX_SSL_PORT=443 \
|
-e NGINX_SSL_PORT=443 \
|
||||||
-e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> \
|
-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_CLIENT_ID=<SET YOUR CLEITN ID> \
|
||||||
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
||||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
||||||
wiretrustee/dashboard:main
|
netbirdio/dashboard:main
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to run local development
|
## 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`
|
1. Install [Node](https://nodejs.org/)
|
||||||
3. run `npm install`
|
2. Create and update the `.local-config.json` file. This file should contain values to be replaced from `config.json`
|
||||||
4. run `npm run start dev`
|
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
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@
|
|||||||
"authClientSecret": "$AUTH_CLIENT_SECRET",
|
"authClientSecret": "$AUTH_CLIENT_SECRET",
|
||||||
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
|
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
|
||||||
"authAudience": "$AUTH_AUDIENCE",
|
"authAudience": "$AUTH_AUDIENCE",
|
||||||
|
|
||||||
"apiOrigin": "$NETBIRD_MGMT_API_ENDPOINT",
|
"apiOrigin": "$NETBIRD_MGMT_API_ENDPOINT",
|
||||||
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
|
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
|
||||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
|
||||||
"redirectURI": "$AUTH_REDIRECT_URI",
|
"redirectURI": "$AUTH_REDIRECT_URI",
|
||||||
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
|
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
|
||||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
|
"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
@@ -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
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
5
cypress/fixtures/example.json
Normal 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"
|
||||||
|
}
|
||||||
37
cypress/support/commands.ts
Normal 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
@@ -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
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["es5", "dom"],
|
||||||
|
"baseUrl": "http://localhost:3000",
|
||||||
|
"types": ["cypress", "node"],
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
@@ -21,4 +21,4 @@ RUN chmod +x /usr/local/init_react_envs.sh
|
|||||||
# configure supervisor
|
# configure supervisor
|
||||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||||
# copy build files
|
# copy build files
|
||||||
COPY build/ /usr/share/nginx/html/
|
COPY out/ /usr/share/nginx/html/
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Wiretrustee Dashboard
|
# NetBird Dashboard
|
||||||
Wiretrustee Dashboard is a the Wiretrustee Management server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
|
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
|
## Tags
|
||||||
```latest``` ```vX.X.X``` not available yet.
|
```latest``` ```vX.X.X``` not available yet.
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ Using SSL certificate from Let's Encrypt®:
|
|||||||
docker run -d --rm -p 80:80 -p 443:443 \
|
docker run -d --rm -p 80:80 -p 443:443 \
|
||||||
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
|
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
|
||||||
-e LETSENCRYPT_EMAIL=hello@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.
|
> 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
|
## Environment variables
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri /index.html;
|
try_files $uri $uri.html $uri/ =404;
|
||||||
}
|
}
|
||||||
# You may need this to prevent return 404 recursion.
|
|
||||||
location = /404.html {
|
|
||||||
internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
location = /404.html {
|
||||||
|
internal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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_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_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||||
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
|
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_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||||
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
|
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
|
||||||
|
|
||||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||||
|
|
||||||
# replace ENVs in the config
|
# 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"
|
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"
|
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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
return 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
|
|
||||||
@@ -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 control 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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,125 +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 iosTab = this.page.getByTestId('add-peer-modal-ios-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 openIOSTab() {
|
|
||||||
await test.step('Open iOS tab on add peer modal', async () => {
|
|
||||||
await this.iosTab.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 assertiOSDownloadButtonHasCorrectLink() {
|
|
||||||
await test.step('Assert iOS download button has a correct link', async () => {
|
|
||||||
await expect(this.androidDownloadButton).toHaveAttribute('href', 'https://apps.apple.com/app/netbird-p2p-vpn/id6469329339');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
@@ -1,54 +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 iOS tab on a first access add peer modal', async () => {
|
|
||||||
await addPeerModal.openIOSTab();
|
|
||||||
await addPeerModal.assertiOSDownloadButtonHasCorrectLink();
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 44 KiB |
BIN
media/auth.png
|
Before Width: | Height: | Size: 37 KiB |
BIN
media/peers.png
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 158 KiB |
9
next.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: "export",
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
30427
package-lock.json
generated
143
package.json
@@ -1,85 +1,78 @@
|
|||||||
{
|
{
|
||||||
"name": "wiretrustee-dashboard",
|
"name": "netbird-dashboard",
|
||||||
"version": "0.1.0",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"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": {
|
"scripts": {
|
||||||
"copy": "copyfiles -f ./node_modules/@axa-fr/react-oidc/dist/OidcServiceWorker.js ./public",
|
"copy": "copyfiles -f ./node_modules/@axa-fr/react-oidc/dist/OidcServiceWorker.js ./public",
|
||||||
"copytrusted": "copyfiles -f ./public/local/OidcTrustedDomains.js ./public",
|
"copytrusted": "copyfiles -f ./public/local/OidcTrustedDomains.js ./public",
|
||||||
"start": "react-scripts start",
|
"dev": "next dev -p 3000",
|
||||||
"build": "react-scripts build",
|
"turbo": "next dev -p 3000 --turbo",
|
||||||
"test": "react-scripts test",
|
"build": "next build",
|
||||||
"eject": "react-scripts eject"
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"cypress:open": "cypress open"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"dependencies": {
|
||||||
"extends": [
|
"@axa-fr/react-oidc": "^5.14.0",
|
||||||
"react-app",
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"react-app/jest"
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
]
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
},
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"browserslist": {
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"production": [
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
">0.2%",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"not dead",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"not op_mini all"
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
],
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"development": [
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"last 1 chrome version",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"last 1 firefox version",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"last 1 safari version"
|
"@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": {
|
"devDependencies": {
|
||||||
"@types/react-syntax-highlighter": "^15.5.3",
|
"cypress": "^13.3.3",
|
||||||
"@playwright/test": "^1.36.2"
|
"postcss": "^8",
|
||||||
|
"prettier": "3.0.3",
|
||||||
|
"tailwindcss": "^3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 104 KiB |
@@ -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>
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
18
run-local.sh
@@ -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
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
123
src/App.tsx
@@ -1,123 +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";
|
|
||||||
import {isLocalDev, isNetBirdHosted} from "./utils/common";
|
|
||||||
|
|
||||||
|
|
||||||
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>
|
|
||||||
{(isNetBirdHosted() || isLocalDev()) && <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;
|
|
||||||
8
src/app/(dashboard)/access-control/layout.tsx
Normal 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;
|
||||||
66
src/app/(dashboard)/access-control/page.tsx
Normal 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 Rules`
|
||||||
|
: "Access Control Rules"}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/activity/layout.tsx
Normal 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;
|
||||||
58
src/app/(dashboard)/activity/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/dns/nameservers/layout.tsx
Normal 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;
|
||||||
70
src/app/(dashboard)/dns/nameservers/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/app/(dashboard)/dns/page.tsx
Normal 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"} />;
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/dns/settings/layout.tsx
Normal 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;
|
||||||
131
src/app/(dashboard)/dns/settings/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
8
src/app/(dashboard)/integrations/layout.tsx
Normal 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;
|
||||||
39
src/app/(dashboard)/integrations/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/app/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import DashboardLayout from "@/layouts/DashboardLayout";
|
||||||
|
|
||||||
|
export default DashboardLayout;
|
||||||
8
src/app/(dashboard)/network-routes/layout.tsx
Normal 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;
|
||||||
75
src/app/(dashboard)/network-routes/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/peer/layout.tsx
Normal 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;
|
||||||
454
src/app/(dashboard)/peer/page.tsx
Normal 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"}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/peers/layout.tsx
Normal 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;
|
||||||
61
src/app/(dashboard)/peers/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/settings/layout.tsx
Normal 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;
|
||||||
46
src/app/(dashboard)/settings/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/setup-keys/layout.tsx
Normal 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;
|
||||||
79
src/app/(dashboard)/setup-keys/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/app/(dashboard)/team/page.tsx
Normal 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"} />;
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/team/service-users/layout.tsx
Normal 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;
|
||||||
69
src/app/(dashboard)/team/service-users/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/team/user/layout.tsx
Normal 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;
|
||||||
317
src/app/(dashboard)/team/user/page.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
"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} 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 || "System User")
|
||||||
|
: "#808080",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{user.is_service_user ? (
|
||||||
|
<IconSettings2 size={16} />
|
||||||
|
) : (
|
||||||
|
user?.name?.charAt(0)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h1 className={"flex items-center gap-3"}>{user.name}</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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Card.List>
|
||||||
|
<Card.ListItem
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<User2 size={16} />
|
||||||
|
Name
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
value={user.name}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!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={
|
||||||
|
dayjs(user.last_login).format("D MMMM, YYYY [at] h:mm A") +
|
||||||
|
" (" +
|
||||||
|
dayjs().to(user.last_login) +
|
||||||
|
")"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card.List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/(dashboard)/team/users/layout.tsx
Normal 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;
|
||||||
62
src/app/(dashboard)/team/users/page.tsx
Normal 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
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
67
src/app/globals.css
Normal 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%;
|
||||||
|
}
|
||||||
8
src/app/install/layout.tsx
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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 />;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 454 KiB |
BIN
src/assets/avatars/009.jpg
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
src/assets/avatars/030.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
src/assets/avatars/063.jpg
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
src/assets/avatars/086.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
@@ -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 |
20
src/assets/countries/CountryEURounded.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
20
src/assets/countries/CountryJPRounded.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
20
src/assets/countries/CountryUSRounded.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
src/assets/countries/eu.svg
Normal 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 |
4
src/assets/countries/jp.svg
Normal 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 |
1
src/assets/countries/us.svg
Normal 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 |
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="39" height="6" viewBox="0 0 39 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0.451731 2.66267C0.315048 2.79935 0.315048 3.02096 0.451731 3.15764L2.67912 5.38503C2.8158 5.52171 3.03741 5.52171 3.17409 5.38503C3.31078 5.24835 3.31078 5.02674 3.17409 4.89006L1.19419 2.91016L3.17409 0.930257C3.31078 0.793574 3.31078 0.571966 3.17409 0.435282C3.03741 0.298599 2.8158 0.298599 2.67912 0.435282L0.451731 2.66267ZM38.3807 3.15764C38.5174 3.02096 38.5174 2.79935 38.3807 2.66267L36.1533 0.435282C36.0166 0.298599 35.795 0.298599 35.6583 0.435282C35.5216 0.571966 35.5216 0.793574 35.6583 0.930257L37.6382 2.91016L35.6583 4.89006C35.5216 5.02674 35.5216 5.24835 35.6583 5.38503C35.795 5.52171 36.0166 5.52171 36.1533 5.38503L38.3807 3.15764ZM0.699219 3.26016H38.1332V2.56016H0.699219V3.26016Z" fill="#03543F"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 837 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="29.497" height="7.636" viewBox="0 0 29.497 7.636">
|
|
||||||
<path id="direct_out" d="M4.728,8l.656-.656-2.7-2.693H30.407V3.713H2.683l2.7-2.7L4.728.364.91,4.182Z" transform="translate(-0.91 -0.364)" fill="#03543f"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 262 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="39" height="6" viewBox="0 0 39 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M38.3807 3.15764C38.5174 3.02096 38.5174 2.79935 38.3807 2.66267L36.1533 0.435282C36.0166 0.298599 35.795 0.298599 35.6583 0.435282C35.5216 0.571966 35.5216 0.793574 35.6583 0.930257L37.6382 2.91016L35.6583 4.89006C35.5216 5.02674 35.5216 5.24835 35.6583 5.38503C35.795 5.52171 36.0166 5.52171 36.1533 5.38503L38.3807 3.15764ZM0.699219 3.26016H38.1332V2.56016H0.699219V3.26016Z" fill="#1E429F"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 506 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M38.3846 3.68889C38.5213 3.55221 38.5213 3.3306 38.3846 3.19392L36.1572 0.966532C36.0205 0.829849 35.7989 0.829849 35.6622 0.966532C35.5255 1.10322 35.5255 1.32482 35.6622 1.46151L37.6421 3.44141L35.6622 5.42131C35.5255 5.55799 35.5255 5.7796 35.6622 5.91628C35.7989 6.05296 36.0205 6.05296 36.1572 5.91628L38.3846 3.68889ZM0.703125 3.79141H38.1371V3.09141H0.703125V3.79141Z" fill="#D9D9D9"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 503 B |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 46 KiB |