mirror of
https://github.com/dec0dOS/zero-ui.git
synced 2024-09-20 06:56:05 +08:00
refactor: squash commits
This commit is contained in:
parent
63ebcb5915
commit
1e6e237aa3
15
.dockerignore
Normal file
15
.dockerignore
Normal 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
34
.github/ISSUE_TEMPLATE/01_BUG_REPORT.md
vendored
Normal 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. -->
|
29
.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md
vendored
Normal 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)_
|
9
.github/ISSUE_TEMPLATE/03_SUPPORT_QUESTION.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE/03_SUPPORT_QUESTION.md
vendored
Normal 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
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
164
.gitignore
vendored
Normal file
164
.gitignore
vendored
Normal 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
1
.husky/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
_
|
4
.husky/commit-msg
Executable file
4
.husky/commit-msg
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn commitlint --edit
|
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal 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
242
README.md
Executable 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
41
backend/.gitignore
vendored
Normal 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
75
backend/app.js
Normal 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
87
backend/bin/www
Executable 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
6
backend/jsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"exclude": ["node_modules", "**/node_modules/*"],
|
||||
"typeAcquisition": {
|
||||
"exclude": ["dotenv"]
|
||||
}
|
||||
}
|
21
backend/package.json
Normal file
21
backend/package.json
Normal 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
22
backend/routes/auth.js
Normal 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;
|
13
backend/routes/controller.js
Normal file
13
backend/routes/controller.js
Normal 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
85
backend/routes/member.js
Normal 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
90
backend/routes/network.js
Normal 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
33
backend/services/auth.js
Normal 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
172
backend/services/member.js
Normal 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();
|
||||
}
|
94
backend/services/network.js
Normal file
94
backend/services/network.js
Normal 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();
|
||||
}
|
56
backend/utils/constants.js
Normal file
56
backend/utils/constants.js
Normal 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"
|
||||
}
|
||||
]
|
||||
`;
|
19
backend/utils/controller-api.js
Normal file
19
backend/utils/controller-api.js
Normal 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
8
backend/utils/db.js
Normal 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;
|
16
backend/utils/init-admin.js
Normal file
16
backend/utils/init-admin.js
Normal 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"),
|
||||
};
|
||||
};
|
6
backend/utils/zt-address.js
Normal file
6
backend/utils/zt-address.js
Normal 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
554
backend/yarn.lock
Normal 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
1
commitlint.config.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = { extends: ["@commitlint/config-conventional"] };
|
52
docker-compose.yml
Normal file
52
docker-compose.yml
Normal 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
32
docker/zero-ui/Dockerfile
Normal 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" ]
|
9
docker/zerotier/Dockerfile
Normal file
9
docker/zerotier/Dockerfile
Normal 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
7
docs/SCREENSHOTS.md
Normal 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
17
docs/SECURITY.md
Normal 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
BIN
docs/images/homepage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 162 KiB |
BIN
docs/images/logo.png
Normal file
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
BIN
docs/images/network.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 834 KiB |
21
frontend/.gitignore
vendored
Normal file
21
frontend/.gitignore
vendored
Normal 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
10
frontend/jsconfig.json
Normal 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
48
frontend/package.json
Normal 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
BIN
frontend/public/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
19
frontend/public/index.html
Normal file
19
frontend/public/index.html
Normal 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>
|
13
frontend/public/manifest.json
Normal file
13
frontend/public/manifest.json
Normal 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"
|
||||
}
|
3
frontend/public/robots.txt
Normal file
3
frontend/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow: /
|
28
frontend/src/App.jsx
Normal file
28
frontend/src/App.jsx
Normal 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;
|
144
frontend/src/components/Bar/Bar.jsx
Normal file
144
frontend/src/components/Bar/Bar.jsx
Normal 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;
|
BIN
frontend/src/components/Bar/assets/logo.png
Normal file
BIN
frontend/src/components/Bar/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
1
frontend/src/components/Bar/index.jsx
Normal file
1
frontend/src/components/Bar/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./Bar";
|
71
frontend/src/components/HomeLoggedIn/HomeLoggedIn.jsx
Normal file
71
frontend/src/components/HomeLoggedIn/HomeLoggedIn.jsx
Normal 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;
|
16
frontend/src/components/HomeLoggedIn/HomeLoggedIn.styles.jsx
Normal file
16
frontend/src/components/HomeLoggedIn/HomeLoggedIn.styles.jsx
Normal 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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./NetworkButton";
|
1
frontend/src/components/HomeLoggedIn/index.jsx
Normal file
1
frontend/src/components/HomeLoggedIn/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./HomeLoggedIn";
|
31
frontend/src/components/HomeLoggedOut/HomeLoggedOut.jsx
Normal file
31
frontend/src/components/HomeLoggedOut/HomeLoggedOut.jsx
Normal 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;
|
1
frontend/src/components/HomeLoggedOut/index.jsx
Normal file
1
frontend/src/components/HomeLoggedOut/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./HomeLoggedOut";
|
20
frontend/src/components/LogIn/LogIn.jsx
Normal file
20
frontend/src/components/LogIn/LogIn.jsx
Normal 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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./LogInToken";
|
123
frontend/src/components/LogIn/components/LogInUser/LogInUser.jsx
Normal file
123
frontend/src/components/LogIn/components/LogInUser/LogInUser.jsx
Normal 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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./LogInUser";
|
1
frontend/src/components/LogIn/index.jsx
Normal file
1
frontend/src/components/LogIn/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./LogIn";
|
17
frontend/src/components/NetworkHeader/NetworkHeader.jsx
Normal file
17
frontend/src/components/NetworkHeader/NetworkHeader.jsx
Normal 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;
|
1
frontend/src/components/NetworkHeader/index.jsx
Normal file
1
frontend/src/components/NetworkHeader/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./NetworkHeader";
|
|
@ -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;
|
1
frontend/src/components/NetworkManagment/index.jsx
Normal file
1
frontend/src/components/NetworkManagment/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./NetworkManagment";
|
185
frontend/src/components/NetworkMembers/NetworkMembers.jsx
Normal file
185
frontend/src/components/NetworkMembers/NetworkMembers.jsx
Normal 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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./AddMember";
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./DeleteMember";
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./ManagedIP";
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./MemberName";
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./MemberSettings";
|
1
frontend/src/components/NetworkMembers/index.jsx
Normal file
1
frontend/src/components/NetworkMembers/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./NetworkMembers";
|
132
frontend/src/components/NetworkRules/NetworkRules.jsx
Normal file
132
frontend/src/components/NetworkRules/NetworkRules.jsx
Normal 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;
|
1
frontend/src/components/NetworkRules/index.jsx
Normal file
1
frontend/src/components/NetworkRules/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./NetworkRules";
|
141
frontend/src/components/NetworkSettings/NetworkSettings.jsx
Normal file
141
frontend/src/components/NetworkSettings/NetworkSettings.jsx
Normal 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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./IPv4AutoAssign";
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./ManagedRoutes";
|
1
frontend/src/components/NetworkSettings/index.jsx
Normal file
1
frontend/src/components/NetworkSettings/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./NetworkSettings";
|
21
frontend/src/components/Theme/Theme.jsx
Normal file
21
frontend/src/components/Theme/Theme.jsx
Normal 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;
|
1
frontend/src/components/Theme/index.jsx
Normal file
1
frontend/src/components/Theme/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./Theme";
|
1147
frontend/src/external/RuleCompiler.js
vendored
Normal file
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
9
frontend/src/index.css
Normal 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
13
frontend/src/index.jsx
Normal 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")
|
||||
);
|
16
frontend/src/routes/Home/Home.jsx
Normal file
16
frontend/src/routes/Home/Home.jsx
Normal 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;
|
1
frontend/src/routes/Home/index.jsx
Normal file
1
frontend/src/routes/Home/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./Home";
|
83
frontend/src/routes/Network/Network.jsx
Normal file
83
frontend/src/routes/Network/Network.jsx
Normal 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;
|
12
frontend/src/routes/Network/Network.styles.jsx
Normal file
12
frontend/src/routes/Network/Network.styles.jsx
Normal 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;
|
1
frontend/src/routes/Network/index.jsx
Normal file
1
frontend/src/routes/Network/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./Network";
|
28
frontend/src/routes/NotFound/NotFound.jsx
Normal file
28
frontend/src/routes/NotFound/NotFound.jsx
Normal 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;
|
1
frontend/src/routes/NotFound/index.jsx
Normal file
1
frontend/src/routes/NotFound/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./NotFound";
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue