From 1e6e237aa3e83fccefb3b8ea79fa391b7c2194d2 Mon Sep 17 00:00:00 2001
From: dec0dOS
Date: Sun, 21 Mar 2021 22:25:13 +0300
Subject: [PATCH] refactor: squash commits
---
.dockerignore | 15 +
.github/ISSUE_TEMPLATE/01_BUG_REPORT.md | 34 +
.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md | 29 +
.github/ISSUE_TEMPLATE/03_SUPPORT_QUESTION.md | 9 +
.github/ISSUE_TEMPLATE/config.yml | 1 +
.gitignore | 164 +
.husky/.gitignore | 1 +
.husky/commit-msg | 4 +
.prettierrc.json | 6 +
CHANGELOG.md | 5 +
README.md | 242 +
backend/.gitignore | 41 +
backend/app.js | 75 +
backend/bin/www | 87 +
backend/jsconfig.json | 6 +
backend/package.json | 21 +
backend/routes/auth.js | 22 +
backend/routes/controller.js | 13 +
backend/routes/member.js | 85 +
backend/routes/network.js | 90 +
backend/services/auth.js | 33 +
backend/services/member.js | 172 +
backend/services/network.js | 94 +
backend/utils/constants.js | 56 +
backend/utils/controller-api.js | 19 +
backend/utils/db.js | 8 +
backend/utils/init-admin.js | 16 +
backend/utils/zt-address.js | 6 +
backend/yarn.lock | 554 +
commitlint.config.js | 1 +
docker-compose.yml | 52 +
docker/zero-ui/Dockerfile | 32 +
docker/zerotier/Dockerfile | 9 +
docs/SCREENSHOTS.md | 7 +
docs/SECURITY.md | 17 +
docs/images/homepage.png | Bin 0 -> 166439 bytes
docs/images/logo.png | Bin 0 -> 3598 bytes
docs/images/network.png | Bin 0 -> 854513 bytes
frontend/.gitignore | 21 +
frontend/jsconfig.json | 10 +
frontend/package.json | 48 +
frontend/public/favicon.ico | Bin 0 -> 15406 bytes
frontend/public/index.html | 19 +
frontend/public/manifest.json | 13 +
frontend/public/robots.txt | 3 +
frontend/src/App.jsx | 28 +
frontend/src/components/Bar/Bar.jsx | 144 +
frontend/src/components/Bar/assets/logo.png | Bin 0 -> 3598 bytes
frontend/src/components/Bar/index.jsx | 1 +
.../components/HomeLoggedIn/HomeLoggedIn.jsx | 71 +
.../HomeLoggedIn/HomeLoggedIn.styles.jsx | 16 +
.../NetworkButton/NetworkButton.css | 20 +
.../NetworkButton/NetworkButton.jsx | 35 +
.../NetworkButton/NetworkButton.styles.jsx | 27 +
.../components/NetworkButton/index.jsx | 1 +
.../src/components/HomeLoggedIn/index.jsx | 1 +
.../HomeLoggedOut/HomeLoggedOut.jsx | 31 +
.../src/components/HomeLoggedOut/index.jsx | 1 +
frontend/src/components/LogIn/LogIn.jsx | 20 +
.../components/LogInToken/LogInToken.jsx | 90 +
.../LogIn/components/LogInToken/index.jsx | 1 +
.../LogIn/components/LogInUser/LogInUser.jsx | 123 +
.../LogIn/components/LogInUser/index.jsx | 1 +
frontend/src/components/LogIn/index.jsx | 1 +
.../NetworkHeader/NetworkHeader.jsx | 17 +
.../src/components/NetworkHeader/index.jsx | 1 +
.../NetworkManagment/NetworkManagment.jsx | 80 +
.../src/components/NetworkManagment/index.jsx | 1 +
.../NetworkMembers/NetworkMembers.jsx | 185 +
.../components/AddMember/AddMember.jsx | 55 +
.../components/AddMember/index.jsx | 1 +
.../components/DeleteMember/DeleteMember.jsx | 59 +
.../components/DeleteMember/index.jsx | 1 +
.../components/ManagedIP/ManagedIP.jsx | 76 +
.../components/ManagedIP/index.jsx | 1 +
.../components/MemberName/MemberName.jsx | 28 +
.../components/MemberName/index.jsx | 1 +
.../MemberSettings/MemberSettings.jsx | 64 +
.../components/MemberSettings/index.jsx | 1 +
.../src/components/NetworkMembers/index.jsx | 1 +
.../components/NetworkRules/NetworkRules.jsx | 132 +
.../src/components/NetworkRules/index.jsx | 1 +
.../NetworkSettings/NetworkSettings.jsx | 141 +
.../IPv4AutoAssign/IPv4AutoAssign.jsx | 177 +
.../components/IPv4AutoAssign/index.jsx | 1 +
.../ManagedRoutes/ManagedRoutes.jsx | 131 +
.../components/ManagedRoutes/index.jsx | 1 +
.../src/components/NetworkSettings/index.jsx | 1 +
frontend/src/components/Theme/Theme.jsx | 21 +
frontend/src/components/Theme/index.jsx | 1 +
frontend/src/external/RuleCompiler.js | 1147 ++
frontend/src/index.css | 9 +
frontend/src/index.jsx | 13 +
frontend/src/routes/Home/Home.jsx | 16 +
frontend/src/routes/Home/index.jsx | 1 +
frontend/src/routes/Network/Network.jsx | 83 +
.../src/routes/Network/Network.styles.jsx | 12 +
frontend/src/routes/Network/index.jsx | 1 +
frontend/src/routes/NotFound/NotFound.jsx | 28 +
frontend/src/routes/NotFound/index.jsx | 1 +
frontend/src/utils/API.js | 11 +
frontend/src/utils/ChangeHelper.js | 52 +
frontend/src/utils/IP.js | 50 +
frontend/src/utils/NetworkConfig.js | 136 +
frontend/yarn.lock | 12045 ++++++++++++++++
package.json | 26 +
yarn.lock | 2515 ++++
107 files changed, 20077 insertions(+)
create mode 100644 .dockerignore
create mode 100644 .github/ISSUE_TEMPLATE/01_BUG_REPORT.md
create mode 100644 .github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md
create mode 100644 .github/ISSUE_TEMPLATE/03_SUPPORT_QUESTION.md
create mode 100644 .github/ISSUE_TEMPLATE/config.yml
create mode 100644 .gitignore
create mode 100644 .husky/.gitignore
create mode 100755 .husky/commit-msg
create mode 100644 .prettierrc.json
create mode 100644 CHANGELOG.md
create mode 100755 README.md
create mode 100644 backend/.gitignore
create mode 100644 backend/app.js
create mode 100755 backend/bin/www
create mode 100644 backend/jsconfig.json
create mode 100644 backend/package.json
create mode 100644 backend/routes/auth.js
create mode 100644 backend/routes/controller.js
create mode 100644 backend/routes/member.js
create mode 100644 backend/routes/network.js
create mode 100644 backend/services/auth.js
create mode 100644 backend/services/member.js
create mode 100644 backend/services/network.js
create mode 100644 backend/utils/constants.js
create mode 100644 backend/utils/controller-api.js
create mode 100644 backend/utils/db.js
create mode 100644 backend/utils/init-admin.js
create mode 100644 backend/utils/zt-address.js
create mode 100644 backend/yarn.lock
create mode 100644 commitlint.config.js
create mode 100644 docker-compose.yml
create mode 100644 docker/zero-ui/Dockerfile
create mode 100644 docker/zerotier/Dockerfile
create mode 100644 docs/SCREENSHOTS.md
create mode 100644 docs/SECURITY.md
create mode 100644 docs/images/homepage.png
create mode 100644 docs/images/logo.png
create mode 100644 docs/images/network.png
create mode 100644 frontend/.gitignore
create mode 100644 frontend/jsconfig.json
create mode 100644 frontend/package.json
create mode 100755 frontend/public/favicon.ico
create mode 100644 frontend/public/index.html
create mode 100644 frontend/public/manifest.json
create mode 100644 frontend/public/robots.txt
create mode 100644 frontend/src/App.jsx
create mode 100644 frontend/src/components/Bar/Bar.jsx
create mode 100644 frontend/src/components/Bar/assets/logo.png
create mode 100644 frontend/src/components/Bar/index.jsx
create mode 100644 frontend/src/components/HomeLoggedIn/HomeLoggedIn.jsx
create mode 100644 frontend/src/components/HomeLoggedIn/HomeLoggedIn.styles.jsx
create mode 100644 frontend/src/components/HomeLoggedIn/components/NetworkButton/NetworkButton.css
create mode 100644 frontend/src/components/HomeLoggedIn/components/NetworkButton/NetworkButton.jsx
create mode 100644 frontend/src/components/HomeLoggedIn/components/NetworkButton/NetworkButton.styles.jsx
create mode 100644 frontend/src/components/HomeLoggedIn/components/NetworkButton/index.jsx
create mode 100644 frontend/src/components/HomeLoggedIn/index.jsx
create mode 100644 frontend/src/components/HomeLoggedOut/HomeLoggedOut.jsx
create mode 100644 frontend/src/components/HomeLoggedOut/index.jsx
create mode 100644 frontend/src/components/LogIn/LogIn.jsx
create mode 100644 frontend/src/components/LogIn/components/LogInToken/LogInToken.jsx
create mode 100644 frontend/src/components/LogIn/components/LogInToken/index.jsx
create mode 100644 frontend/src/components/LogIn/components/LogInUser/LogInUser.jsx
create mode 100644 frontend/src/components/LogIn/components/LogInUser/index.jsx
create mode 100644 frontend/src/components/LogIn/index.jsx
create mode 100644 frontend/src/components/NetworkHeader/NetworkHeader.jsx
create mode 100644 frontend/src/components/NetworkHeader/index.jsx
create mode 100644 frontend/src/components/NetworkManagment/NetworkManagment.jsx
create mode 100644 frontend/src/components/NetworkManagment/index.jsx
create mode 100644 frontend/src/components/NetworkMembers/NetworkMembers.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/AddMember/AddMember.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/AddMember/index.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/DeleteMember/DeleteMember.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/DeleteMember/index.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/ManagedIP/ManagedIP.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/ManagedIP/index.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/MemberName/MemberName.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/MemberName/index.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/MemberSettings/MemberSettings.jsx
create mode 100644 frontend/src/components/NetworkMembers/components/MemberSettings/index.jsx
create mode 100644 frontend/src/components/NetworkMembers/index.jsx
create mode 100644 frontend/src/components/NetworkRules/NetworkRules.jsx
create mode 100644 frontend/src/components/NetworkRules/index.jsx
create mode 100644 frontend/src/components/NetworkSettings/NetworkSettings.jsx
create mode 100644 frontend/src/components/NetworkSettings/components/IPv4AutoAssign/IPv4AutoAssign.jsx
create mode 100644 frontend/src/components/NetworkSettings/components/IPv4AutoAssign/index.jsx
create mode 100644 frontend/src/components/NetworkSettings/components/ManagedRoutes/ManagedRoutes.jsx
create mode 100644 frontend/src/components/NetworkSettings/components/ManagedRoutes/index.jsx
create mode 100644 frontend/src/components/NetworkSettings/index.jsx
create mode 100644 frontend/src/components/Theme/Theme.jsx
create mode 100644 frontend/src/components/Theme/index.jsx
create mode 100644 frontend/src/external/RuleCompiler.js
create mode 100644 frontend/src/index.css
create mode 100644 frontend/src/index.jsx
create mode 100644 frontend/src/routes/Home/Home.jsx
create mode 100644 frontend/src/routes/Home/index.jsx
create mode 100644 frontend/src/routes/Network/Network.jsx
create mode 100644 frontend/src/routes/Network/Network.styles.jsx
create mode 100644 frontend/src/routes/Network/index.jsx
create mode 100644 frontend/src/routes/NotFound/NotFound.jsx
create mode 100644 frontend/src/routes/NotFound/index.jsx
create mode 100644 frontend/src/utils/API.js
create mode 100644 frontend/src/utils/ChangeHelper.js
create mode 100644 frontend/src/utils/IP.js
create mode 100644 frontend/src/utils/NetworkConfig.js
create mode 100644 frontend/yarn.lock
create mode 100644 package.json
create mode 100644 yarn.lock
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..872a4be
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,15 @@
+.git
+*Dockerfile*
+*docker-compose*
+node_modules
+jsconfig.js
+.DS_Store
+tmp
+temp
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+backend/data
diff --git a/.github/ISSUE_TEMPLATE/01_BUG_REPORT.md b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.md
new file mode 100644
index 0000000..f926100
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.md
@@ -0,0 +1,34 @@
+---
+name: Bug Report
+about: Create a report to help ZeroUI to improve
+title: 'bug: '
+labels: ''
+assignees: ''
+---
+
+
+
+# Bug Report
+
+**ZeroUI version:**
+
+latest
+
+**Current behavior:**
+
+
+**Expected behavior:**
+
+
+**Steps to reproduce:**
+
+
+**Related code:**
+
+
+```
+insert short code snippets here
+```
+
+**Other information:**
+
diff --git a/.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md
new file mode 100644
index 0000000..6e7c0e4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md
@@ -0,0 +1,29 @@
+---
+name: Feature Request
+about: Suggest an idea for this project
+title: 'feat: '
+labels: ''
+assignees: ''
+---
+
+
+
+# Feature Request
+
+**Describe the Feature Request**
+
+
+**Describe Preferred Solution**
+
+
+**Describe Alternatives**
+
+
+**Related Code**
+
+
+**Additional Context**
+
+
+**If the feature request is approved, would you be willing to submit a PR?**
+Yes / No _(Help can be provided if you need assistance submitting a PR)_
diff --git a/.github/ISSUE_TEMPLATE/03_SUPPORT_QUESTION.md b/.github/ISSUE_TEMPLATE/03_SUPPORT_QUESTION.md
new file mode 100644
index 0000000..60261c2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/03_SUPPORT_QUESTION.md
@@ -0,0 +1,9 @@
+---
+name: Support Question
+about: Question on how to use this project
+title: 'support: '
+labels: ''
+assignees: ''
+---
+
+# Support Question
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3ba13e0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0a35440
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,164 @@
+# Misc
+tmp
+temp
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Created by https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node
+# Edit at https://www.toptal.com/developers/gitignore?templates=vscode,yarn,react,node
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+.env*.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+### react ###
+.DS_*
+**/*.backup.*
+**/*.back.*
+
+node_modules
+
+*.sublime*
+
+psd
+thumb
+sketch
+
+### vscode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+### yarn ###
+# https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored
+
+.yarn/*
+!.yarn/releases
+!.yarn/plugins
+!.yarn/sdks
+!.yarn/versions
+
+# if you are NOT using Zero-installs, then:
+# comment the following lines
+!.yarn/cache
+
+# and uncomment the following lines
+# .pnp.*
+
+# End of https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node
\ No newline at end of file
diff --git a/.husky/.gitignore b/.husky/.gitignore
new file mode 100644
index 0000000..31354ec
--- /dev/null
+++ b/.husky/.gitignore
@@ -0,0 +1 @@
+_
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100755
index 0000000..babe8fa
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+yarn commitlint --edit
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..f0eb61e
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": false
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ca00810
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+## 1.0.0 (2021-03-21)
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..a157854
--- /dev/null
+++ b/README.md
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+ ZeroUI - ZeroTier Controller Web UI - is a web user interface for a self-hosted ZeroTier network controller.
+
+ Explore the docs »
+
+
+ Report Bug
+ ·
+ Request Feature
+
+
+
+Table of Contents
+
+- [About](#about)
+ - [Built With](#built-with)
+- [Getting Started](#getting-started)
+ - [Prerequisites](#prerequisites)
+ - [Installation](#installation)
+- [Usage](#usage)
+ - [Update](#update)
+ - [Backup](#backup)
+- [Roadmap](#roadmap)
+- [Contributing](#contributing)
+ - [Development environment](#development-environment)
+- [Support](#support)
+- [Security](#security)
+- [Copyright notice](#copyright-notice)
+- [License](#license)
+
+---
+
+
+## About
+
+
+
+
+
+This project is highly inspired by [ztncui](https://github.com/key-networks/ztncui) and was developed to address the current limitations of applying the self-hosted [network controllers](https://github.com/zerotier/ZeroTierOne/tree/master/controller). Some [ztncui](https://github.com/key-networks/ztncui) problems cannot be fixed because of the core architecture of the project. ZeroUI tries to solve them and implements the following features:
+* Full React-powered lightweight [SPA](https://en.wikipedia.org/wiki/Single-page_application) that brings better user experience, and ZeroUI is mobile-friendly.
+* ZeroUI has ZeroTier Central complitible API. That means you could use CLI tools and custom applications made only for ZeroTier Central to manage your networks.
+* ZeroUI implements controller-specific workarounds that address some existing [issues](https://github.com/zerotier/ZeroTierOne/issues/859)
+* ZeroUI is more feature complete. ZeroUI has almost all network-controller supported features like rule editor. The development process hasn't stopped, so you will enjoy new features and bug fixes in the near future.
+* ZeroUI deployment is simple. Please refer to [installation](#installation) for more info.
+
+
+
+Wait, I haven't heard about ZeroTier yet...
+
+
+[ZeroTier](https://www.zerotier.com) is awesome [open source project](https://github.com/zerotier/ZeroTierOne) that is avalible on wide range of [platforms](https://www.zerotier.com/download/).
+Most of your hard networking problems could be solved with ZeroTier. It could replace all your complex VPN setups. You can place all your devices on a virtual LAN and manage it easily.
+
+To sum up, ZeroTier combines the capabilities of VPN and SD-WAN, simplifying network management.
+
+
+ |
+
+
+
+
+### Built With
+
+Frontend:
+- [React](https://reactjs.org)
+- [Material UI](https://material-ui.com)
+
+Backend:
+- [NodeJS](https://nodejs.org)
+- [Express](https://expressjs.com)
+- [Lowdb](https://github.com/typicode/lowdb)
+
+Deploy:
+- [Docker](https://www.docker.com)
+- [Docker Compose](https://docs.docker.com/compose/)
+- [Caddy](https://caddyserver.com)
+
+
+## Getting Started
+
+### Prerequisites
+
+The recommended method to install ZeroUI is by using Docker and Docker Compose.
+To install [Docker](https://docs.docker.com/get-docker) and [Docker Compose](https://docs.docker.com/compose/install) on your system, please follow the installation guide from the [official Docker documentation](https://docs.docker.com/get-docker).
+
+For HTTPS setup you will need a domain name.
+
+### Installation
+
+The most simple one-minute installation. Great for the fresh VPS setup.
+
+1. Download the `docker-compose.yml` file
+ ```sh
+ wget https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml
+ ```
+2. Replace `example.com` with your domain name in `docker-compose.yml`
+3. Pull the images
+ ```sh
+ docker-compose pull
+ ```
+4. Run the containers
+ ```sh
+ docker-compose up -d --no-build
+ ```
+5. Check if everything is okay
+ ```sh
+ docker-compose logs
+ ```
+6. Disable your firewall for the following ports: `80/tcp`, `443/tcp` and `9993/udp`
+ * on ubuntu/debian with ufw installed:
+ ```sh
+ ufw allow 80/tcp
+ ufw allow 443/tcp
+ ufw allow 9993/udp
+ ```
+ * or you may use the old good iptables:
+ ```sh
+ iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT
+ iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT
+ iptables -I INPUT 6 -m state --state NEW -p udp --dport 9993 -j ACCEPT
+ ```
+7. Navigate to `https://YOURDOMAIN.com/app/`.
+Now you could use your ZeroUI instance with HTTPS support and automated certificate renewal.
+
+> To disable HTTPS, please remove https-proxy from `docker-compose.yml`, set `ZU_SECURE_HEADERS` to `false` and change zero-ui port `expose` to `ports`.
+
+Advanced manual setups are also supported. Check the following environment variables as a reference:
+| Name | Default value | Description |
+| ---------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
+| NODE_ENV | unset | You could learn more [here](https://nodejs.dev/learn/nodejs-the-difference-between-development-and-production) |
+| ZU_SERVE_FRONTEND | true | You could disable frontend serving and use ZeroUI instance as REST API for your ZeroTier controller |
+| ZU_SECURE_HEADERS | true | Enables [helmet](https://helmetjs.github.io) |
+| ZU_CONTROLLER_ENDPOINT | http://localhost:9993/ | ZeroTier controller API endpoint |
+| ZU_CONTROLLER_TOKEN | from /var/lib/zerotier-one/authtoken.secret | ZeroTier controller API token |
+| ZU_DEFAULT_USERNAME | unset (docker-compose.yml: admin) | Default username that will be set on the first run |
+| ZU_DEFAULT_PASSWORD | unset (docker-compose.yml: zero-ui) | Default password that will be set on the first run |
+| ZU_DATAPATH | data/db.json | ZeroUI data storage path |
+
+ZeroUI could be deployed as a regular nodejs web application, but it requires ZeroTier controller that is installed with `zerotier-one` package. More info about the network controller you could read [here](https://github.com/zerotier/ZeroTierOne/tree/master/controller)
+
+
+## Usage
+
+After installation, log in with your credentials that are declared with ZU_DEFAULT_USERNAME and ZU_DEFAULT_PASSWORD.
+
+Currently, almost all actions are available through the UI. Refer to the [roadmap](#roadmap) for more information.
+
+_For the screenshots, please refer to the [screenshots](docs/SCREENSHOTS.md)_
+
+### Update
+To get the latest version just run
+
+ docker-compose down && docker-compose pull && docker-compose up -d --no-build
+
+in the folder where `docker-compose.yml` is located. Backup is not required as your data is saved in Docker volumes but recommended.
+
+### Backup
+The easiest way to create your ZeroUI data backup is to use the following commands:
+
+ docker run --rm --volumes-from zu-controller -v $(pwd):/backup ubuntu tar cvf /backup/backup-controller.tar /var/lib/zerotier-one
+ docker run --rm --volumes-from zu-main -v $(pwd):/backup ubuntu tar cvf /backup/backup-ui.tar /app/backend/data
+
+
+## Roadmap
+
+See the [open issues](https://github.com/dec0dOS/zero-ui/issues) for a list of proposed features (and known issues).
+
+
+## Contributing
+
+Contributions are what makes the open-source community such an amazing place to learn, inspire, and create. Any contributions you make will benefit everybody else and are **greatly appreciated**.
+
+1. Fork the project
+2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
+3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
+4. Push to the branch (`git push origin feature/AmazingFeature`)
+5. Open a pull request
+
+ZeroUI uses [conventional commits](https://www.conventionalcommits.org), so please follow the guidelines.
+
+### Development environment
+
+To set up a development environment, please follow these steps:
+
+1. Clone the repo
+ ```sh
+ git clone https://github.com/dec0dOS/zero-ui.git
+ ```
+2. Install packages
+ ```sh
+ yarn installDeps
+ ```
+3. Start the development server
+ ```sh
+ yarn dev
+ ```
+4. Navigate to http://localhost:3000
+
+It is also required to install ZeroTier controller. On Linux installing `zerotier-one` package is enough, other platforms require some tweaking. Firstly you should get the controller token. On macOS, you could find it with the following command:
+
+ sudo cat "/Library/Application Support/ZeroTier/One/authtoken.secret"
+
+After you could start ZeroUI development environment:
+
+ ZU_CONTROLLER_TOKEN=TOKEN_FROM_authtoken.secret yarn dev
+
+
+## Support
+
+Reach out to me at one of the following places:
+
+- Telegram: ***REMOVED***
+- E-Mail: *****REMOVED*****
+
+
+## Security
+
+ZeroUI follows good practices of security, but 100% security can't be granted in software. ZeroUI is provided "as is" without any warranty. Use at your own risk.
+
+For enterprise support, a more reliable and scalable solution, please use ZeroTier Central.
+
+_For more info, please refer to the [security](docs/SECURITY.md)_
+
+
+## Copyright notice
+
+ZeroUI is not affiliated or associated with or endorsed by ZeroTier Central or ZeroTier, Inc.
+
+
+## License
+
+[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)]()
+
+See [LICENSE](LICENSE) for more information.
diff --git a/backend/.gitignore b/backend/.gitignore
new file mode 100644
index 0000000..5eb03b1
--- /dev/null
+++ b/backend/.gitignore
@@ -0,0 +1,41 @@
+# Data
+data
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# Misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
diff --git a/backend/app.js b/backend/app.js
new file mode 100644
index 0000000..f08e96e
--- /dev/null
+++ b/backend/app.js
@@ -0,0 +1,75 @@
+const express = require("express");
+const path = require("path");
+const logger = require("morgan");
+const compression = require("compression");
+const bearerToken = require("express-bearer-token");
+const helmet = require("helmet");
+
+const db = require("./utils/db");
+const initAdmin = require("./utils/init-admin");
+
+const authRoutes = require("./routes/auth");
+const networkRoutes = require("./routes/network");
+const memberRoutes = require("./routes/member");
+const controllerRoutes = require("./routes/controller");
+
+const app = express();
+
+app.use(logger("dev"));
+app.use(express.json());
+app.use(express.urlencoded({ extended: false }));
+app.use(
+ bearerToken({
+ headerKey: "Bearer",
+ })
+);
+
+if (
+ process.env.NODE_ENV === "production" &&
+ process.env.ZU_SECURE_HEADERS !== "false"
+) {
+ app.use(helmet());
+}
+
+if (
+ process.env.NODE_ENV === "production" &&
+ process.env.ZU_SERVE_FRONTEND !== "false"
+) {
+ app.use(compression());
+ app.use(
+ ["/app", "/app/*"],
+ express.static(path.join(__dirname, "..", "frontend", "build"))
+ );
+ app.get(["/app/network/*"], function (req, res) {
+ res.sendFile(path.join(__dirname, "..", "frontend", "build", "index.html"));
+ });
+ app.get("/", function (req, res) {
+ res.redirect("/app");
+ });
+}
+
+initAdmin().then(function (admin) {
+ db.defaults({ users: [admin], networks: {} }).write();
+});
+
+const routerAPI = express.Router();
+const routerController = express.Router();
+
+routerAPI.use("/network", networkRoutes);
+routerAPI.use("/network/:nwid/member", memberRoutes);
+routerController.use("", controllerRoutes);
+
+app.use("/auth", authRoutes);
+app.use("/api", routerAPI); // offical SaaS API compatible
+app.use("/controller", routerController); // other controller-specific routes
+
+// error handlers
+app.get("*", async function (req, res) {
+ res.status(404).json({ error: "404 Not found" });
+});
+app.use(async function (err, req, res) {
+ console.error(err.stack); // TODO: replace with production logger
+ res.status(500).json({ error: "500 Internal server error" });
+});
+
+module.exports = app;
diff --git a/backend/bin/www b/backend/bin/www
new file mode 100755
index 0000000..85f026a
--- /dev/null
+++ b/backend/bin/www
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+require("dotenv").config();
+
+/**
+ * Module dependencies.
+ */
+
+var app = require("../app");
+var debug = require("debug")("zero-ui:server");
+var http = require("http");
+
+/**
+ * Get port from environment and store in Express.
+ */
+
+var port = normalizePort(process.env.PORT || "4000");
+app.set("port", port);
+
+/**
+ * Create HTTP server.
+ */
+
+var server = http.createServer(app);
+
+/**
+ * Listen on provided port, on all network interfaces.
+ */
+
+server.listen(port);
+server.on("error", onError);
+server.on("listening", onListening);
+
+/**
+ * Normalize a port into a number, string, or false.
+ */
+
+function normalizePort(val) {
+ var port = parseInt(val, 10);
+
+ if (isNaN(port)) {
+ // named pipe
+ return val;
+ }
+
+ if (port >= 0) {
+ // port number
+ return port;
+ }
+
+ return false;
+}
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+
+function onError(error) {
+ if (error.syscall !== "listen") {
+ throw error;
+ }
+
+ var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
+
+ // handle specific listen errors with friendly messages
+ switch (error.code) {
+ case "EACCES":
+ console.error(bind + " requires elevated privileges");
+ process.exit(1);
+ break;
+ case "EADDRINUSE":
+ console.error(bind + " is already in use");
+ process.exit(1);
+ break;
+ default:
+ throw error;
+ }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+
+function onListening() {
+ var addr = server.address();
+ var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
+ debug("Listening on " + bind);
+}
diff --git a/backend/jsconfig.json b/backend/jsconfig.json
new file mode 100644
index 0000000..011412e
--- /dev/null
+++ b/backend/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": ["node_modules", "**/node_modules/*"],
+ "typeAcquisition": {
+ "exclude": ["dotenv"]
+ }
+}
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..5ef4ef8
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "zero-ui-backend",
+ "private": true,
+ "scripts": {
+ "start": "node ./bin/www"
+ },
+ "dependencies": {
+ "axios": "^0.21.1",
+ "compression": "^1.7.4",
+ "debug": "~4.3.1",
+ "dotenv": "^8.2.0",
+ "express": "~4.17.1",
+ "express-bearer-token": "^2.4.0",
+ "helmet": "^4.4.1",
+ "lodash": "^4.17.21",
+ "lowdb": "^1.0.0",
+ "morgan": "~1.10.0",
+ "p-debounce": "^3.0.1",
+ "pbkdf2-wrapper": "^1.3.2"
+ }
+}
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
new file mode 100644
index 0000000..cda1e83
--- /dev/null
+++ b/backend/routes/auth.js
@@ -0,0 +1,22 @@
+const express = require("express");
+const router = express.Router();
+
+const auth = require("../services/auth");
+
+router.post("/login", async function (req, res) {
+ if (req.body.username && req.body.password) {
+ auth.authorize(req.body.username, req.body.password, function (err, user) {
+ if (user) {
+ res.send({ token: user["token"] });
+ } else {
+ res.status(401).send({
+ error: err.message,
+ });
+ }
+ });
+ } else {
+ res.status(400).send({ error: "Specify username and password" });
+ }
+});
+
+module.exports = router;
diff --git a/backend/routes/controller.js b/backend/routes/controller.js
new file mode 100644
index 0000000..94b8f38
--- /dev/null
+++ b/backend/routes/controller.js
@@ -0,0 +1,13 @@
+const express = require("express");
+const router = express.Router();
+
+const auth = require("../services/auth");
+const api = require("../utils/controller-api");
+
+router.get("/status", auth.isAuthorized, async function (req, res) {
+ api.get("status").then(function (controllerRes) {
+ res.send(controllerRes.data);
+ });
+});
+
+module.exports = router;
diff --git a/backend/routes/member.js b/backend/routes/member.js
new file mode 100644
index 0000000..d638462
--- /dev/null
+++ b/backend/routes/member.js
@@ -0,0 +1,85 @@
+const express = require("express");
+const router = express.Router({ mergeParams: true });
+
+const auth = require("../services/auth");
+const member = require("../services/member");
+
+const api = require("../utils/controller-api");
+
+// get all members
+router.get("/", auth.isAuthorized, async function (req, res) {
+ const nwid = req.params.nwid;
+ api
+ .get("controller/network/" + nwid + "/member")
+ .then(async function (controllerRes) {
+ const mids = Object.keys(controllerRes.data);
+ const data = await member.getMembersData(nwid, mids);
+ res.send(data);
+ })
+ .catch(function () {
+ res.status(404).send({ error: "Network not found" });
+ });
+});
+
+// get member
+router.get("/:mid", auth.isAuthorized, async function (req, res) {
+ const nwid = req.params.nwid;
+ const mid = req.params.mid;
+ const data = await member.getMembersData(nwid, [mid]);
+ if (data[0]) {
+ res.send(data[0]);
+ } else {
+ res.status(404).send({ error: "Member not found" });
+ }
+});
+
+// update member
+router.post("/:mid", auth.isAuthorized, async function (req, res) {
+ const nwid = req.params.nwid;
+ const mid = req.params.mid;
+ member.updateMemberAdditionalData(nwid, mid, req.body);
+ if (req.body.config) {
+ api
+ .post("controller/network/" + nwid + "/member/" + mid, req.body.config)
+ .then(async function () {
+ const data = await member.getMembersData(nwid, [mid]);
+ res.send(data[0]);
+ })
+ .catch(function (err) {
+ res.status(500).send({ error: err.message });
+ });
+ } else {
+ const data = await member.getMembersData(nwid, [mid]);
+ res.send(data[0]);
+ }
+});
+
+// delete member
+router.delete("/:mid", auth.isAuthorized, async function (req, res) {
+ const nwid = req.params.nwid;
+ const mid = req.params.mid;
+ member.deleteMemberAdditionalData(nwid, mid);
+ api
+ .delete("controller/network/" + nwid + "/member/" + mid)
+ .then(function () {})
+ .catch(function (err) {
+ res.status(500).send({ error: err.message });
+ });
+ // Need this to fix ZT controller bug https://github.com/zerotier/ZeroTierOne/issues/859
+ const defaultConfig = {
+ authorized: false,
+ ipAssignments: [],
+ capabilities: [],
+ tags: [],
+ };
+ api
+ .post("controller/network/" + nwid + "/member/" + mid, defaultConfig)
+ .then(function (controllerRes) {
+ res.status(controllerRes.status).send("");
+ })
+ .catch(function (err) {
+ res.status(500).send({ error: err.message });
+ });
+});
+
+module.exports = router;
diff --git a/backend/routes/network.js b/backend/routes/network.js
new file mode 100644
index 0000000..48d99b3
--- /dev/null
+++ b/backend/routes/network.js
@@ -0,0 +1,90 @@
+const express = require("express");
+const router = express.Router();
+
+const auth = require("../services/auth");
+const network = require("../services/network");
+
+const api = require("../utils/controller-api");
+const constants = require("../utils/constants");
+const getZTAddress = require("../utils/zt-address");
+
+let ZT_ADDRESS = null;
+getZTAddress().then(function (address) {
+ ZT_ADDRESS = address;
+});
+
+// get all networks
+router.get("/", auth.isAuthorized, async function (req, res) {
+ api.get("controller/network").then(async function (controllerRes) {
+ const nwids = controllerRes.data;
+ const data = await network.getNetworksData(nwids);
+ res.send(data);
+ });
+});
+
+// create new network
+router.post("/", auth.isAuthorized, async function (req, res) {
+ let reqData = req.body;
+ if (reqData.config) {
+ const config = reqData.config;
+ delete reqData.config;
+ reqData = config;
+ reqData.rules = JSON.parse(constants.defaultRules);
+ } else {
+ res.status(400).send({ error: "Bad request" });
+ }
+ api
+ .post("controller/network/" + ZT_ADDRESS + "______", reqData)
+ .then(async function (controllerRes) {
+ await network.createNetworkAdditionalData(controllerRes.data);
+ const data = await network.getNetworksData([controllerRes.data.id]);
+ res.send(data[0]);
+ });
+});
+
+// get network
+router.get("/:nwid", auth.isAuthorized, async function (req, res) {
+ const nwid = req.params.nwid;
+ const data = await network.getNetworksData([nwid]);
+ if (data[0]) {
+ res.send(data[0]);
+ } else {
+ res.status(404).send({ error: "Network not found" });
+ }
+});
+
+// update network
+router.post("/:nwid", auth.isAuthorized, async function (req, res) {
+ const nwid = req.params.nwid;
+ network.updateNetworkAdditionalData(nwid, req.body);
+ if (req.body.config) {
+ api
+ .post("controller/network/" + nwid, req.body.config)
+ .then(async function () {
+ const data = await network.getNetworksData([nwid]);
+ res.send(data[0]);
+ })
+ .catch(function (err) {
+ res.status(500).send({ error: err.message });
+ });
+ } else {
+ const data = await network.getNetworksData([nwid]);
+ res.send(data[0]);
+ }
+});
+
+// delete network
+router.delete("/:nwid", auth.isAuthorized, async function (req, res) {
+ const nwid = req.params.nwid;
+ network.deleteNetworkAdditionalData(nwid);
+ api
+ .delete("controller/network/" + nwid)
+ .then(function (controllerRes) {
+ res.status(controllerRes.status).send("");
+ })
+ .catch(function (err) {
+ res.status(500).send({ error: err.message });
+ });
+});
+
+module.exports = router;
diff --git a/backend/services/auth.js b/backend/services/auth.js
new file mode 100644
index 0000000..f1b986c
--- /dev/null
+++ b/backend/services/auth.js
@@ -0,0 +1,33 @@
+const db = require("../utils/db");
+const verifyHash = require("pbkdf2-wrapper/verifyHash");
+
+exports.authorize = authorize;
+async function authorize(username, password, callback) {
+ try {
+ var users = await db.get("users");
+ } catch (err) {
+ throw err;
+ }
+ const user = users.find({ username: username });
+ if (!user.value()) return callback(new Error("Cannot find user"));
+ const verified = await verifyHash(password, user.value()["password_hash"]);
+ if (verified) {
+ return callback(null, user.value());
+ } else {
+ return callback(new Error("Invalid password"));
+ }
+}
+
+exports.isAuthorized = isAuthorized;
+async function isAuthorized(req, res, next) {
+ if (req.token) {
+ const user = await db.get("users").find({ token: req.token }).value();
+ if (user) {
+ next();
+ } else {
+ res.status(403).send({ error: "Invalid token" });
+ }
+ } else {
+ res.status(401).send({ error: "Specify token" });
+ }
+}
diff --git a/backend/services/member.js b/backend/services/member.js
new file mode 100644
index 0000000..0107daf
--- /dev/null
+++ b/backend/services/member.js
@@ -0,0 +1,172 @@
+const _ = require("lodash");
+const axios = require("axios");
+
+const api = require("../utils/controller-api");
+const db = require("../utils/db");
+const getZTAddress = require("../utils/zt-address");
+
+let ZT_ADDRESS = null;
+getZTAddress().then(function (address) {
+ ZT_ADDRESS = address;
+});
+
+async function getPeer(mid) {
+ try {
+ const peer = await api.get("peer/" + mid);
+ return peer.data;
+ } catch (err) {
+ return;
+ }
+}
+
+async function getMemberAdditionalData(data) {
+ const additionalData = db
+ .get("networks")
+ .find({ id: data.nwid })
+ .get("members")
+ .find({ id: data.id })
+ .get("additionalConfig")
+ .value();
+
+ const peer = await getPeer(data.id);
+ let peerData = {};
+ if (peer) {
+ peerData.latency = peer.latency;
+ peerData.online = peer.latency !== -1;
+ peerData.clientVersion = peer.version;
+ if (peer.paths[0]) {
+ peerData.lastOnline = peer.paths[0].lastReceive;
+ peerData.physicalAddress = peer.paths[0].address.split("/")[0];
+ }
+ }
+
+ delete data.lastAuthorizedCredential;
+ delete data.lastAuthorizedCredentialType;
+ delete data.objtype;
+ delete data.remoteTraceLevel;
+ delete data.remoteTraceTarget;
+
+ return {
+ id: data.nwid + "-" + data.id,
+ type: "Member",
+ clock: Math.floor(new Date().getTime() / 1000),
+ networkId: data.nwid,
+ nodeId: data.id,
+ controllerId: ZT_ADDRESS,
+ ...additionalData,
+ ...peerData,
+ config: data,
+ };
+}
+
+async function filterDeleted(nwid, mid) {
+ const member = db
+ .get("networks")
+ .find({ id: nwid })
+ .get("members")
+ .find({ id: mid });
+
+ if (!member.get("deleted").value()) return mid;
+ else return;
+}
+
+exports.getMembersData = getMembersData;
+async function getMembersData(nwid, mids) {
+ const prefix = "/controller/network/" + nwid + "/member/";
+ const filtered = (
+ await Promise.all(mids.map(async (mid) => await filterDeleted(nwid, mid)))
+ ).filter((item) => item !== undefined);
+ const links = filtered.map((mid) => prefix + mid);
+
+ const multipleRes = await axios
+ .all(links.map((l) => api.get(l)))
+ .then(
+ axios.spread(function (...res) {
+ return res;
+ })
+ )
+ .catch(function () {
+ return [];
+ });
+
+ let data = Promise.all(
+ multipleRes.map((el) => {
+ return getMemberAdditionalData(el.data);
+ })
+ );
+
+ return data;
+}
+
+exports.updateMemberAdditionalData = updateMemberAdditionalData;
+async function updateMemberAdditionalData(nwid, mid, data) {
+ if (data.config && data.config.authorized) {
+ db.get("networks")
+ .filter({ id: nwid })
+ .map("members")
+ .first()
+ .filter({ id: mid })
+ .first()
+ .set("deleted", false)
+ .write();
+ }
+
+ let additionalData = {};
+
+ if (data.hasOwnProperty("name")) {
+ additionalData.name = data.name;
+ }
+ if (data.hasOwnProperty("description")) {
+ additionalData.description = data.description;
+ }
+
+ if (additionalData) {
+ const member = db
+ .get("networks")
+ .filter({ id: nwid })
+ .map("members")
+ .first()
+ .filter({ id: mid });
+ if (member.value().length) {
+ member
+ .map("additionalConfig")
+ .map((additionalConfig) => _.assign(additionalConfig, additionalData))
+ .write();
+ } else {
+ additionalData = { name: "", description: "" };
+
+ if (data.hasOwnProperty("name")) {
+ additionalData.name = data.name;
+ }
+ if (data.hasOwnProperty("description")) {
+ additionalData.description = data.description;
+ }
+ db.get("networks")
+ .filter({ id: nwid })
+ .map("members")
+ .first()
+ .push({ id: mid, additionalConfig: additionalData })
+ .write();
+ }
+ }
+}
+
+exports.deleteMemberAdditionalData = deleteMemberAdditionalData;
+async function deleteMemberAdditionalData(nwid, mid) {
+ // ZT controller bug
+ /* db.get("networks")
+ .find({ id: nwid })
+ .get("members")
+ .remove({ id: mid })
+ .write();
+ */
+
+ db.get("networks")
+ .filter({ id: nwid })
+ .map("members")
+ .first()
+ .filter({ id: mid })
+ .first()
+ .set("deleted", true)
+ .write();
+}
diff --git a/backend/services/network.js b/backend/services/network.js
new file mode 100644
index 0000000..879c6c2
--- /dev/null
+++ b/backend/services/network.js
@@ -0,0 +1,94 @@
+const _ = require("lodash");
+const axios = require("axios");
+
+const api = require("../utils/controller-api");
+const db = require("../utils/db");
+const constants = require("../utils/constants");
+
+async function getNetworkAdditionalData(data) {
+ let additionalData = db
+ .get("networks")
+ .find({ id: data.id })
+ .get("additionalConfig");
+
+ if (!additionalData.value()) {
+ createNetworkAdditionalData(data);
+ }
+
+ delete data.rulesSource;
+ delete data.objtype;
+ delete data.revision;
+ delete data.remoteTraceLevel;
+ delete data.remoteTraceTarget;
+
+ return {
+ id: data.id,
+ type: "Network",
+ clock: Math.floor(new Date().getTime() / 1000),
+ ...additionalData.value(),
+ config: data,
+ };
+}
+
+exports.getNetworksData = getNetworksData;
+async function getNetworksData(nwids) {
+ const prefix = "/controller/network/";
+ const links = nwids.map((nwid) => prefix + nwid);
+
+ const multipleRes = await axios
+ .all(links.map((l) => api.get(l)))
+ .then(
+ axios.spread(function (...res) {
+ return res;
+ })
+ )
+ .catch(function () {
+ return [];
+ });
+
+ let data = Promise.all(
+ multipleRes.map((el) => {
+ return getNetworkAdditionalData(el.data);
+ })
+ );
+
+ return data;
+}
+
+exports.createNetworkAdditionalData = createNetworkAdditionalData;
+async function createNetworkAdditionalData(data) {
+ const saveData = {
+ id: data.id,
+ additionalConfig: {
+ description: "",
+ rulesSource: constants.defaultRulesSource,
+ },
+ };
+
+ db.get("networks").push(saveData).write();
+}
+
+exports.updateNetworkAdditionalData = updateNetworkAdditionalData;
+async function updateNetworkAdditionalData(nwid, data) {
+ let additionalData = {};
+
+ if (data.hasOwnProperty("description")) {
+ additionalData.description = data.description;
+ }
+ if (data.hasOwnProperty("rulesSource")) {
+ additionalData.rulesSource = data.rulesSource;
+ }
+
+ if (additionalData) {
+ db.get("networks")
+ .filter({ id: nwid })
+ .map("additionalConfig")
+ .map((additionalConfig) => _.assign(additionalConfig, additionalData))
+ .write();
+ }
+}
+
+exports.deleteNetworkAdditionalData = deleteNetworkAdditionalData;
+async function deleteNetworkAdditionalData(nwid) {
+ db.get("networks").remove({ id: nwid }).write();
+}
diff --git a/backend/utils/constants.js b/backend/utils/constants.js
new file mode 100644
index 0000000..b8b16bf
--- /dev/null
+++ b/backend/utils/constants.js
@@ -0,0 +1,56 @@
+exports.defaultRulesSource = `
+# This is a default rule set that allows IPv4 and IPv6 traffic but otherwise
+# behaves like a standard Ethernet switch.
+
+#
+# Allow only IPv4, IPv4 ARP, and IPv6 Ethernet frames.
+#
+drop
+ not ethertype ipv4
+ and not ethertype arp
+ and not ethertype ipv6
+;
+
+#
+# Uncomment to drop non-ZeroTier issued and managed IP addresses.
+#
+# This prevents IP spoofing but also blocks manual IP management at the OS level and
+# bridging unless special rules to exempt certain hosts or traffic are added before
+# this rule.
+#
+#drop
+# not chr ipauth
+#;
+
+# Accept anything else. This is required since default is 'drop'.
+accept;
+`;
+
+exports.defaultRules = `
+[
+ {
+ "type": "MATCH_ETHERTYPE",
+ "not": true,
+ "or": false,
+ "etherType": 2048
+ },
+ {
+ "type": "MATCH_ETHERTYPE",
+ "not": true,
+ "or": false,
+ "etherType": 2054
+ },
+ {
+ "type": "MATCH_ETHERTYPE",
+ "not": true,
+ "or": false,
+ "etherType": 34525
+ },
+ {
+ "type": "ACTION_DROP"
+ },
+ {
+ "type": "ACTION_ACCEPT"
+ }
+ ]
+`;
diff --git a/backend/utils/controller-api.js b/backend/utils/controller-api.js
new file mode 100644
index 0000000..37f7338
--- /dev/null
+++ b/backend/utils/controller-api.js
@@ -0,0 +1,19 @@
+const axios = require("axios");
+const fs = require("fs");
+
+const baseURL = process.env.ZU_CONTROLLER_ENDPOINT || "http://localhost:9993/";
+
+var token;
+if (process.env.ZU_CONTROLLER_TOKEN) {
+ token = process.env.ZU_CONTROLLER_TOKEN;
+} else {
+ token = fs.readFileSync("/var/lib/zerotier-one/authtoken.secret", "utf8");
+}
+
+module.exports = axios.create({
+ baseURL: baseURL,
+ responseType: "json",
+ headers: {
+ "X-ZT1-Auth": token,
+ },
+});
diff --git a/backend/utils/db.js b/backend/utils/db.js
new file mode 100644
index 0000000..eff70fa
--- /dev/null
+++ b/backend/utils/db.js
@@ -0,0 +1,8 @@
+const low = require("lowdb");
+const FileSync = require("lowdb/adapters/FileSync");
+
+const adapter = new FileSync(process.env.ZU_DATAPATH || "data/db.json");
+
+const db = low(adapter);
+
+module.exports = db;
diff --git a/backend/utils/init-admin.js b/backend/utils/init-admin.js
new file mode 100644
index 0000000..e900534
--- /dev/null
+++ b/backend/utils/init-admin.js
@@ -0,0 +1,16 @@
+const crypto = require("crypto");
+const hashPassword = require("pbkdf2-wrapper/hashText");
+
+module.exports = async function () {
+ if (!process.env.ZU_DEFAULT_PASSWORD || !process.env.ZU_DEFAULT_USERNAME) {
+ console.error("ZU_DEFAULT_PASSWORD or ZU_DEFAULT_USERNAME not found!");
+ process.exit(1);
+ }
+ const username = process.env.ZU_DEFAULT_USERNAME;
+ const hash = await hashPassword(process.env.ZU_DEFAULT_PASSWORD);
+ return {
+ username: username,
+ password_hash: hash,
+ token: crypto.randomBytes(16).toString("hex"),
+ };
+};
diff --git a/backend/utils/zt-address.js b/backend/utils/zt-address.js
new file mode 100644
index 0000000..1a09d57
--- /dev/null
+++ b/backend/utils/zt-address.js
@@ -0,0 +1,6 @@
+const api = require("../utils/controller-api");
+
+module.exports = async function () {
+ const res = await api.get("status");
+ return res.data.address;
+};
diff --git a/backend/yarn.lock b/backend/yarn.lock
new file mode 100644
index 0000000..ec1247e
--- /dev/null
+++ b/backend/yarn.lock
@@ -0,0 +1,554 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+abbott@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/abbott/-/abbott-1.1.3.tgz"
+ integrity sha1-JvOtm7vb/+LFa1sDdU5ZgasOXlw=
+
+accepts@~1.3.5, accepts@~1.3.7:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz"
+ integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
+ dependencies:
+ mime-types "~2.1.24"
+ negotiator "0.6.2"
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz"
+ integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+
+axios@^0.21.1:
+ version "0.21.1"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz"
+ integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
+ dependencies:
+ follow-redirects "^1.10.0"
+
+basic-auth@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz"
+ integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
+ dependencies:
+ safe-buffer "5.1.2"
+
+body-parser@1.19.0:
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz"
+ integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
+ dependencies:
+ bytes "3.1.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.2"
+ http-errors "1.7.2"
+ iconv-lite "0.4.24"
+ on-finished "~2.3.0"
+ qs "6.7.0"
+ raw-body "2.4.0"
+ type-is "~1.6.17"
+
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+ integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
+
+bytes@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz"
+ integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
+compressible@~2.0.16:
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
+ integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
+ dependencies:
+ mime-db ">= 1.43.0 < 2"
+
+compression@^1.7.4:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
+ integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
+ dependencies:
+ accepts "~1.3.5"
+ bytes "3.0.0"
+ compressible "~2.0.16"
+ debug "2.6.9"
+ on-headers "~1.0.2"
+ safe-buffer "5.1.2"
+ vary "~1.1.2"
+
+content-disposition@0.5.3:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz"
+ integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+ dependencies:
+ safe-buffer "5.1.2"
+
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz"
+ integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+cookie-parser@^1.4.4:
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz"
+ integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==
+ dependencies:
+ cookie "0.4.0"
+ cookie-signature "1.0.6"
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz"
+ integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+
+cookie@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz"
+ integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+
+cookie@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz"
+ integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
+
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@~4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz"
+ integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+ dependencies:
+ ms "2.1.2"
+
+depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz"
+ integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+depd@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz"
+ integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz"
+ integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+dotenv@^8.2.0:
+ version "8.2.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz"
+ integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz"
+ integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz"
+ integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz"
+ integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz"
+ integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+express-bearer-token@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/express-bearer-token/-/express-bearer-token-2.4.0.tgz"
+ integrity sha512-2+kRZT2xo+pmmvSY7Ma5FzxTJpO3kGaPCEXPbAm3GaoZ/z6FE4K6L7cvs1AUZwY2xkk15PcQw7t4dWjsl5rdJw==
+ dependencies:
+ cookie "^0.3.1"
+ cookie-parser "^1.4.4"
+
+express@~4.17.1:
+ version "4.17.1"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz"
+ integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
+ dependencies:
+ accepts "~1.3.7"
+ array-flatten "1.1.1"
+ body-parser "1.19.0"
+ content-disposition "0.5.3"
+ content-type "~1.0.4"
+ cookie "0.4.0"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "~1.1.2"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "~1.1.2"
+ fresh "0.5.2"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.5"
+ qs "6.7.0"
+ range-parser "~1.2.1"
+ safe-buffer "5.1.2"
+ send "0.17.1"
+ serve-static "1.14.1"
+ setprototypeof "1.1.1"
+ statuses "~1.5.0"
+ type-is "~1.6.18"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+finalhandler@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz"
+ integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ statuses "~1.5.0"
+ unpipe "~1.0.0"
+
+follow-redirects@^1.10.0:
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz"
+ integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==
+
+forwarded@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz"
+ integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz"
+ integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+graceful-fs@^4.1.3:
+ version "4.2.6"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz"
+ integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
+
+helmet@^4.4.1:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.4.1.tgz#a17e1444d81d7a83ddc6e6f9bc6e2055b994efe7"
+ integrity sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw==
+
+http-errors@1.7.2:
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz"
+ integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.1"
+ statuses ">= 1.5.0 < 2"
+ toidentifier "1.0.0"
+
+http-errors@~1.7.2:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz"
+ integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.4"
+ setprototypeof "1.1.1"
+ statuses ">= 1.5.0 < 2"
+ toidentifier "1.0.0"
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+inherits@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+inherits@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ipaddr.js@1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
+ integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+is-promise@^2.1.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz"
+ integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
+lodash@4:
+ version "4.17.20"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz"
+ integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
+lodash@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+lowdb@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz"
+ integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==
+ dependencies:
+ graceful-fs "^4.1.3"
+ is-promise "^2.1.0"
+ lodash "4"
+ pify "^3.0.0"
+ steno "^0.4.1"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz"
+ integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
+ integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz"
+ integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+
+mime-db@1.45.0:
+ version "1.45.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz"
+ integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
+
+"mime-db@>= 1.43.0 < 2":
+ version "1.46.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
+ integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
+
+mime-types@~2.1.24:
+ version "2.1.28"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz"
+ integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
+ dependencies:
+ mime-db "1.45.0"
+
+mime@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+morgan@~1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz"
+ integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==
+ dependencies:
+ basic-auth "~2.0.1"
+ debug "2.6.9"
+ depd "~2.0.0"
+ on-finished "~2.3.0"
+ on-headers "~1.0.2"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+negotiator@0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz"
+ integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz"
+ integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz"
+ integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+
+p-debounce@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/p-debounce/-/p-debounce-3.0.1.tgz#51c38b03aa09f319ec507f1d8aba831949c8bbf2"
+ integrity sha512-7n7FWY/f4gmVkd6BwC2EZRbTnAmZbL/Zdrc3qbJRnwkb3OUp4HbPlEN1XybpQk0MML6RDDdePMFIr4dOXXfPNw==
+
+parseurl@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz"
+ integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
+ integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+
+pbkdf2-wrapper@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/pbkdf2-wrapper/-/pbkdf2-wrapper-1.3.2.tgz"
+ integrity sha512-McL8NfgXcIsLewiKd8MS4vQO+Q0JuQ7fxEAOIIKs/FJt49fnuJDRG6nkSp0TpXVjRydTllmhALFfPckc3zcA8w==
+ dependencies:
+ righto "^6.1.3"
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+proxy-addr@~2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz"
+ integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
+ dependencies:
+ forwarded "~0.1.2"
+ ipaddr.js "1.9.1"
+
+qs@6.7.0:
+ version "6.7.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz"
+ integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
+
+range-parser@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz"
+ integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz"
+ integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
+ dependencies:
+ bytes "3.1.0"
+ http-errors "1.7.2"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
+righto@^6.1.3:
+ version "6.1.3"
+ resolved "https://registry.yarnpkg.com/righto/-/righto-6.1.3.tgz"
+ integrity sha512-tfnK3e10FjBCKSfVI69vJCzSCsHNaxCK7pdEhnxGM89KxHm4ykxT5B1jq6Xoj12+vK1atUvcKwAIFG84IBrPLw==
+ dependencies:
+ abbott "^1.1.3"
+ setimmediate "^1.0.5"
+
+safe-buffer@5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+send@0.17.1:
+ version "0.17.1"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz"
+ integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
+ dependencies:
+ debug "2.6.9"
+ depd "~1.1.2"
+ destroy "~1.0.4"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "~1.7.2"
+ mime "1.6.0"
+ ms "2.1.1"
+ on-finished "~2.3.0"
+ range-parser "~1.2.1"
+ statuses "~1.5.0"
+
+serve-static@1.14.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz"
+ integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
+ dependencies:
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ parseurl "~1.3.3"
+ send "0.17.1"
+
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz"
+ integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+
+"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz"
+ integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+steno@^0.4.1:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz"
+ integrity sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=
+ dependencies:
+ graceful-fs "^4.1.3"
+
+toidentifier@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz"
+ integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+
+type-is@~1.6.17, type-is@~1.6.18:
+ version "1.6.18"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz"
+ integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.24"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz"
+ integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz"
+ integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz"
+ integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 0000000..5073c20
--- /dev/null
+++ b/commitlint.config.js
@@ -0,0 +1 @@
+module.exports = { extends: ["@commitlint/config-conventional"] };
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..b4f0eae
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,52 @@
+version: "3.9"
+
+services:
+ zerotier:
+ image: dec0dos/zerotier-controller:latest
+ container_name: zu-controller
+ build:
+ context: .
+ dockerfile: ./docker/zerotier/Dockerfile
+ restart: unless-stopped
+ volumes:
+ - controller_data:/var/lib/zerotier-one
+ expose:
+ - "9993/tcp"
+ ports:
+ - "9993:9993/udp"
+ zero-ui:
+ image: dec0dos/zero-ui:latest
+ container_name: zu-main
+ build:
+ context: .
+ dockerfile: ./docker/zero-ui/Dockerfile
+ restart: unless-stopped
+ depends_on:
+ - zerotier
+ volumes:
+ - controller_data:/var/lib/zerotier-one
+ - zero-ui_data:/app/backend/data
+ environment:
+ - ZU_CONTROLLER_ENDPOINT=http://zerotier:9993/
+ - ZU_SECURE_HEADERS=true
+ - ZU_DEFAULT_USERNAME=admin
+ - ZU_DEFAULT_PASSWORD=zero-ui
+ expose:
+ - "4000"
+ https-proxy:
+ image: caddy:latest
+ container_name: zu-https-proxy
+ restart: unless-stopped
+ depends_on:
+ - zero-ui
+ command: caddy reverse-proxy --from example.com --to zero-ui:4000
+ volumes:
+ - caddy_data:/data
+ ports:
+ - "80:80"
+ - "443:443"
+
+volumes:
+ zero-ui_data:
+ controller_data:
+ caddy_data:
diff --git a/docker/zero-ui/Dockerfile b/docker/zero-ui/Dockerfile
new file mode 100644
index 0000000..f8f0f69
--- /dev/null
+++ b/docker/zero-ui/Dockerfile
@@ -0,0 +1,32 @@
+FROM node:current-alpine3.13 as build-stage
+
+ENV INLINE_RUNTIME_CHUNK=false
+ENV GENERATE_SOURCEMAP=false
+
+WORKDIR /app/frontend
+COPY ./frontend/package*.json /app/frontend
+COPY ./frontend/yarn.lock /app/frontend
+RUN yarn install
+
+COPY ./frontend /app/frontend
+RUN yarn build
+
+
+FROM node:current-alpine3.13
+
+WORKDIR /app/frontend/build
+COPY --from=build-stage /app/frontend/build /app/frontend/build/
+
+WORKDIR /app/backend
+COPY ./backend/package*.json /app/backend
+COPY ./backend/yarn.lock /app/backend
+RUN yarn install
+
+COPY ./backend /app/backend
+
+EXPOSE 4000
+ENV NODE_ENV=production
+ENV ZU_SECURE_HEADERS=true
+ENV ZU_SERVE_FRONTEND=true
+
+CMD [ "node", "./bin/www" ]
diff --git a/docker/zerotier/Dockerfile b/docker/zerotier/Dockerfile
new file mode 100644
index 0000000..7bcfc38
--- /dev/null
+++ b/docker/zerotier/Dockerfile
@@ -0,0 +1,9 @@
+FROM alpine:latest
+
+RUN apk add --no-cache zerotier-one
+RUN echo "{\"settings\": {\"portMappingEnabled\": true,\"softwareUpdate\": \"disable\",\"allowManagementFrom\": [\"0.0.0.0/0\"]}}" > /var/lib/zerotier-one/local.conf
+
+EXPOSE 9993/tcp
+EXPOSE 9993/udp
+
+ENTRYPOINT ["zerotier-one"]
diff --git a/docs/SCREENSHOTS.md b/docs/SCREENSHOTS.md
new file mode 100644
index 0000000..21942bd
--- /dev/null
+++ b/docs/SCREENSHOTS.md
@@ -0,0 +1,7 @@
+# Home page
+
+![ZeroUI Home Page](images/homepage.png)
+
+# Network page
+
+![ZeroUI Network Page](images/network.png)
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..e6df6f1
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,17 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If there are any vulnerability in **ZeroUI** project, don't hesitate to _report them_.
+
+1. Use any of the [contact addresses](https://github.com/dec0dOS/zero-ui#support).
+2. Describe the vulnerability.
+
+- If you have a fix, explain or attach it.
+- In the near time, expect a reply with the required steps. Also, there may be a demand for a pull request which include the fixes.
+
+##### You should not disclose the vulnerability publicly if you haven't received an answer in some weeks.
+
+##### If the vulnerability is rejected, you may post it publicly within some hour of rejection, unless the rejection is withdrawn within that time period.
+
+##### After the vulnerability has been fixed, you may disclose the vulnerability details publicly over some days.
diff --git a/docs/images/homepage.png b/docs/images/homepage.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4b54ac88bbb992c89fbec79bb2b4c14f84b4a16
GIT binary patch
literal 166439
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