refactor: squash commits

This commit is contained in:
dec0dOS 2021-03-21 22:25:13 +03:00
parent 63ebcb5915
commit 1e6e237aa3
107 changed files with 20077 additions and 0 deletions

15
.dockerignore Normal file
View file

@ -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

34
.github/ISSUE_TEMPLATE/01_BUG_REPORT.md vendored Normal file
View file

@ -0,0 +1,34 @@
---
name: Bug Report
about: Create a report to help ZeroUI to improve
title: 'bug: '
labels: ''
assignees: ''
---
<!-- ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION. -->
# Bug Report
**ZeroUI version:**
<!-- Please specify commit or tag version. -->
latest
**Current behavior:**
<!-- Describe how the bug manifests. -->
**Expected behavior:**
<!-- Describe what the behavior would be without the bug. -->
**Steps to reproduce:**
<!-- Please explain the steps required to duplicate the issue, especially if you are able to provide a sample application. -->
**Related code:**
<!-- If you are able to illustrate the bug or feature request with an example, please provide it here -->
```
insert short code snippets here
```
**Other information:**
<!-- List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to fix, Stack Overflow links, forum links, etc. -->

View file

@ -0,0 +1,29 @@
---
name: Feature Request
about: Suggest an idea for this project
title: 'feat: '
labels: ''
assignees: ''
---
<!-- ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION. -->
# Feature Request
**Describe the Feature Request**
<!-- A clear and concise description of what the feature request is. Please include if your feature request is related to a problem. -->
**Describe Preferred Solution**
<!-- A clear and concise description of what you want to happen. -->
**Describe Alternatives**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Related Code**
<!-- If you are able to illustrate the bug or feature request with an example, please provide it here -->
**Additional Context**
<!-- List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to add, use case, Stack Overflow links, forum links, screenshots, OS if applicable, etc. -->
**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)_

View file

@ -0,0 +1,9 @@
---
name: Support Question
about: Question on how to use this project
title: 'support: '
labels: ''
assignees: ''
---
# Support Question

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: false

164
.gitignore vendored Normal file
View file

@ -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

1
.husky/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_

4
.husky/commit-msg Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit

6
.prettierrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}

5
CHANGELOG.md Normal file
View file

@ -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)

242
README.md Executable file
View file

@ -0,0 +1,242 @@
<!-- PROJECT LOGO -->
<br />
<p align="center">
<a href="https://github.com/dec0dOS/zero-ui">
<img src="docs/images/logo.png" alt="Logo" width="80" height="80">
</a>
<p align="center">
ZeroUI - ZeroTier Controller Web UI - is a web user interface for a self-hosted ZeroTier network controller.
<br />
<a href="https://github.com/dec0dOS/zero-ui"><strong>Explore the docs »</strong></a>
<br />
<br />
<a href="https://github.com/dec0dOS/zero-ui/issues">Report Bug</a>
·
<a href="https://github.com/dec0dOS/zero-ui/issues">Request Feature</a>
</p>
</p>
<summary><h2 style="display: inline-block">Table of Contents</h2></summary>
- [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
<table>
<tr>
<td>
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.
<details open>
<summary>Wait, I haven't heard about ZeroTier yet...</summary>
<br>
[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.
</details>
</td>
</tr>
</table>
### 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)](<https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3)>)
See [LICENSE](LICENSE) for more information.

41
backend/.gitignore vendored Normal file
View file

@ -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

75
backend/app.js Normal file
View file

@ -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;

87
backend/bin/www Executable file
View file

@ -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);
}

6
backend/jsconfig.json Normal file
View file

@ -0,0 +1,6 @@
{
"exclude": ["node_modules", "**/node_modules/*"],
"typeAcquisition": {
"exclude": ["dotenv"]
}
}

21
backend/package.json Normal file
View file

@ -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"
}
}

22
backend/routes/auth.js Normal file
View file

@ -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;

View file

@ -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;

85
backend/routes/member.js Normal file
View file

@ -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;

90
backend/routes/network.js Normal file
View file

@ -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;

33
backend/services/auth.js Normal file
View file

@ -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" });
}
}

172
backend/services/member.js Normal file
View file

@ -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();
}

View file

@ -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();
}

View file

@ -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"
}
]
`;

View file

@ -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,
},
});

8
backend/utils/db.js Normal file
View file

@ -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;

View file

@ -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"),
};
};

View file

@ -0,0 +1,6 @@
const api = require("../utils/controller-api");
module.exports = async function () {
const res = await api.get("status");
return res.data.address;
};

554
backend/yarn.lock Normal file
View file

@ -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=

1
commitlint.config.js Normal file
View file

@ -0,0 +1 @@
module.exports = { extends: ["@commitlint/config-conventional"] };

52
docker-compose.yml Normal file
View file

@ -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:

32
docker/zero-ui/Dockerfile Normal file
View file

@ -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" ]

View file

@ -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"]

7
docs/SCREENSHOTS.md Normal file
View file

@ -0,0 +1,7 @@
# Home page
![ZeroUI Home Page](images/homepage.png)
# Network page
![ZeroUI Network Page](images/network.png)

17
docs/SECURITY.md Normal file
View file

@ -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.

BIN
docs/images/homepage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
docs/images/network.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

21
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
# Dependencies
node_modules
.pnp
.pnp.js
# Testing
coverage
# Production
build
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

10
frontend/jsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"],
"exclude": ["node_modules", "**/node_modules/*"],
"typeAcquisition": {
"exclude": ["dotenv", "harmony-reflect"]
}
}

48
frontend/package.json Normal file
View file

@ -0,0 +1,48 @@
{
"name": "zero-ui-frontend",
"private": true,
"dependencies": {
"@fontsource/roboto": "^4.2.2",
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"@uiw/react-codemirror": "^3.0.5",
"axios": "^0.21.1",
"history": "^5.0.0",
"ipaddr.js": "^2.0.0",
"lodash": "^4.17.21",
"react": "^17.0.1",
"react-data-table-component": "^6.11.7",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-use": "^17.2.1",
"styled-components": "^5.2.1"
},
"devDependencies": {
"source-map-explorer": "^2.5.2"
},
"scripts": {
"start": "BROWSER=none react-scripts start",
"build": "react-scripts build",
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
"homepage": "/app",
"proxy": "http://localhost:4000",
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
frontend/public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="%PUBLIC_URL%/">
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta
name="description"
content="ZeroUI"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>ZeroUI</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View file

@ -0,0 +1,13 @@
{
"icons": [
{
"src": "favicon.ico",
"sizes": "48x48",
"type": "image/x-icon"
}
],
"start_url": "./app",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

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

28
frontend/src/App.jsx Normal file
View file

@ -0,0 +1,28 @@
import "@fontsource/roboto";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import Theme from "./components/Theme";
import Bar from "./components/Bar";
import Home from "./routes/Home";
import NotFound from "./routes/NotFound";
import Network from "./routes/Network/Network";
function App() {
return (
<Theme>
<BrowserRouter basename="/app">
<Bar />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/network/:nwid" component={Network} />
<Route path="/404" component={NotFound} />
<Redirect to="/404" />
</Switch>
</BrowserRouter>
</Theme>
);
}
export default App;

View file

@ -0,0 +1,144 @@
import logo from "./assets/logo.png";
import { useState } from "react";
import { Link as RouterLink, useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import {
AppBar,
Toolbar,
Typography,
Box,
Button,
Divider,
Menu,
MenuItem,
Link,
} from "@material-ui/core";
import MenuIcon from "@material-ui/icons/Menu";
import LogIn from "components/LogIn";
function Bar() {
const [loggedIn, setLoggedIn] = useLocalStorage("loggedIn", false);
const [anchorEl, setAnchorEl] = useState(null);
const history = useHistory();
const openMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const closeMenu = () => {
setAnchorEl(null);
};
const onLogOutClick = () => {
setLoggedIn(false);
localStorage.clear();
history.push("/");
history.go(0);
};
const menuItems = [
// TODO: add settings page
// {
// name: "Settings",
// to: "/settings",
// },
{
name: "Log out",
divide: true,
onClick: onLogOutClick,
},
];
return (
<AppBar
color="secondary"
style={{ background: "#000000" }}
position="static"
>
<Toolbar>
<Box display="flex" flexGrow={1}>
<Typography color="inherit" variant="h6">
<Link
color="inherit"
component={RouterLink}
to="/"
underline="none"
>
<img src={logo} width="100" height="100" alt="logo" />
</Link>
</Typography>
</Box>
{loggedIn && (
<>
<Button color="inherit" onClick={openMenu}>
<MenuIcon></MenuIcon>
</Button>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeMenu}
>
{menuItems.map((menuItem, index) => {
if (
menuItem.hasOwnProperty("condition") &&
!menuItem.condition
) {
return null;
}
let component = null;
if (menuItem.to) {
component = (
<MenuItem
key={index}
component={RouterLink}
to={menuItem.to}
onClick={closeMenu}
>
{menuItem.name}
</MenuItem>
);
} else {
component = (
<MenuItem
key={index}
onClick={() => {
closeMenu();
menuItem.onClick();
}}
>
{menuItem.name}
</MenuItem>
);
}
if (menuItem.divide) {
return (
<span key={index}>
<Divider />
{component}
</span>
);
}
return component;
})}
</Menu>
</>
)}
{!loggedIn && LogIn()}
</Toolbar>
</AppBar>
);
}
export default Bar;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1 @@
export { default } from "./Bar";

View file

@ -0,0 +1,71 @@
import { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { Divider, Button, Grid, Typography, Box } from "@material-ui/core";
import useStyles from "./HomeLoggedIn.styles";
import NetworkButton from "./components/NetworkButton";
import API from "utils/API";
import { generateNetworkConfig } from "utils/NetworkConfig";
function HomeLoggedIn() {
const [networks, setNetworks] = useState([]);
const classes = useStyles();
const history = useHistory();
const createNetwork = async () => {
const network = await API.post("network", generateNetworkConfig());
console.log(network);
history.push("/network/" + network.data["config"]["id"]);
};
useEffect(() => {
async function fetchData() {
const networks = await API.get("network");
setNetworks(networks.data);
console.log("Networks:", networks.data);
}
fetchData();
}, []);
return (
<div className={classes.root}>
<Button
variant="contained"
color="primary"
className={classes.createBtn}
onClick={createNetwork}
>
Create A Network
</Button>
<Divider />
<Grid container spacing={3} className={classes.container}>
<Grid item xs={6}>
<Typography variant="h5">Controller networks</Typography>
{networks[0] && "Network controller address"}
<Box fontWeight="fontWeightBold">
{networks[0] && networks[0]["id"].slice(0, 10)}
</Box>
</Grid>
<Grid item xs="auto">
<Typography>Networks</Typography>
<Grid item>
{networks[0] ? (
networks.map((network) => (
<Grid key={network["id"]} item>
<NetworkButton network={network} />
</Grid>
))
) : (
<div>Please create at least one network</div>
)}
</Grid>
</Grid>
</Grid>
</div>
);
}
export default HomeLoggedIn;

View file

@ -0,0 +1,16 @@
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
root: {
margin: "5%",
flexGrow: 1,
},
createBtn: {
marginBottom: "1%",
},
container: {
marginTop: "1%",
},
}));
export default useStyles;

View file

@ -0,0 +1,20 @@
.netBtn {
font-size: 1em;
padding: 0 10px;
min-height: 50px;
max-height: 50px;
border-radius: 2px;
border: 1px solid #b5b5b5;
margin: 2px;
}
.netBtn:hover {
transform: translateY(0) scale(1.02);
background: rgba(0,0,0,0);
box-shadow: inset 0 0 0 3px #ffc107;
}
.netBtn:focus {
border: 1px solid white;
outline: 0;
}

View file

@ -0,0 +1,35 @@
import "./NetworkButton.css";
import { Link } from "react-router-dom";
import { List, ListItem, Hidden } from "@material-ui/core";
import useStyles from "./NetworkButton.styles";
import { getCIDRAddress } from "utils/IP";
function NetworkButton({ network }) {
const classes = useStyles();
return (
<div className="netBtn" role="button">
<Link to={"/network/" + network["id"]} className={classes.link}>
<List className={classes.flexContainer}>
<ListItem className={classes.nwid}>{network["id"]}</ListItem>
<ListItem className={classes.name}>
{network["config"]["name"]}
</ListItem>
<Hidden mdDown>
<ListItem className={classes.cidr}>
{network["config"]["ipAssignmentPools"] &&
getCIDRAddress(
network["config"]["ipAssignmentPools"][0]["ipRangeStart"],
network["config"]["ipAssignmentPools"][0]["ipRangeEnd"]
)}
</ListItem>
</Hidden>
</List>
</Link>
</div>
);
}
export default NetworkButton;

View file

@ -0,0 +1,27 @@
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
link: {
textDecoration: "none",
color: "black",
},
flexContainer: {
display: "flex",
flexDirection: "row",
paddingTop: "8px",
},
name: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
},
nwid: {
color: "#007fff",
fontWeight: "bolder",
},
cidr: {
color: "#b5b5b5",
},
}));
export default useStyles;

View file

@ -0,0 +1 @@
export { default } from "./NetworkButton";

View file

@ -0,0 +1 @@
export { default } from "./HomeLoggedIn";

View file

@ -0,0 +1,31 @@
import { Grid, Typography } from "@material-ui/core";
function HomeLoggedOut() {
return (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Grid item xs={10}>
<Typography variant="h5">
<span>
ZeroUI - ZeroTier Controller Web UI - is a web user interface for a
self-hosted ZeroTier network controller.
</span>
</Typography>
<Typography>
<span>Please Log In to continue</span>
</Typography>
</Grid>
</Grid>
);
}
export default HomeLoggedOut;

View file

@ -0,0 +1 @@
export { default } from "./HomeLoggedOut";

View file

@ -0,0 +1,20 @@
import { Divider } from "@material-ui/core";
import LogInUser from "./components/LogInUser";
import LogInToken from "./components/LogInToken";
function LogIn() {
return (
<>
{process.env.NODE_ENV === "development" && (
<>
<LogInToken />
<Divider orientation="vertical" />
</>
)}
<LogInUser />
</>
);
}
export default LogIn;

View file

@ -0,0 +1,90 @@
import { useState } from "react";
import { useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import {
TextField,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@material-ui/core";
function LogInToken() {
const [open, setOpen] = useState(false);
const [errorText, setErrorText] = useState("");
const [, setLoggedIn] = useLocalStorage("loggedIn", false);
const [token, setToken] = useLocalStorage("token", null);
const history = useHistory();
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleKeyPress = (event) => {
const key = event.key;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
if (key === "Enter") {
LogIn();
}
};
const LogIn = () => {
if (token.length !== 32) {
setErrorText("Token length error");
return;
}
setLoggedIn(true);
setToken(token);
handleClose();
history.go(0);
};
return (
<div>
<Button onClick={handleClickOpen} color="inherit" variant="outlined">
Token Log In
</Button>
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
<DialogTitle>Log In</DialogTitle>
<DialogContent>
<DialogContentText>ADVANCED FEATURE.</DialogContentText>
<TextField
value={token}
onChange={(e) => {
setToken(e.target.value);
}}
error={!!errorText}
helperText={errorText}
margin="dense"
label="token"
type="text"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={LogIn} color="primary">
Log In
</Button>
</DialogActions>
</Dialog>
</div>
);
}
export default LogInToken;

View file

@ -0,0 +1 @@
export { default } from "./LogInToken";

View file

@ -0,0 +1,123 @@
import { useState } from "react";
import { useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import {
TextField,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Snackbar,
} from "@material-ui/core";
import axios from "axios";
function LogInUser() {
const [open, setOpen] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [, setLoggedIn] = useLocalStorage("loggedIn", false);
const [, setToken] = useLocalStorage("token", null);
const history = useHistory();
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
setSnackbarOpen(false);
};
const handleKeyPress = (event) => {
const key = event.key;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
if (key === "Enter") {
LogIn();
}
};
const LogIn = () => {
if (!username || !password) {
return;
}
axios
.post("/auth/login", {
username: username,
password: password,
})
.then(function (response) {
setLoggedIn(true);
setToken(response.data.token);
handleClose();
history.go(0);
})
.catch(function (error) {
setPassword("");
setSnackbarOpen(true);
console.log(error);
});
};
return (
<>
<Button onClick={handleClickOpen} color="primary" variant="contained">
Log In
</Button>
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
<DialogTitle>Log In</DialogTitle>
<DialogContent>
<TextField
autoFocus
value={username}
onChange={(e) => {
setUsername(e.target.value);
}}
margin="dense"
label="username"
type="username"
fullWidth
/>
<TextField
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
margin="dense"
label="password"
type="password"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={LogIn} color="primary">
Log In
</Button>
</DialogActions>
</Dialog>
<Snackbar
open={snackbarOpen}
anchorOrigin={{
vertical: "top",
horizontal: "center",
}}
message="Invalid username or password"
/>
</>
);
}
export default LogInUser;

View file

@ -0,0 +1 @@
export { default } from "./LogInUser";

View file

@ -0,0 +1 @@
export { default } from "./LogIn";

View file

@ -0,0 +1,17 @@
import { Grid, Typography } from "@material-ui/core";
function NetworkHeader({ network }) {
return (
<Grid item>
<Typography variant="h5">
<span>{network["config"]["id"]}</span>
</Typography>
<Typography variant="h6" style={{ fontStyle: "italic" }}>
<span>{network["config"] && network["config"]["name"]}</span>
</Typography>
<span>{network["config"] && network["description"]}</span>
</Grid>
);
}
export default NetworkHeader;

View file

@ -0,0 +1 @@
export { default } from "./NetworkHeader";

View file

@ -0,0 +1,80 @@
import { useState } from "react";
import { useParams, useHistory } from "react-router-dom";
import {
Accordion,
AccordionSummary,
AccordionDetails,
Button,
Dialog,
DialogContent,
DialogContentText,
DialogTitle,
DialogActions,
Typography,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import DeleteIcon from "@material-ui/icons/Delete";
import API from "utils/API";
function NetworkManagment() {
const { nwid } = useParams();
const history = useHistory();
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const sendDelReq = async () => {
const req = await API.delete("/network/" + nwid);
console.log("Action:", req);
};
const deleteNetwork = async () => {
await sendDelReq();
history.push("/");
history.go(0);
};
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Managment</Typography>
</AccordionSummary>
<AccordionDetails>
<Button
variant="contained"
color="secondary"
startIcon={<DeleteIcon />}
onClick={handleClickOpen}
>
Delete Network
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>
{"Are you sure you want to delete this network?"}
</DialogTitle>
<DialogContent>
<DialogContentText>This action cannot be undone.</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={deleteNetwork} color="secondary">
Delete
</Button>
</DialogActions>
</Dialog>
</AccordionDetails>
</Accordion>
);
}
export default NetworkManagment;

View file

@ -0,0 +1 @@
export { default } from "./NetworkManagment";

View file

@ -0,0 +1,185 @@
import { useState, useEffect, useCallback } from "react";
import { useParams } from "react-router-dom";
import {
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox,
Grid,
Typography,
IconButton,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import RefreshIcon from "@material-ui/icons/Refresh";
import DataTable from "react-data-table-component";
import MemberName from "./components/MemberName";
import ManagedIP from "./components/ManagedIP";
import DeleteMember from "./components/DeleteMember";
import MemberSettings from "./components/MemberSettings";
import AddMember from "./components/AddMember";
import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
function NetworkMembers() {
const { nwid } = useParams();
const [members, setMembers] = useState([]);
const fetchData = useCallback(async () => {
try {
const members = await API.get("network/" + nwid + "/member");
setMembers(members.data);
console.log("Members:", members.data);
} catch (err) {
console.error(err);
}
}, [nwid]);
useEffect(() => {
fetchData();
const timer = setInterval(() => fetchData(), 30000);
return () => clearInterval(timer);
}, [nwid, fetchData]);
const sendReq = async (mid, data) => {
const req = await API.post("/network/" + nwid + "/member/" + mid, data);
console.log("Action:", req);
};
const handleChange = (
member,
key1,
key2 = null,
mode = "text",
id = null
) => (event) => {
const value = parseValue(event, mode, member, key1, key2, id);
const updatedMember = replaceValue({ ...member }, key1, key2, value);
const index = members.findIndex((item) => {
return item["config"]["id"] === member["config"]["id"];
});
let mutableMembers = [...members];
mutableMembers[index] = updatedMember;
setMembers(mutableMembers);
const data = setValue({}, key1, key2, value);
sendReq(member["config"]["id"], data);
};
const columns = [
{
id: "auth",
name: "Authorized",
minWidth: "80px",
cell: (row) => (
<Checkbox
checked={row.config.authorized}
color="primary"
onChange={handleChange(row, "config", "authorized", "checkbox")}
/>
),
},
{
id: "address",
name: "Address",
minWidth: "150px",
cell: (row) => (
<Typography variant="body2">{row.config.address}</Typography>
),
},
{
id: "name",
name: "Name/Description",
minWidth: "250px",
cell: (row) => <MemberName member={row} handleChange={handleChange} />,
},
{
id: "ips",
name: "Managed IPs",
minWidth: "220px",
cell: (row) => <ManagedIP member={row} handleChange={handleChange} />,
},
{
***REMOVED***
id: "status",
name: "Peer status",
minWidth: "100px",
cell: (row) =>
row.online ? (
<Typography style={{ color: "#008000" }}>
{"ONLINE (v" +
row.config.vMajor +
"." +
row.config.vMinor +
"." +
row.config.vRev +
")"}
</Typography>
) : (
<Typography color="error">OFFLINE</Typography>
),
},
{
id: "delete",
name: "",
minWidth: "50px",
right: true,
cell: (row) => (
<>
<MemberSettings member={row} handleChange={handleChange} />
<DeleteMember nwid={nwid} mid={row.config.id} callback={fetchData} />
</>
),
},
];
return (
<Accordion defaultExpanded={true}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Members</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container direction="column" spacing={3}>
<IconButton color="primary" onClick={fetchData}>
<RefreshIcon />
</IconButton>
<Grid container>
{members.length ? (
<DataTable
noHeader={true}
columns={columns}
data={[...members]}
/>
) : (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Typography variant="h6" style={{ padding: "10%" }}>
No devices have joined this network. Use the app on your
devices to join <b>{nwid}</b>.
</Typography>
</Grid>
)}
</Grid>
<Grid item>
<AddMember nwid={nwid} callback={fetchData} />
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
);
}
export default NetworkMembers;

View file

@ -0,0 +1,55 @@
import { useState } from "react";
import { List, Typography, IconButton, TextField } from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import API from "utils/API";
function AddMember({ nwid, callback }) {
const [member, setMember] = useState("");
const handleInput = (event) => {
setMember(event.target.value);
};
const addMemberReq = async () => {
if (member.length === 10) {
const req = await API.post("/network/" + nwid + "/member/" + member, {
config: { authorized: true },
hidden: false,
});
console.log("Action:", req);
callback();
}
setMember("");
};
return (
<>
<Typography>Manually Add Member</Typography>
<List
disablePadding={true}
style={{
display: "flex",
flexDirection: "row",
}}
>
<TextField
value={member}
onChange={handleInput}
placeholder={"##########"}
/>
<IconButton size="small" color="primary" onClick={addMemberReq}>
<AddIcon
style={{
fontSize: 16,
}}
/>
</IconButton>
</List>
</>
);
}
export default AddMember;

View file

@ -0,0 +1 @@
export { default } from "./AddMember";

View file

@ -0,0 +1,59 @@
import { useState } from "react";
import {
Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
IconButton,
} from "@material-ui/core";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import API from "utils/API";
function DeleteMember({ nwid, mid, callback }) {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const deleteMemberReq = async () => {
const req = await API.delete("/network/" + nwid + "/member/" + mid);
console.log("Action:", req);
setOpen(false);
callback();
};
return (
<>
<IconButton color="primary" onClick={handleClickOpen}>
<DeleteOutlineIcon color="secondary" style={{ fontSize: 20 }} />
</IconButton>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>
{"Are you sure you want to delete this member?"}
</DialogTitle>
<DialogContent>
<DialogContentText>This action cannot be undone.</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={deleteMemberReq} color="secondary">
Delete
</Button>
</DialogActions>
</Dialog>
</>
);
}
export default DeleteMember;

View file

@ -0,0 +1 @@
export { default } from "./DeleteMember";

View file

@ -0,0 +1,76 @@
import { useState } from "react";
import { Grid, List, TextField, IconButton } from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import { validateIP, normilizeIP } from "utils/IP";
function ManagedIP({ member, handleChange }) {
const [ipInput, setIpInput] = useState();
const [normolizedInput, setNormolizedInput] = useState();
const handleInput = (event) => {
const ip = event.target.value;
setIpInput(ip);
if (validateIP(ip)) {
setNormolizedInput(normilizeIP(ip));
}
};
return (
<Grid>
{member.config.ipAssignments.map((value, i) => (
<List
key={i + "_ips"}
disablePadding={true}
style={{ display: "flex", flexDirection: "row" }}
>
<IconButton
size="small"
color="secondary"
onClick={handleChange(
member,
"config",
"ipAssignments",
"arrayDel",
i
)}
>
<DeleteOutlineIcon style={{ fontSize: 14 }} />
</IconButton>
{value}
</List>
))}
<List
disablePadding={true}
style={{
display: "flex",
flexDirection: "row",
}}
>
<IconButton
size="small"
color="primary"
onClick={handleChange(
member,
"config",
"ipAssignments",
"arrayAdd",
normolizedInput
)}
>
<AddIcon
style={{
fontSize: 14,
}}
/>
</IconButton>
<TextField value={ipInput} onChange={handleInput} />
</List>
</Grid>
);
}
export default ManagedIP;

View file

@ -0,0 +1 @@
export { default } from "./ManagedIP";

View file

@ -0,0 +1,28 @@
import { Grid, TextField } from "@material-ui/core";
function MemberName({ member, handleChange }) {
return (
<Grid>
<TextField
value={member.name}
onChange={handleChange(member, "name")}
label="Name"
variant="filled"
InputLabelProps={{
shrink: true,
}}
/>
<TextField
value={member.description}
onChange={handleChange(member, "description")}
label="Description"
variant="filled"
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
);
}
export default MemberName;

View file

@ -0,0 +1 @@
export { default } from "./MemberName";

View file

@ -0,0 +1,64 @@
import { useState } from "react";
import {
Checkbox,
Dialog,
DialogTitle,
DialogContent,
Grid,
IconButton,
} from "@material-ui/core";
import BuildIcon from "@material-ui/icons/Build";
function MemberSettings({ member, handleChange }) {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<>
<IconButton color="primary" onClick={handleClickOpen}>
<BuildIcon style={{ fontSize: 20 }} />
</IconButton>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{"Member " + member.config.id + " settings"}</DialogTitle>
<DialogContent>
<Grid item>
<Checkbox
checked={member["config"]["activeBridge"]}
color="primary"
onChange={handleChange(
member,
"config",
"activeBridge",
"checkbox"
)}
/>
<span>Allow Ethernet Bridging</span>
</Grid>
<Grid item>
<Checkbox
checked={member["config"]["noAutoAssignIps"]}
color="primary"
onChange={handleChange(
member,
"config",
"noAutoAssignIps",
"checkbox"
)}
/>
<span>Do Not Auto-Assign IPs</span>
</Grid>
</DialogContent>
</Dialog>
</>
);
}
export default MemberSettings;

View file

@ -0,0 +1 @@
export { default } from "./MemberSettings";

View file

@ -0,0 +1 @@
export { default } from "./NetworkMembers";

View file

@ -0,0 +1,132 @@
import { useState } from "react";
import {
Accordion,
AccordionSummary,
AccordionDetails,
Button,
Divider,
Snackbar,
Hidden,
Grid,
Typography,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import CodeMirror from "@uiw/react-codemirror";
import "codemirror/theme/3024-day.css";
import { compile } from "external/RuleCompiler";
import debounce from "lodash/debounce";
import API from "utils/API";
function NetworkRules({ network }) {
const [editor, setEditor] = useState(null);
const [flowData, setFlowData] = useState({
rules: [...network.config.rules],
capabilities: [...network.config.capabilities],
tags: [...network.config.tags],
});
const [errors, setErrors] = useState([]);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const saveChanges = async () => {
if (editor) {
const req = await API.post("/network/" + network["config"]["id"], {
config: { ...flowData },
rulesSource: editor.getValue(),
});
console.log("Action", req);
setSnackbarOpen(true);
const timer = setTimeout(() => {
setSnackbarOpen(false);
}, 1500);
return () => clearTimeout(timer);
}
};
const onChange = debounce((event) => {
const src = event.getValue();
setEditor(event);
let rules = [],
caps = [],
tags = [];
const res = compile(src, rules, caps, tags);
if (!res) {
setFlowData({
rules: [...rules],
capabilities: [...caps],
tags: [...tags],
});
setErrors([]);
} else {
setErrors(res);
}
}, 100);
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Flow Rules</Typography>
</AccordionSummary>
<AccordionDetails>
{/* Important note: value in CodeMirror instance means INITAIL VALUE
or it could be used to replace editor state with the new value.
No need to update on every user character input
*/}
<CodeMirror
value={network["rulesSource"]}
onChange={onChange}
options={{ tabSize: 2, lineWrapping: true }}
/>
<Hidden mdDown>
<div>
<CodeMirror
value={JSON.stringify(flowData, null, 2)}
width="100%"
height="50%"
options={{
theme: "3024-day",
readOnly: true,
lineNumbers: false,
lineWrapping: true,
}}
/>
</div>
</Hidden>
<Divider />
<Grid
item
style={{
margin: "1%",
display: "block",
overflowWrap: "break-word",
width: "250px",
}}
>
{!!errors.length ? (
<Typography color="error">
{"[" + errors[0] + ":" + errors[1] + "] " + errors[2]}
</Typography>
) : (
<Button variant="contained" color="primary" onClick={saveChanges}>
Save Changes
</Button>
)}
</Grid>
<Snackbar
open={snackbarOpen}
anchorOrigin={{
vertical: "top",
horizontal: "center",
}}
message="Saved"
/>
</AccordionDetails>
</Accordion>
);
}
export default NetworkRules;

View file

@ -0,0 +1 @@
export { default } from "./NetworkRules";

View file

@ -0,0 +1,141 @@
import {
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox,
Divider,
Grid,
Typography,
TextField,
Select,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import ManagedRoutes from "./components/ManagedRoutes";
import IPv4AutoAssign from "./components/IPv4AutoAssign";
import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
function NetworkSettings({ network, setNetwork }) {
const sendReq = async (data) => {
try {
const req = await API.post("/network/" + network["config"]["id"], data);
console.log("Action", req);
} catch (err) {
console.error(err);
}
};
const handleChange = (key1, key2, mode = "text", additionalData = null) => (
event
) => {
const value = parseValue(event, mode, additionalData);
let updatedNetwork = replaceValue({ ...network }, key1, key2, value);
setNetwork(updatedNetwork);
let data = setValue({}, key1, key2, value);
sendReq(data);
};
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>General settings</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container direction="column" spacing={3}>
<Grid item>
<Typography>Network ID</Typography>
<Typography variant="h5">
<span>{network["config"]["id"]}</span>
</Typography>
</Grid>
<Grid item>
<TextField
value={network["config"]["name"]}
onChange={handleChange("config", "name")}
label="Name"
variant="filled"
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item>
<TextField
value={network["description"]}
onChange={handleChange("description")}
multiline
rows={2}
rowsMax={Infinity}
label="Description"
variant="filled"
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Divider />
<Grid item>
<Typography>Access Control</Typography>
<Select
native
value={network["config"]["private"]}
onChange={handleChange("config", "private", "json")}
>
<option value={true}>Private</option>
<option value={false}>Public</option>
</Select>
</Grid>
<Divider />
<Grid item>
<ManagedRoutes
routes={network["config"]["routes"]}
handleChange={handleChange}
/>
</Grid>
<Divider />
<Grid item>
<IPv4AutoAssign
ipAssignmentPools={network["config"]["ipAssignmentPools"]}
handleChange={handleChange}
/>
</Grid>
{/* TODO: */}
{/* <Grid item>
<Typography>IPv6 Auto-Assign</Typography>
</Grid> */}
<Divider />
<Grid item>
<TextField
label="Multicast Recipient Limit"
type="number"
value={network["config"]["multicastLimit"]}
onChange={handleChange("config", "multicastLimit", "json")}
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item>
<Checkbox
checked={network["config"]["enableBroadcast"]}
color="primary"
onChange={handleChange("config", "enableBroadcast", "checkbox")}
/>
<span>Enable Broadcast</span>
</Grid>
{/* TODO: */}
{/* <Grid item>
<Typography>DNS</Typography>
</Grid> */}
</Grid>
</AccordionDetails>
</Accordion>
);
}
export default NetworkSettings;

View file

@ -0,0 +1,177 @@
import { useState } from "react";
import {
Button,
Box,
Divider,
Grid,
List,
Typography,
TextField,
IconButton,
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import DataTable from "react-data-table-component";
import { addressPool } from "utils/NetworkConfig";
import { getCIDRAddress, validateIP, normilizeIP } from "utils/IP";
function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
const [start, setStart] = useState("");
const [end, setEnd] = useState("");
const handleStartInput = (event) => {
setStart(event.target.value);
};
const handleEndInput = (event) => {
setEnd(event.target.value);
};
const setDefaultPool = (index) => {
addPoolReq(addressPool[index]["start"], addressPool[index]["end"], true);
handleChange("config", "routes", "custom", [
{
target: getCIDRAddress(
addressPool[index]["start"],
addressPool[index]["end"]
),
},
])(null);
};
const addPoolReq = (localStart, localEnd, reset = false) => {
let data = {};
console.log(localStart, localEnd);
if (validateIP(localStart) && validateIP(localEnd)) {
data["ipRangeStart"] = normilizeIP(localStart);
data["ipRangeEnd"] = normilizeIP(localEnd);
} else {
return;
}
let newPool = [];
if (ipAssignmentPools && !reset) {
newPool = [...ipAssignmentPools];
}
newPool.push(data);
console.log(newPool);
handleChange("config", "ipAssignmentPools", "custom", newPool)(null);
setStart("");
setEnd("");
};
const removePoolReq = (index) => {
let newPool = [...ipAssignmentPools];
newPool.splice(index, 1);
handleChange("config", "ipAssignmentPools", "custom", newPool)(null);
};
const columns = [
{
id: "remove",
width: "10px",
cell: (_, index) => (
<IconButton
size="small"
color="secondary"
onClick={() => removePoolReq(index)}
>
<DeleteOutlineIcon style={{ fontSize: 14 }} />
</IconButton>
),
},
{
id: "Start",
name: "Start",
cell: (row) => row["ipRangeStart"],
},
{
id: "End",
name: "End",
cell: (row) => row["ipRangeEnd"],
},
];
return (
<>
<Typography>IPv4 Auto-Assign</Typography>
<div
style={{
padding: "30px",
}}
>
<Grid container spacing={1}>
{addressPool.map((item, index) => (
<Grid item xs={3} key={item["name"]}>
<Button
variant="contained"
fullWidth={true}
onClick={() => setDefaultPool(index)}
>
{item["name"]}
</Button>
</Grid>
))}
</Grid>
</div>
<Typography style={{ paddingBottom: "10px" }}>
Auto-Assign Pools
</Typography>
<Box border={1} borderColor="grey.300">
<Grid item style={{ margin: "10px" }}>
<DataTable
noHeader={true}
columns={columns}
data={ipAssignmentPools}
/>
<Divider />
<Typography>Add IPv4 Pool</Typography>
<List
style={{
display: "flex",
flexDirection: "row",
}}
>
<TextField
value={start}
onChange={handleStartInput}
placeholder={"Start"}
/>
<Divider
orientation="vertical"
style={{
margin: "10px",
}}
flexItem
/>
<TextField
value={end}
onChange={handleEndInput}
placeholder={"End"}
/>
<IconButton
size="small"
color="primary"
onClick={() => addPoolReq(start, end)}
>
<AddIcon
style={{
fontSize: 16,
}}
/>
</IconButton>
</List>
</Grid>
</Box>
</>
);
}
export default IPv4AutoAssign;

View file

@ -0,0 +1 @@
export { default } from "./IPv4AutoAssign";

View file

@ -0,0 +1,131 @@
import { useState } from "react";
import {
Box,
Divider,
Grid,
List,
Typography,
TextField,
IconButton,
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import DataTable from "react-data-table-component";
import { validateIP, normilizeIP, validateCIDR } from "utils/IP";
function ManagedRoutes({ routes, handleChange }) {
const [destination, setDestination] = useState("");
const [via, setVia] = useState("");
const handleDestinationInput = (event) => {
setDestination(event.target.value);
};
const handleViaInput = (event) => {
setVia(event.target.value);
};
const addRouteReq = () => {
let data = {};
if (validateCIDR(destination)) {
data["target"] = destination;
} else {
return;
}
if (via && validateIP(via)) {
data["via"] = normilizeIP(via);
}
let newRoutes = [...routes];
newRoutes.push(data);
handleChange("config", "routes", "custom", newRoutes)(null);
setDestination("");
setVia("");
};
const removeRouteReq = (index) => {
let newRoutes = [...routes];
newRoutes.splice(index, 1);
handleChange("config", "routes", "custom", newRoutes)(null);
};
const columns = [
{
id: "remove",
width: "10px",
cell: (_, index) => (
<IconButton
size="small"
color="secondary"
onClick={() => removeRouteReq(index)}
>
<DeleteOutlineIcon style={{ fontSize: 14 }} />
</IconButton>
),
},
{
id: "target",
name: "Target",
cell: (row) => row["target"],
},
{
id: "via",
name: "via",
cell: (row) => (row["via"] ? row["via"] : "(LAN)"),
},
];
return (
<>
<Typography style={{ paddingBottom: "10px" }}>
Managed Routes ({routes.length + "/32"})
</Typography>
<Box border={1} borderColor="grey.300">
<Grid item style={{ margin: "10px" }}>
<DataTable noHeader={true} columns={columns} data={routes} />
<Divider />
<Typography>Add Routes</Typography>
<List
style={{
display: "flex",
flexDirection: "row",
}}
>
<TextField
value={destination}
onChange={handleDestinationInput}
placeholder={"Destination (CIDR)"}
/>
<Divider
orientation="vertical"
style={{
margin: "10px",
}}
flexItem
/>
<TextField
value={via}
onChange={handleViaInput}
placeholder={"Via (Optional)"}
/>
<IconButton size="small" color="primary" onClick={addRouteReq}>
<AddIcon
style={{
fontSize: 16,
}}
/>
</IconButton>
</List>
</Grid>
</Box>
</>
);
}
export default ManagedRoutes;

View file

@ -0,0 +1 @@
export { default } from "./ManagedRoutes";

View file

@ -0,0 +1 @@
export { default } from "./NetworkSettings";

View file

@ -0,0 +1,21 @@
import { ThemeProvider } from "@material-ui/styles";
import { createMuiTheme } from "@material-ui/core/styles";
import { red, amber } from "@material-ui/core/colors";
const theme = createMuiTheme({
palette: {
primary: {
main: amber[500],
},
secondary: {
main: red[500],
},
type: "light",
},
});
function Theme({ children }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
export default Theme;

View file

@ -0,0 +1 @@
export { default } from "./Theme";

1147
frontend/src/external/RuleCompiler.js vendored Normal file

File diff suppressed because it is too large Load diff

9
frontend/src/index.css Normal file
View file

@ -0,0 +1,9 @@
body {
margin: 0;
overflow-x: hidden;
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

13
frontend/src/index.jsx Normal file
View file

@ -0,0 +1,13 @@
import "./index.css";
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);

View file

@ -0,0 +1,16 @@
import { useLocalStorage } from "react-use";
import HomeLoggedIn from "components/HomeLoggedIn";
import HomeLoggedOut from "components/HomeLoggedOut";
function Home() {
const [loggedIn] = useLocalStorage("loggedIn", false);
if (loggedIn) {
return <HomeLoggedIn />;
} else {
return <HomeLoggedOut />;
}
}
export default Home;

View file

@ -0,0 +1 @@
export { default } from "./Home";

View file

@ -0,0 +1,83 @@
import { useState, useEffect } from "react";
import { Link as RouterLink, useParams, useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import { Link, Grid, Typography } from "@material-ui/core";
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
import useStyles from "./Network.styles";
import NetworkHeader from "components/NetworkHeader";
import NetworkSettings from "components/NetworkSettings";
import NetworkMembers from "components/NetworkMembers";
import NetworkRules from "components/NetworkRules";
import NetworkManagment from "components/NetworkManagment";
import API from "utils/API";
function Network() {
const { nwid } = useParams();
const [loggedIn] = useLocalStorage("loggedIn", false);
const [network, setNetwork] = useState({});
const classes = useStyles();
const history = useHistory();
useEffect(() => {
async function fetchData() {
try {
const network = await API.get("network/" + nwid);
setNetwork(network.data);
console.log("Current network:", network.data);
} catch (err) {
if (err.response.status === 404) {
history.push("/404");
}
console.error(err);
}
}
fetchData();
}, [nwid, history]);
if (loggedIn) {
return (
<>
<Link color="inherit" component={RouterLink} to="/" underline="none">
<ArrowBackIcon className={classes.backIcon}></ArrowBackIcon>
Networks
</Link>
<div className={classes.container}>
{network["config"] && (
<>
<NetworkHeader network={network} />
<NetworkSettings network={network} setNetwork={setNetwork} />
</>
)}
<NetworkMembers />
{network["config"] && <NetworkRules network={network} />}
<NetworkManagment />
</div>
</>
);
} else {
return (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Grid item xs={10}>
<Typography variant="h5">
You are not authorized. Please Log In
</Typography>
</Grid>
</Grid>
);
}
}
export default Network;

View file

@ -0,0 +1,12 @@
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
backIcon: {
fontSize: 12,
},
container: {
margin: "1%",
},
}));
export default useStyles;

View file

@ -0,0 +1 @@
export { default } from "./Network";

View file

@ -0,0 +1,28 @@
import { Grid, Typography } from "@material-ui/core";
function NotFound() {
return (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Grid item xs={10}>
<Typography variant="h1">
<span>404</span>
</Typography>
<Typography variant="h4">
<span>Not found</span>
</Typography>
</Grid>
</Grid>
);
}
export default NotFound;

View file

@ -0,0 +1 @@
export { default } from "./NotFound";

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