mirror of
https://github.com/dec0dOS/zero-ui.git
synced 2025-07-05 20:41:43 -07:00
Compare commits
171 commits
Author | SHA1 | Date | |
---|---|---|---|
|
6c2654ea8c | ||
|
c510ed3e0b | ||
|
94581a8999 | ||
|
2b79e9dd80 | ||
|
568b411b28 | ||
|
95767e8373 | ||
|
3ca5aa1898 | ||
|
ff708f5f20 | ||
|
85e8a0265b | ||
|
00592d9ad4 | ||
|
bdd2f068fe | ||
|
29127f0433 | ||
|
0fb92649df | ||
|
d826cda2a0 | ||
|
5b1ad30e98 | ||
|
6f7bc6c2c2 | ||
|
ba4e5f2001 | ||
|
beec9d7d78 | ||
|
f5e257e9c8 | ||
|
7db15c26ab | ||
|
fa1a687147 | ||
|
5be92007b7 | ||
|
6978495963 | ||
|
b8a5a37fc7 | ||
|
4a5f8469b3 | ||
|
ee73374c29 | ||
|
2cf3a64cc8 | ||
|
83502755d7 | ||
|
3d67022b38 | ||
|
d19bf07382 | ||
|
80f1ab4185 | ||
|
48485fc546 | ||
|
213c9499f2 | ||
|
f17067f832 | ||
|
61fd0e7186 | ||
|
60ddb2f9dd | ||
|
41f12ad2f3 | ||
|
d65e6fb71a | ||
|
569384038a | ||
|
af9d6447e7 | ||
|
9b1ac15b46 | ||
|
7dd6f3729b | ||
|
efb37caab2 | ||
|
60777f5165 | ||
|
559a98850e | ||
|
91176a2ead | ||
|
986be8d8a0 | ||
|
8a2ba8d1b1 | ||
|
e83bbef742 | ||
|
0521c9dea0 | ||
|
c53beabd66 | ||
|
a22e69e6cb | ||
|
ea828c326f | ||
|
4592671d21 | ||
|
b85bb7299b | ||
|
abcf031f81 | ||
|
19c92ed244 | ||
|
4dd6c3d91d | ||
|
773b64ea30 | ||
|
48a3e8acb5 | ||
|
c87da554d2 | ||
|
3f6098c162 | ||
|
704595bbc2 | ||
|
7e8ca3247f | ||
|
81d5405b88 | ||
|
bc99b5e70b | ||
|
4ae93f95f7 | ||
|
4825289f03 | ||
|
78520c344e | ||
|
f915d7584a | ||
|
a18d8a2146 | ||
|
02384d4d01 | ||
|
a731b8b3af | ||
|
26cb077a70 | ||
|
306112de5d | ||
|
f6a90c3483 | ||
|
a0a6997206 | ||
|
6ae44ed99c | ||
|
e88c95454e | ||
|
69d4d2288f | ||
|
1131b7794a | ||
|
c10ffbe378 | ||
|
473c61b78b | ||
|
bdf406f99f | ||
|
70c580474c | ||
|
5c69694505 | ||
|
5d041f6db6 | ||
|
6df591654e | ||
|
7ae94db630 | ||
|
856682bad1 | ||
|
db8f4979e6 | ||
|
8785b94392 | ||
|
9e6af9705d | ||
|
fbd10cb142 | ||
|
44d0f888c3 | ||
|
d52cf1ca63 | ||
|
90ab2ec140 | ||
|
076b496bf6 | ||
|
450a6ad194 | ||
|
15e4051625 | ||
|
7b51fbc8c6 | ||
|
c9b847c1da | ||
|
4715b557f6 | ||
|
aaaf01aa51 | ||
|
60130dd168 | ||
|
358c07dfe0 | ||
|
19e7c9847a | ||
|
d6f05af126 | ||
|
1e84548dd9 | ||
|
fdf5794e15 | ||
|
40f98cc9df | ||
|
206d12ded3 | ||
|
bc873675f1 | ||
|
d13297e638 | ||
|
6bf0f2975e | ||
|
45244b4433 | ||
|
d3fdac61bd | ||
|
54ec7677db | ||
|
af3e7cab7f | ||
|
0d9feaa8d8 | ||
|
6d69831bc6 | ||
|
3f8b45dd8d | ||
|
e2c651ad05 | ||
|
5a35bdd93d | ||
|
69cddeaf39 | ||
|
646e2b09e7 | ||
|
cb24c16c1d | ||
|
7672e0a233 | ||
|
4444d35128 | ||
|
6b3eacabf6 | ||
|
96fd0b70aa | ||
|
d17b81c52f | ||
|
a9ec7dc4fa | ||
|
838cf4a567 | ||
|
838220520a | ||
|
eae12ccc53 | ||
|
252ade73e0 | ||
|
13976c3c46 | ||
|
153563dc88 | ||
|
2396e973dc | ||
|
cd0276e598 | ||
|
8f7d1e433b | ||
|
f0bc4af6fa | ||
|
d7f2e15328 | ||
|
ce9f7943c0 | ||
|
4151978a68 | ||
|
74a36ad3ef | ||
|
b8026818fe | ||
|
6725a57237 | ||
|
c19b2581b2 | ||
|
f883c7371b | ||
|
52ab0af80c | ||
|
04624d13c8 | ||
|
bc5d78f3d3 | ||
|
96667c39aa | ||
|
ff541dfb06 | ||
|
f1d97afc2e | ||
|
4030e89bfa | ||
|
d67a587db0 | ||
|
0df252378d | ||
|
ddb3f442f8 | ||
|
036e5779ba | ||
|
f30dec6eac | ||
|
75933d7e59 | ||
|
5096a99f75 | ||
|
9b519079ca | ||
|
a247dc07a5 | ||
|
e7fb4d0aa8 | ||
|
30069a699f | ||
|
c8fc67a3d4 | ||
|
8095d2bea2 |
84 changed files with 6828 additions and 15711 deletions
|
@ -3,7 +3,7 @@
|
|||
*Dockerfile*
|
||||
*docker-compose*
|
||||
node_modules
|
||||
jsconfig.js
|
||||
.eslintrc.json
|
||||
.DS_Store
|
||||
tmp
|
||||
temp
|
||||
|
@ -13,4 +13,6 @@ npm-debug.log*
|
|||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.yarn/cache
|
||||
db.json
|
||||
backend/data/db.json
|
||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -7,7 +7,7 @@ on:
|
|||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "27 2 * * 4"
|
||||
- cron: "30 2 * * 6"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
@ -25,12 +25,12 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
|
20
.github/workflows/lock.yml
vendored
20
.github/workflows/lock.yml
vendored
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Lock
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
name: 🔒 Lock closed issues and PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2.0.3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
issue-lock-reason: ""
|
||||
pr-lock-inactive-days: "1"
|
||||
pr-lock-reason: ""
|
22
.github/workflows/main.yml
vendored
22
.github/workflows/main.yml
vendored
|
@ -4,20 +4,21 @@ on:
|
|||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Prepare zero-ui
|
||||
id: prep_zero-ui
|
||||
|
@ -34,14 +35,14 @@ jobs:
|
|||
echo ::set-output name=tags::${TAGS}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and push zero-ui
|
||||
id: docker_build_zero-ui
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/zero-ui/Dockerfile
|
||||
|
@ -49,14 +50,3 @@ jobs:
|
|||
platforms: linux/amd64,linux/arm64,linux/arm
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.prep_zero-ui.outputs.tags }}
|
||||
|
||||
- name: Build and push zerotier-controller
|
||||
id: docker_build_zerotier-controller
|
||||
uses: docker/build-push-action@master
|
||||
with:
|
||||
context: ./
|
||||
file: ./docker/zerotier/Dockerfile
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/zerotier-controller:latest
|
||||
|
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -150,15 +150,9 @@ sketch
|
|||
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/patches
|
||||
!.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
|
||||
# End of https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node
|
||||
|
|
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
File diff suppressed because one or more lines are too long
778
.yarn/releases/yarn-3.2.0-rc.8.cjs
vendored
778
.yarn/releases/yarn-3.2.0-rc.8.cjs
vendored
File diff suppressed because one or more lines are too long
10
.yarnrc.yml
10
.yarnrc.yml
|
@ -1,7 +1,5 @@
|
|||
enableGlobalCache: true
|
||||
|
||||
enableTelemetry: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.0-rc.8.cjs
|
||||
|
|
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -2,6 +2,73 @@
|
|||
|
||||
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.5.8](https://github.com/dec0dOS/zero-ui/compare/v1.5.7...v1.5.8) (2023-10-04)
|
||||
|
||||
### [1.5.7](https://github.com/dec0dOS/zero-ui/compare/v1.5.6...v1.5.7) (2023-10-04)
|
||||
|
||||
### [1.5.6](https://github.com/dec0dOS/zero-ui/compare/v1.5.5...v1.5.6) (2023-10-04)
|
||||
|
||||
### [1.5.5](https://github.com/dec0dOS/zero-ui/compare/v1.5.4...v1.5.5) (2023-10-04)
|
||||
|
||||
### [1.5.4](https://github.com/dec0dOS/zero-ui/compare/v1.5.3...v1.5.4) (2023-10-04)
|
||||
|
||||
### [1.5.3](https://github.com/dec0dOS/zero-ui/compare/v1.5.2...v1.5.3) (2023-10-04)
|
||||
|
||||
### [1.5.2](https://github.com/dec0dOS/zero-ui/compare/v1.5.1...v1.5.2) (2023-10-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix network member api resp handling ([856682b](https://github.com/dec0dOS/zero-ui/commit/856682bad1ccd46970681e45bea8a992043c38f4))
|
||||
- ping peer response handling ([db8f497](https://github.com/dec0dOS/zero-ui/commit/db8f4979e65d23d93de99ffa428c9b9a3d3fd952))
|
||||
- revert fix for 1.12.0 ([5d041f6](https://github.com/dec0dOS/zero-ui/commit/5d041f6db63345950cb5782d586c71e0402b7ce7))
|
||||
|
||||
### [1.5.1](https://github.com/dec0dOS/zero-ui/compare/v1.5.0...v1.5.1) (2022-10-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **backend/app.js:** fix internal error handler ([15e4051](https://github.com/dec0dOS/zero-ui/commit/15e405162590b2e79dfc32751625f5425613bc52))
|
||||
- **backend/services/member.js:** correctly delete members with unset additionalData ([450a6ad](https://github.com/dec0dOS/zero-ui/commit/450a6ad19414723ce00c48caba98743143a3041f))
|
||||
|
||||
## [1.5.0](https://github.com/dec0dOS/zero-ui/compare/v1.4.1...v1.5.0) (2022-08-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **backend:** add cron and ping members ([d3fdac6](https://github.com/dec0dOS/zero-ui/commit/d3fdac61bdd95c7ff42e7db373cd3973d42ca8ce))
|
||||
- last online ([40f98cc](https://github.com/dec0dOS/zero-ui/commit/40f98cc9df322f2b8b4c4a8baed96c96fd2c56d7))
|
||||
|
||||
### [1.4.1](https://github.com/dec0dOS/zero-ui/compare/v1.4.0...v1.4.1) (2022-06-22)
|
||||
|
||||
## [1.4.0](https://github.com/dec0dOS/zero-ui/compare/v1.3.2...v1.4.0) (2022-06-18)
|
||||
|
||||
### Features
|
||||
|
||||
- support http basic auth ([2396e97](https://github.com/dec0dOS/zero-ui/commit/2396e973dc4e40f247cb5fef75d0403ccf0a285a))
|
||||
|
||||
### [1.3.2](https://github.com/dec0dOS/zero-ui/compare/v1.3.1...v1.3.2) (2022-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- better zt controller error handling ([d7f2e15](https://github.com/dec0dOS/zero-ui/commit/d7f2e153286f9e1dacf4d9fa993321fd6fbc3836))
|
||||
- correct conditional for enabling bearer token ([f30dec6](https://github.com/dec0dOS/zero-ui/commit/f30dec6eacfe0d2ac0031861b4f22f34dbab32c7))
|
||||
- disable authentication properly ([75933d7](https://github.com/dec0dOS/zero-ui/commit/75933d7e59838f7c8728ca08cf39659f24a6cac6))
|
||||
- simplify code and check login status on home page load ([ddb3f44](https://github.com/dec0dOS/zero-ui/commit/ddb3f442f85991db4fa0721f0d7c2b004a9ea12d))
|
||||
- stop redundant fetching /auth/login ([ce9f794](https://github.com/dec0dOS/zero-ui/commit/ce9f7943c04d117b0ace3025cd9f84d7b14cf5f3))
|
||||
- update disableAuth in localStorage if server config changes ([036e577](https://github.com/dec0dOS/zero-ui/commit/036e5779ba319a63c9d749c32fcbd5452d2bd2d2))
|
||||
|
||||
### [1.3.1](https://github.com/dec0dOS/zero-ui/compare/v1.3.0...v1.3.1) (2022-06-12)
|
||||
|
||||
## [1.3.0](https://github.com/dec0dOS/zero-ui/compare/v1.2.2...v1.3.0) (2022-05-24)
|
||||
|
||||
### Features
|
||||
|
||||
- disable auth ([#59](https://github.com/dec0dOS/zero-ui/issues/59)) ([e7fb4d0](https://github.com/dec0dOS/zero-ui/commit/e7fb4d0aa84c26493b58a1cd3349fd98a2861191))
|
||||
|
||||
### [1.2.2](https://github.com/dec0dOS/zero-ui/compare/v1.2.1...v1.2.2) (2022-04-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix crash when network had no IP ranges ([8095d2b](https://github.com/dec0dOS/zero-ui/commit/8095d2bea235e348baf3bac515d8aa9eb7adb8cf))
|
||||
|
||||
### [1.2.1](https://github.com/dec0dOS/zero-ui/compare/v1.2.0...v1.2.1) (2021-12-19)
|
||||
|
||||
## [1.2.0](https://github.com/dec0dOS/zero-ui/compare/v1.1.5...v1.2.0) (2021-12-19)
|
||||
|
|
279
README.md
279
README.md
|
@ -10,9 +10,9 @@
|
|||
<a href="https://github.com/dec0dOS/zero-ui/blob/main/docs/SCREENSHOTS.md"><strong>Explore the screenshots »</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">Bug Report</a>
|
||||
·
|
||||
<a href="https://github.com/dec0dOS/zero-ui/issues">Request Feature</a>
|
||||
<a href="https://github.com/dec0dOS/zero-ui/issues">Feature Request</a>
|
||||
·
|
||||
<a href="https://github.com/dec0dOS/zero-ui/discussions">Ask a Question</a>
|
||||
</p>
|
||||
|
@ -43,34 +43,25 @@
|
|||
|
||||
## About
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
This project drew inspiration from [ztncui](https://github.com/key-networks/ztncui) and was developed to address the current limitations of self-hosted [network controllers](https://github.com/zerotier/ZeroTierOne/tree/master/controller). Some of the issues in [ztncui](https://github.com/key-networks/ztncui) cannot be resolved due to the core architecture of the project. ZeroUI aims to resolve these issues and introduces the following features:
|
||||
|
||||
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 compatible 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). ZTNCUI [does not](https://github.com/key-networks/ztncui/issues/63).
|
||||
- ZeroUI is more feature complete. ZeroUI has almost all network-controller supported features, for example, 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.
|
||||
- It is a lightweight [Single Page Application (SPA)](https://en.wikipedia.org/wiki/Single-page_application) built with React, providing an improved user experience, and it is mobile-friendly.
|
||||
- ZeroUI is compatible with the ZeroTier Central API, allowing you to use CLI tools and custom applications designed for ZeroTier Central to manage your networks.
|
||||
- ZeroUI implements controller-specific workarounds to address certain existing [issues](https://github.com/zerotier/ZeroTierOne/issues/859) that are not addressed in [ZTNCUI](https://github.com/key-networks/ztncui/issues/63).
|
||||
- ZeroUI is more feature-complete, supporting almost all network controller features, including a rule editor. Development is ongoing, so you can expect regular updates with new features and bug fixes.
|
||||
- Deploying ZeroUI is straightforward; refer to the [installation](#installation) section for more information.
|
||||
|
||||
<details>
|
||||
<summary>Wait, I haven't heard about ZeroTier yet...</summary>
|
||||
<summary>Curious about ZeroTier?</summary>
|
||||
<br>
|
||||
|
||||
[ZeroTier](https://www.zerotier.com) is awesome [open source project](https://github.com/zerotier/ZeroTierOne) that is available 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.
|
||||
[ZeroTier](https://www.zerotier.com) is an impressive [open-source project](https://github.com/zerotier/ZeroTierOne) available on a wide range of [platforms](https://www.zerotier.com/download/). It can resolve many of your complex networking issues, potentially replacing your intricate VPN setups. You can create a virtual LAN and manage all your devices effortlessly.
|
||||
|
||||
To sum up, ZeroTier combines the capabilities of VPN and SD-WAN, simplifying network management.
|
||||
In essence, ZeroTier combines the capabilities of VPN and SD-WAN, simplifying network management.
|
||||
|
||||
</details>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Built With
|
||||
## Built With
|
||||
|
||||
Frontend:
|
||||
|
||||
|
@ -83,7 +74,7 @@ Backend:
|
|||
- [Express](https://expressjs.com)
|
||||
- [Lowdb](https://github.com/typicode/lowdb)
|
||||
|
||||
Deploy:
|
||||
Ready-to-use deployment solution:
|
||||
|
||||
- [Docker](https://www.docker.com)
|
||||
- [Docker Compose](https://docs.docker.com/compose/)
|
||||
|
@ -93,53 +84,74 @@ Deploy:
|
|||
|
||||
### 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).
|
||||
The recommended way 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 in the [official Docker documentation](https://docs.docker.com/get-docker).
|
||||
|
||||
For HTTPS setup you will need a domain name.
|
||||
For HTTPS setup, you will need a domain name. You can obtain one for free at https://www.duckdns.org.
|
||||
|
||||
### Installation
|
||||
|
||||
The most simple one-minute installation. Great for the fresh VPS setup.
|
||||
Here's a straightforward one-minute installation guide, perfect for a fresh VPS setup:
|
||||
|
||||
1. Download the `docker-compose.yml` file
|
||||
```sh
|
||||
wget https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml
|
||||
```
|
||||
or
|
||||
```sh
|
||||
curl -L -O 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 (CTRL-C to stop log preview)
|
||||
```sh
|
||||
docker-compose logs -f
|
||||
```
|
||||
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 -A INPUT -p tcp --dport 80 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
|
||||
iptables -A INPUT -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.
|
||||
1. Create a project directory
|
||||
|
||||
> 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`.
|
||||
```sh
|
||||
mkdir -p /srv/zero-ui/
|
||||
cd /srv/zero-ui/
|
||||
```
|
||||
|
||||
2. Download the `docker-compose.yml` file
|
||||
|
||||
```sh
|
||||
wget https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
curl -L -O https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml
|
||||
```
|
||||
|
||||
3. Replace `YOURDOMAIN.com` with your domain name and set admin credentials (`ZU_DEFAULT_PASSWORD`) in `docker-compose.yml`
|
||||
4. Pull the image
|
||||
|
||||
```sh
|
||||
docker pull dec0dos/zero-ui
|
||||
```
|
||||
|
||||
5. Run the containers
|
||||
|
||||
```sh
|
||||
docker-compose up -d --no-build
|
||||
```
|
||||
|
||||
6. Check if everything is okay (`CTRL-C` to stop log preview)
|
||||
|
||||
```sh
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
7. 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 can use iptables:
|
||||
|
||||
```sh
|
||||
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
|
||||
iptables -A INPUT -p udp --dport 9993 -j ACCEPT
|
||||
```
|
||||
|
||||
8. Navigate to `https://YOURDOMAIN.com/app/`.
|
||||
Now you can use your ZeroUI instance with HTTPS support and automated certificate renewal.
|
||||
|
||||
> To disable Caddy proxy and HTTPS, remove the `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 |
|
||||
|
@ -153,138 +165,149 @@ Advanced manual setups are also supported. Check the following environment varia
|
|||
| 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 |
|
||||
| ZU_DISABLE_AUTH | `false` | If set to true, automatically log in all users. This is useful if ZeroUI is protected by an authentication proxy. Note that when this value is changed, the localStorage of instances of logged-in panels should be cleared |
|
||||
| ZU_LAST_SEEN_FETCH | `true`| Enables [Last Seen feature](https://github.com/dec0dOS/zero-ui/issues/40) |
|
||||
| ZU_LAST_SEEN_SCHEDULE | `*/5 * * * *` | Last Seen cron-like schedule |
|
||||
| ZU_LOGIN_LIMIT | `false` | Enable rate limiter for /login endpoint |
|
||||
| ZU_LOGIN_LIMIT_WINDOW | 30 | The duration of the IP ban in minutes |
|
||||
| ZU_LOGIN_LIMIT_ATTEMPTS | 50 | Login attemps before ban |
|
||||
|
||||
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/#readme).
|
||||
ZeroUI could be deployed as a regular nodejs web application, but it requires a ZeroTier controller that is installed with the `zerotier-one` package. For more info about the network controller, you could read [here](https://github.com/zerotier/ZeroTierOne/tree/master/controller/#readme).
|
||||
|
||||
For Ansible Role, please refer to the [zero-ui-ansible repository](https://github.com/dec0dOS/zero-ui-ansible).
|
||||
|
||||
<details>
|
||||
<summary>Controller setup tips</summary>
|
||||
<summary>Controller Setup Tips (Outside Docker)</summary>
|
||||
<br>
|
||||
|
||||
If you are using the existing controller on the host, it may be necessary to allow connection from the Docker container.
|
||||
You could do it in two ways:
|
||||
If you are using an existing controller on the host, you may need to allow connections from the Docker container. There are two ways to do this:
|
||||
|
||||
1. Allowing controller management from any IP address:
|
||||
1. Allow controller management from any IP address:
|
||||
|
||||
```sh
|
||||
echo "{\"settings\": {\"portMappingEnabled\": true,\"softwareUpdate\": \"disable\",\"allowManagementFrom\": [\"0.0.0.0/0\"]}}" > /var/lib/zerotier-one/local.conf
|
||||
```
|
||||
|
||||
> Warning: don't forget to block connections to 9993/TCP from WAN. Direct controller API does not mean to be exposed to WAN, it should be proxified via ZeroUI backend.
|
||||
> Warning: Don't forget to block connections to 9993/TCP from the WAN. Directly exposing the controller API to the WAN is not recommended; it should be proxified via the ZeroUI backend.
|
||||
|
||||
2. Adding `network_mode: "host"` to zero-ui in docker-compose.yml.
|
||||
2. Add `network_mode: "host"` to zero-ui in `docker-compose.yml`.
|
||||
|
||||
More info is provided in the following [discussion](https://github.com/dec0dOS/zero-ui/discussions/8).
|
||||
For more information, please refer to this [discussion](https://github.com/dec0dOS/zero-ui/discussions/8).
|
||||
|
||||
</details>
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, log in with your credentials that are declared with ZU_DEFAULT_USERNAME and ZU_DEFAULT_PASSWORD.
|
||||
After installation, log in with the credentials declared with `ZU_DEFAULT_USERNAME` and `ZU_DEFAULT_PASSWORD`.
|
||||
|
||||
Currently, almost all main ZeroTier Central features are available. Refer to the [roadmap](#roadmap) for more information.
|
||||
Currently, some main ZeroTier Central features are missing. Refer to the [roadmap](#roadmap) for more information.
|
||||
|
||||
_For the screenshots, please refer to the [screenshots](docs/SCREENSHOTS.md)._
|
||||
_For screenshots, please refer to the [screenshots](docs/SCREENSHOTS.md) section._
|
||||
|
||||
### Update
|
||||
|
||||
To get the latest version just run
|
||||
To get the latest version, simply run
|
||||
|
||||
docker-compose pull && docker-compose up -d --no-build
|
||||
```sh
|
||||
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.
|
||||
You could also set up [watchtower](https://github.com/containrrr/watchtower) for automatic updates.
|
||||
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--restart always \
|
||||
containrrr/watchtower \
|
||||
--cleanup --include-restarting \
|
||||
zu-main zu-controller
|
||||
in the folder where `docker-compose.yml` is located. Backups may not be necessary since most of your data is usually saved at the controller level, but it's still a good idea to consider them as a precautionary measure.
|
||||
|
||||
### Backup
|
||||
|
||||
The easiest way to create your ZeroUI data backup is to use the following commands:
|
||||
You should regularly back up the `zerotier-one` and `data` folders in your ZeroUI installation directory. You can do this manually before upgrading using 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
|
||||
```sh
|
||||
tar cvf backup-ui.tar data/
|
||||
tar cvf backup-zt.tar zerotier-one/
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
See the [open issues](https://github.com/dec0dOS/zero-ui/issues) for a list of proposed features (and known issues).
|
||||
For a list of proposed features (and known issues), see the [open issues](https://github.com/dec0dOS/zero-ui/issues).
|
||||
|
||||
- [Top Feature Requests](https://github.com/dec0dOS/zero-ui/issues?q=label%3Aenhancement+is%3Aopen+sort%3Areactions-%2B1-desc) (Add your own votes using the 👍 reaction)
|
||||
- [Top Bugs](https://github.com/dec0dOS/zero-ui/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Areactions-%2B1-desc) (Add your own votes using the 👍 reaction)
|
||||
- [Top Feature Requests](https://github.com/dec0dOS/zero-ui/issues?q=label%3Aenhancement+is%3Aopen+sort%3Areactions-%2B1-desc) (Add your votes using the 👍 reaction)
|
||||
- [Top Bugs](https://github.com/dec0dOS/zero-ui/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Areactions-%2B1-desc) (Add your votes using the 👍 reaction)
|
||||
- [Newest Bugs](https://github.com/dec0dOS/zero-ui/issues?q=is%3Aopen+is%3Aissue+label%3Abug)
|
||||
|
||||
[](https://github.com/dec0dOS/zero-ui/issues)
|
||||
|
||||
Please try to create bug reports that are:
|
||||
When creating bug reports, please ensure they are:
|
||||
|
||||
- _Reproducible._ Include steps to reproduce the problem.
|
||||
- _Specific._ Include as much detail as possible: which version, what environment, etc.
|
||||
- _Unique._ Do not duplicate existing opened issues.
|
||||
- _Scoped to a Single Bug._ One bug per report.
|
||||
- _Specific._ Provide as much detail as possible, including version, environment, etc.
|
||||
- _Unique._ Avoid duplicating existing open issues.
|
||||
- _Scoped to a Single Bug._ Report one bug per issue.
|
||||
|
||||
## Contributing
|
||||
|
||||
First off, thanks for taking the time to contribute! 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**.
|
||||
Firstly, thank you for considering contributing! Contributions are what make the open-source community thrive. Any contributions you make will benefit everyone, and they are highly appreciated.
|
||||
|
||||
1. Fork the project
|
||||
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
|
||||
3. Commit your changes (`git commit -m 'feat: add amazing_feature'`)
|
||||
4. Push to the branch (`git push origin feat/amazing_feature`)
|
||||
To contribute:
|
||||
|
||||
1. Fork the project.
|
||||
2. Create your feature branch (`git checkout -b feat/amazing_feature`).
|
||||
3. Commit your changes (`git commit -m 'feat: add amazing_feature'`).
|
||||
4. Push to the branch (`git push origin feat/amazing_feature`).
|
||||
5. [Open a Pull Request](https://github.com/dec0dOS/zero-ui/compare?expand=1)
|
||||
|
||||
ZeroUI uses [conventional commits](https://www.conventionalcommits.org), so please follow the guidelines.
|
||||
Run `yarn commit` to open [TUI](https://en.wikipedia.org/wiki/Text-based_user_interface) that follows conventional commits guidelines.
|
||||
ZeroUI uses [conventional commits](https://www.conventionalcommits.org), so please follow the guidelines. You can use `yarn commit` to open a [Text-Based User Interface (TUI)](https://en.wikipedia.org/wiki/Text-based_user_interface) that follows conventional commits guidelines.
|
||||
|
||||
### Development environment
|
||||
### Development Environment
|
||||
|
||||
To set up a development environment, please follow these steps:
|
||||
To set up a development environment, follow these steps:
|
||||
|
||||
1. Clone the repo
|
||||
```sh
|
||||
git clone https://github.com/dec0dOS/zero-ui.git
|
||||
```
|
||||
|
||||
```sh
|
||||
git clone https://github.com/dec0dOS/zero-ui.git
|
||||
cd zero-ui
|
||||
```
|
||||
|
||||
2. Install packages
|
||||
```sh
|
||||
yarn install && yarn installDeps
|
||||
```
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
3. Start the development server
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
```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:
|
||||
You will also need to install the ZeroTier controller. On Linux, installing the `zerotier-one` package is sufficient, but other platforms may require some adjustments. First, you should obtain the controller token. On macOS, you can find it using the following command:
|
||||
|
||||
sudo cat "/Library/Application Support/ZeroTier/One/authtoken.secret"
|
||||
```sh
|
||||
sudo cat "/Library/Application Support/ZeroTier/One/authtoken.secret"
|
||||
```
|
||||
|
||||
After you could start ZeroUI development environment:
|
||||
Afterward, you can start the ZeroUI development environment:
|
||||
|
||||
ZU_CONTROLLER_TOKEN=TOKEN_FROM_authtoken.secret yarn dev
|
||||
```sh
|
||||
ZU_CONTROLLER_TOKEN=TOKEN_FROM_authtoken.secret yarn dev
|
||||
```
|
||||
|
||||
_For other platforms, please refer to [ZeroTier manual](https://www.zerotier.com/manual/#4)._
|
||||
For other platforms, please refer to the [ZeroTier manual](https://docs.zerotier.com/service/v1/).
|
||||
|
||||
## Support
|
||||
|
||||
Reach out to me at one of the following places:
|
||||
|
||||
- [GitHub Discussions](https://github.com/dec0dOS/zero-ui/discussions)
|
||||
- Email in [GitHub profile](https://github.com/dec0dOS)
|
||||
If you need assistance or have questions, reach out through [GitHub Discussions](https://github.com/dec0dOS/zero-ui/discussions).
|
||||
|
||||
## Security
|
||||
|
||||
ZeroUI follows good practices of security, but 100% security cannot be assured. ZeroUI is provided "as is" without any warranty. Use at your own risk.
|
||||
ZeroUI follows best practices for security, but complete security cannot be guaranteed. 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 enterprise support and a more reliable and scalable solution, please consider using ZeroTier Central.
|
||||
|
||||
_For more information and to report security issues, please refer to our [security documentation](docs/SECURITY.md)._
|
||||
For more information and to report security issues, please refer to our [security documentation](docs/SECURITY.md).
|
||||
|
||||
## Copyright notice
|
||||
## Copyright Notice
|
||||
|
||||
ZeroUI is not affiliated or associated with or endorsed by ZeroTier Central or ZeroTier, Inc.
|
||||
ZeroUI is not affiliated with, associated with, or endorsed by ZeroTier Central or ZeroTier, Inc.
|
||||
|
||||
## License
|
||||
|
||||
|
|
63
backend/.eslintrc.json
Normal file
63
backend/.eslintrc.json
Normal file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"unicorn",
|
||||
"jsdoc",
|
||||
"import",
|
||||
"promise",
|
||||
"sonarjs"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:n/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:unicorn/recommended",
|
||||
"plugin:jsdoc/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:promise/recommended",
|
||||
"plugin:sonarjs/recommended",
|
||||
"plugin:security/recommended"
|
||||
],
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-misused-promises": [
|
||||
"error",
|
||||
{
|
||||
"checksVoidReturn": false
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"jsdoc/require-jsdoc": ["warn", { "publicOnly": true }],
|
||||
"jsdoc/require-description": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"unicorn/no-empty-file": "off",
|
||||
"unicorn/consistent-function-scoping": [
|
||||
"error",
|
||||
{
|
||||
"checkArrowFunctions": false
|
||||
}
|
||||
],
|
||||
"unicorn/prefer-module": "off",
|
||||
"unicorn/prevent-abbreviations": "off",
|
||||
"unicorn/catch-error-name": "off",
|
||||
"unicorn/prefer-ternary": "off",
|
||||
"unicorn/prefer-event-target": "off",
|
||||
"security/detect-object-injection": "off",
|
||||
"security/detect-non-literal-fs-filename": "off"
|
||||
}
|
||||
}
|
|
@ -1,33 +1,44 @@
|
|||
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");
|
||||
import path from "path";
|
||||
import * as url from "url";
|
||||
import express from "express";
|
||||
import logger from "morgan";
|
||||
import compression from "compression";
|
||||
import bearerToken from "express-bearer-token";
|
||||
import helmet from "helmet";
|
||||
import { Cron } from "croner";
|
||||
|
||||
const db = require("./utils/db");
|
||||
const initAdmin = require("./utils/init-admin");
|
||||
import { db } from "./utils/db.js";
|
||||
import { initAdmin } from "./utils/init-admin.js";
|
||||
import { pingAll } from "./utils/ping.js";
|
||||
|
||||
const authRoutes = require("./routes/auth");
|
||||
const networkRoutes = require("./routes/network");
|
||||
const memberRoutes = require("./routes/member");
|
||||
const controllerRoutes = require("./routes/controller");
|
||||
import authRoutes from "./routes/auth.js";
|
||||
import networkRoutes from "./routes/network.js";
|
||||
import memberRoutes from "./routes/member.js";
|
||||
import controllerRoutes from "./routes/controller.js";
|
||||
|
||||
const app = express();
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(
|
||||
bearerToken({
|
||||
headerKey: "Bearer",
|
||||
})
|
||||
);
|
||||
if (process.env.ZU_DISABLE_AUTH !== "true") {
|
||||
app.use(
|
||||
bearerToken({
|
||||
headerKey: "token",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
console.debug = function () {};
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === "production" &&
|
||||
process.env.ZU_SECURE_HEADERS !== "false"
|
||||
) {
|
||||
// @ts-ignore
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
|
@ -40,6 +51,10 @@ if (
|
|||
["/app", "/app/*"],
|
||||
express.static(path.join(__dirname, "..", "frontend", "build"))
|
||||
);
|
||||
app.use(
|
||||
["/locales", "/locales/*"],
|
||||
express.static(path.join(__dirname, "..", "frontend", "build", "locales"))
|
||||
);
|
||||
app.get(["/app/network/*"], function (req, res) {
|
||||
res.sendFile(path.join(__dirname, "..", "frontend", "build", "index.html"));
|
||||
});
|
||||
|
@ -52,6 +67,18 @@ initAdmin().then(function (admin) {
|
|||
db.defaults({ users: [admin], networks: [] }).write();
|
||||
});
|
||||
|
||||
if (process.env.ZU_LAST_SEEN_FETCH !== "false") {
|
||||
let schedule = process.env.ZU_LAST_SEEN_SCHEDULE || "*/5 * * * *";
|
||||
Cron(schedule, () => {
|
||||
console.debug("Running scheduled job");
|
||||
const networks = db.get("networks").value();
|
||||
networks.forEach(async (network) => {
|
||||
console.debug("Processing network " + network.id);
|
||||
await pingAll(network);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const routerAPI = express.Router();
|
||||
const routerController = express.Router();
|
||||
|
||||
|
@ -67,9 +94,9 @@ app.use("/controller", routerController); // other controller-specific routes
|
|||
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
|
||||
app.use(function (err, req, res, next) {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ error: "500 Internal server error" });
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
export default app;
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
require("dotenv").config();
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require("../app");
|
||||
var debug = require("debug")("zero-ui:server");
|
||||
var http = require("http");
|
||||
import app from "../app.js";
|
||||
|
||||
console.log("zero-ui:server");
|
||||
import http from "node:http";
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
|
@ -82,6 +84,6 @@ function onError(error) {
|
|||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
|
||||
debug("Listening on " + bind);
|
||||
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr?.port;
|
||||
console.log("Listening on " + bind);
|
||||
}
|
1
backend/global.d.ts
vendored
Normal file
1
backend/global.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module "axios";
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"exclude": ["node_modules", "**/node_modules/*"],
|
||||
"typeAcquisition": {
|
||||
"exclude": ["dotenv"]
|
||||
}
|
||||
}
|
|
@ -1,20 +1,43 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||
"typecheck": "tsc --pretty --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.27.2",
|
||||
"compression": "^1.7.4",
|
||||
"debug": "~4.3.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "~4.17.1",
|
||||
"croner": "^7.0.2",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-bearer-token": "^2.4.0",
|
||||
"helmet": "^4.6.0",
|
||||
"express-rate-limit": "^7.1.1",
|
||||
"helmet": "^5.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lowdb": "^1.0.0",
|
||||
"morgan": "~1.10.0",
|
||||
"pbkdf2-wrapper": "^1.3.2"
|
||||
"morgan": "^1.10.0",
|
||||
"pbkdf2-wrapper": "^1.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.3",
|
||||
"@types/debug": "^4.1.9",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/morgan": "^1.9.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsdoc": "^46.8.2",
|
||||
"eslint-plugin-n": "^16.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"eslint-plugin-sonarjs": "^0.21.0",
|
||||
"eslint-plugin-unicorn": "^48.0.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,39 @@
|
|||
const express = require("express");
|
||||
import express from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const auth = require("../services/auth");
|
||||
import * as auth from "../services/auth.js";
|
||||
|
||||
router.post("/login", async function (req, res) {
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: (Number(process.env.ZU_LOGIN_LIMIT_WINDOW) || 30) * 60 * 1000, // 30 minutes
|
||||
max: Number(process.env.ZU_LOGIN_LIMIT_ATTEMPTS) || 50, // limit each IP to 50 requests per windowMs
|
||||
message: {
|
||||
status: 429,
|
||||
error: "tooManyAttempts",
|
||||
},
|
||||
});
|
||||
|
||||
const loginLimiterWrapper = (req, res, next) => {
|
||||
if (
|
||||
process.env.NODE_ENV === "production" &&
|
||||
process.env.ZU_LOGIN_LIMIT === "true"
|
||||
) {
|
||||
return loginLimiter(req, res, next);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
};
|
||||
|
||||
router.get("/login", async function (req, res) {
|
||||
if (process.env.ZU_DISABLE_AUTH === "true") {
|
||||
res.send({ enabled: false });
|
||||
} else {
|
||||
res.send({ enabled: true });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/login", loginLimiterWrapper, async function (req, res) {
|
||||
if (req.body.username && req.body.password) {
|
||||
auth.authorize(req.body.username, req.body.password, function (err, user) {
|
||||
if (user) {
|
||||
|
@ -19,4 +49,4 @@ router.post("/login", async function (req, res) {
|
|||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const express = require("express");
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
|
||||
const auth = require("../services/auth");
|
||||
const api = require("../utils/controller-api");
|
||||
import * as auth from "../services/auth.js";
|
||||
import { api } from "../utils/controller-api.js";
|
||||
|
||||
router.get("/status", auth.isAuthorized, async function (req, res) {
|
||||
api.get("status").then(function (controllerRes) {
|
||||
|
@ -10,4 +10,4 @@ router.get("/status", auth.isAuthorized, async function (req, res) {
|
|||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
const express = require("express");
|
||||
import express from "express";
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
const auth = require("../services/auth");
|
||||
const member = require("../services/member");
|
||||
import * as auth from "../services/auth.js";
|
||||
import * as member from "../services/member.js";
|
||||
|
||||
const api = require("../utils/controller-api");
|
||||
import { api } from "../utils/controller-api.js";
|
||||
|
||||
// get all members
|
||||
router.get("/", auth.isAuthorized, async function (req, res) {
|
||||
// @ts-ignore
|
||||
const nwid = req.params.nwid;
|
||||
api
|
||||
.get("controller/network/" + nwid + "/member")
|
||||
|
@ -16,13 +17,14 @@ router.get("/", auth.isAuthorized, async function (req, res) {
|
|||
const data = await member.getMembersData(nwid, mids);
|
||||
res.send(data);
|
||||
})
|
||||
.catch(function () {
|
||||
res.status(404).send({ error: "Network not found" });
|
||||
.catch(function (err) {
|
||||
res.status(404).send({ error: `Network not found ${err}` });
|
||||
});
|
||||
});
|
||||
|
||||
// get member
|
||||
router.get("/:mid", auth.isAuthorized, async function (req, res) {
|
||||
// @ts-ignore
|
||||
const nwid = req.params.nwid;
|
||||
const mid = req.params.mid;
|
||||
const data = await member.getMembersData(nwid, [mid]);
|
||||
|
@ -35,6 +37,7 @@ router.get("/:mid", auth.isAuthorized, async function (req, res) {
|
|||
|
||||
// update member
|
||||
router.post("/:mid", auth.isAuthorized, async function (req, res) {
|
||||
// @ts-ignore
|
||||
const nwid = req.params.nwid;
|
||||
const mid = req.params.mid;
|
||||
member.updateMemberAdditionalData(nwid, mid, req.body);
|
||||
|
@ -56,6 +59,7 @@ router.post("/:mid", auth.isAuthorized, async function (req, res) {
|
|||
|
||||
// delete member
|
||||
router.delete("/:mid", auth.isAuthorized, async function (req, res) {
|
||||
// @ts-ignore
|
||||
const nwid = req.params.nwid;
|
||||
const mid = req.params.mid;
|
||||
member.deleteMemberAdditionalData(nwid, mid);
|
||||
|
@ -82,4 +86,4 @@ router.delete("/:mid", auth.isAuthorized, async function (req, res) {
|
|||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
const express = require("express");
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
|
||||
const auth = require("../services/auth");
|
||||
const network = require("../services/network");
|
||||
import * as auth from "../services/auth.js";
|
||||
import * as network from "../services/network.js";
|
||||
|
||||
const api = require("../utils/controller-api");
|
||||
const constants = require("../utils/constants");
|
||||
const getZTAddress = require("../utils/zt-address");
|
||||
import { api } from "../utils/controller-api.js";
|
||||
import { defaultRules } from "../utils/constants.js";
|
||||
import { getZTAddress } from "../utils/zt-address.js";
|
||||
|
||||
let ZT_ADDRESS = null;
|
||||
getZTAddress().then(function (address) {
|
||||
|
@ -40,7 +40,7 @@ router.post("/", auth.isAuthorized, async function (req, res) {
|
|||
const config = reqData.config;
|
||||
delete reqData.config;
|
||||
reqData = config;
|
||||
reqData.rules = JSON.parse(constants.defaultRules);
|
||||
reqData.rules = JSON.parse(defaultRules);
|
||||
} else {
|
||||
res.status(400).send({ error: "Bad request" });
|
||||
}
|
||||
|
@ -87,4 +87,4 @@ router.delete("/:nwid", auth.isAuthorized, async function (req, res) {
|
|||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
const db = require("../utils/db");
|
||||
const verifyHash = require("pbkdf2-wrapper/verifyHash");
|
||||
import { db } from "../utils/db.js";
|
||||
import verifyHash from "pbkdf2-wrapper/verifyHash.js";
|
||||
|
||||
exports.authorize = authorize;
|
||||
async function authorize(username, password, callback) {
|
||||
export 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"));
|
||||
if (!user.value()) return callback(new Error("logInFailed")); // If return "user not found" someone can do a user listing
|
||||
const verified = await verifyHash(password, user.value()["password_hash"]);
|
||||
if (verified) {
|
||||
return callback(null, user.value());
|
||||
} else {
|
||||
return callback(new Error("Invalid password"));
|
||||
return callback(new Error("logInFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
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" });
|
||||
}
|
||||
export async function isAuthorized(req, res, next) {
|
||||
if (process.env.ZU_DISABLE_AUTH === "true") {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).send({ error: "Specify token" });
|
||||
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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
const _ = require("lodash");
|
||||
const axios = require("axios");
|
||||
import _ from "lodash";
|
||||
import axios from "axios";
|
||||
|
||||
const api = require("../utils/controller-api");
|
||||
const db = require("../utils/db");
|
||||
const getZTAddress = require("../utils/zt-address");
|
||||
import { api } from "../utils/controller-api.js";
|
||||
import { db } from "../utils/db.js";
|
||||
import { getZTAddress } from "../utils/zt-address.js";
|
||||
|
||||
let ZT_ADDRESS = null;
|
||||
getZTAddress().then(function (address) {
|
||||
|
@ -25,25 +25,35 @@ async function getMemberAdditionalData(data) {
|
|||
network.defaults({ members: [] }).get("members").write();
|
||||
// END MIGRATION SECTION
|
||||
|
||||
const additionalData = db
|
||||
const member = db
|
||||
.get("networks")
|
||||
.find({ id: data.nwid })
|
||||
.get("members")
|
||||
.find({ id: data.id })
|
||||
.get("additionalConfig")
|
||||
.value();
|
||||
.find({ id: data.id });
|
||||
|
||||
const additionalData = member.get("additionalConfig").value() || {};
|
||||
const lastOnline = member.get("lastOnline").value() || 0;
|
||||
|
||||
const peer = await getPeer(data.id);
|
||||
let peerData = {};
|
||||
if (peer) {
|
||||
if (peer && !_.isEmpty(peer)) {
|
||||
peerData.latency = peer.latency;
|
||||
if (peer.latency !== -1) peerData.online = 1;
|
||||
if (peer.latency == -1) peerData.online = 2;
|
||||
peerData.clientVersion = peer.version;
|
||||
if (peer.paths[0]) {
|
||||
peerData.lastOnline = peer.paths[0].lastReceive;
|
||||
peerData.physicalAddress = peer.paths[0].address.split("/")[0];
|
||||
peerData.physicalPort = peer.paths[0].address.split("/")[1];
|
||||
if (peer.paths.length > 0) {
|
||||
let path = peer.paths.filter((p) => {
|
||||
let ret = p.active && !p.expired;
|
||||
if (typeof p.preferred !== "undefined") {
|
||||
ret = ret && p.preferred;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
if (path.length > 0) {
|
||||
peerData.lastOnline = path[0].lastReceive;
|
||||
peerData.physicalAddress = path[0].address.split("/")[0];
|
||||
peerData.physicalPort = path[0].address.split("/")[1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
peerData.online = 0;
|
||||
|
@ -57,11 +67,12 @@ async function getMemberAdditionalData(data) {
|
|||
|
||||
return {
|
||||
id: data.nwid + "-" + data.id,
|
||||
type: "Member",
|
||||
clock: Math.floor(new Date().getTime() / 1000),
|
||||
clock: new Date().getTime(),
|
||||
networkId: data.nwid,
|
||||
nodeId: data.id,
|
||||
controllerId: ZT_ADDRESS,
|
||||
// @ts-ignore
|
||||
lastOnline: lastOnline,
|
||||
...additionalData,
|
||||
...peerData,
|
||||
config: data,
|
||||
|
@ -75,12 +86,12 @@ async function filterDeleted(nwid, mid) {
|
|||
.get("members")
|
||||
.find({ id: mid });
|
||||
|
||||
if (!member.get("deleted").value()) return mid;
|
||||
let deleted = member.get("deleted").value() || false;
|
||||
if (!deleted) return mid;
|
||||
else return;
|
||||
}
|
||||
|
||||
exports.getMembersData = getMembersData;
|
||||
async function getMembersData(nwid, mids) {
|
||||
export async function getMembersData(nwid, mids) {
|
||||
const prefix = "/controller/network/" + nwid + "/member/";
|
||||
const filtered = (
|
||||
await Promise.all(mids.map(async (mid) => await filterDeleted(nwid, mid)))
|
||||
|
@ -94,7 +105,7 @@ async function getMembersData(nwid, mids) {
|
|||
return res;
|
||||
})
|
||||
)
|
||||
.catch(function () {
|
||||
.catch(function (err) {
|
||||
return [];
|
||||
});
|
||||
|
||||
|
@ -107,8 +118,7 @@ async function getMembersData(nwid, mids) {
|
|||
return data;
|
||||
}
|
||||
|
||||
exports.updateMemberAdditionalData = updateMemberAdditionalData;
|
||||
async function updateMemberAdditionalData(nwid, mid, data) {
|
||||
export async function updateMemberAdditionalData(nwid, mid, data) {
|
||||
if (data.config && data.config.authorized) {
|
||||
db.get("networks")
|
||||
.filter({ id: nwid })
|
||||
|
@ -160,8 +170,7 @@ async function updateMemberAdditionalData(nwid, mid, data) {
|
|||
}
|
||||
}
|
||||
|
||||
exports.deleteMemberAdditionalData = deleteMemberAdditionalData;
|
||||
async function deleteMemberAdditionalData(nwid, mid) {
|
||||
export async function deleteMemberAdditionalData(nwid, mid) {
|
||||
// ZT controller bug
|
||||
/* db.get("networks")
|
||||
.find({ id: nwid })
|
||||
|
@ -170,6 +179,8 @@ async function deleteMemberAdditionalData(nwid, mid) {
|
|||
.write();
|
||||
*/
|
||||
|
||||
await updateMemberAdditionalData(nwid, mid, {});
|
||||
|
||||
db.get("networks")
|
||||
.filter({ id: nwid })
|
||||
.map("members")
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
const _ = require("lodash");
|
||||
const axios = require("axios");
|
||||
import _ from "lodash";
|
||||
import axios from "axios";
|
||||
|
||||
const api = require("../utils/controller-api");
|
||||
const db = require("../utils/db");
|
||||
const constants = require("../utils/constants");
|
||||
import { api } from "../utils/controller-api.js";
|
||||
import { db } from "../utils/db.js";
|
||||
import { defaultRulesSource } from "../utils/constants.js";
|
||||
|
||||
async function getNetworkAdditionalData(data) {
|
||||
export async function getNetworkAdditionalData(data) {
|
||||
let additionalData = db
|
||||
.get("networks")
|
||||
.find({ id: data.id })
|
||||
|
@ -23,15 +23,13 @@ async function getNetworkAdditionalData(data) {
|
|||
|
||||
return {
|
||||
id: data.id,
|
||||
type: "Network",
|
||||
clock: Math.floor(new Date().getTime() / 1000),
|
||||
clock: new Date().getTime(),
|
||||
...additionalData.value(),
|
||||
config: data,
|
||||
};
|
||||
}
|
||||
|
||||
exports.getNetworksData = getNetworksData;
|
||||
async function getNetworksData(nwids) {
|
||||
export async function getNetworksData(nwids) {
|
||||
const prefix = "/controller/network/";
|
||||
const links = nwids.map((nwid) => prefix + nwid);
|
||||
|
||||
|
@ -55,13 +53,12 @@ async function getNetworksData(nwids) {
|
|||
return data;
|
||||
}
|
||||
|
||||
exports.createNetworkAdditionalData = createNetworkAdditionalData;
|
||||
async function createNetworkAdditionalData(nwid) {
|
||||
export async function createNetworkAdditionalData(nwid) {
|
||||
const saveData = {
|
||||
id: nwid,
|
||||
additionalConfig: {
|
||||
description: "",
|
||||
rulesSource: constants.defaultRulesSource,
|
||||
rulesSource: defaultRulesSource,
|
||||
tagsByName: {},
|
||||
capabilitiesByName: {},
|
||||
},
|
||||
|
@ -71,8 +68,7 @@ async function createNetworkAdditionalData(nwid) {
|
|||
db.get("networks").push(saveData).write();
|
||||
}
|
||||
|
||||
exports.updateNetworkAdditionalData = updateNetworkAdditionalData;
|
||||
async function updateNetworkAdditionalData(nwid, data) {
|
||||
export async function updateNetworkAdditionalData(nwid, data) {
|
||||
let additionalData = {};
|
||||
|
||||
if (data.hasOwnProperty("description")) {
|
||||
|
@ -97,7 +93,6 @@ async function updateNetworkAdditionalData(nwid, data) {
|
|||
}
|
||||
}
|
||||
|
||||
exports.deleteNetworkAdditionalData = deleteNetworkAdditionalData;
|
||||
async function deleteNetworkAdditionalData(nwid) {
|
||||
export async function deleteNetworkAdditionalData(nwid) {
|
||||
db.get("networks").remove({ id: nwid }).write();
|
||||
}
|
||||
|
|
10
backend/tsconfig.json
Normal file
10
backend/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
},
|
||||
"include": ["."]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
exports.defaultRulesSource = `
|
||||
export const defaultRulesSource = `
|
||||
# This is a default rule set that allows IPv4 and IPv6 traffic but otherwise
|
||||
# behaves like a standard Ethernet switch.
|
||||
|
||||
|
@ -26,7 +26,7 @@ drop
|
|||
accept;
|
||||
`;
|
||||
|
||||
exports.defaultRules = `
|
||||
export const defaultRules = `
|
||||
[
|
||||
{
|
||||
"type": "MATCH_ETHERTYPE",
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
const axios = require("axios");
|
||||
const fs = require("fs");
|
||||
import axios from "axios";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
|
||||
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 {
|
||||
} else if (os.platform() === "linux") {
|
||||
token = fs.readFileSync("/var/lib/zerotier-one/authtoken.secret", "utf8");
|
||||
} else {
|
||||
throw new Error("Please provide ZU_CONTROLLER_TOKEN in environment");
|
||||
}
|
||||
|
||||
module.exports = axios.create({
|
||||
export const api = axios.create({
|
||||
baseURL: baseURL,
|
||||
responseType: "json",
|
||||
headers: {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
const low = require("lowdb");
|
||||
const FileSync = require("lowdb/adapters/FileSync");
|
||||
import low from "lowdb";
|
||||
import FileSync from "lowdb/adapters/FileSync.js";
|
||||
|
||||
const adapter = new FileSync(process.env.ZU_DATAPATH || "data/db.json");
|
||||
|
||||
const db = low(adapter);
|
||||
|
||||
module.exports = db;
|
||||
export const db = low(adapter);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const crypto = require("crypto");
|
||||
const hashPassword = require("pbkdf2-wrapper/hashText");
|
||||
import crypto from "crypto";
|
||||
import hashPassword from "pbkdf2-wrapper/hashText.js";
|
||||
|
||||
module.exports = async function () {
|
||||
export async function initAdmin() {
|
||||
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);
|
||||
|
@ -13,4 +13,4 @@ module.exports = async function () {
|
|||
password_hash: hash,
|
||||
token: crypto.randomBytes(16).toString("hex"),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
31
backend/utils/ping.js
Normal file
31
backend/utils/ping.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import _ from "lodash";
|
||||
|
||||
import { api } from "./controller-api.js";
|
||||
import { db } from "./db.js";
|
||||
|
||||
export async function pingAll(network) {
|
||||
await Promise.all(
|
||||
network.members.map(async (member) => {
|
||||
console.debug("Processing member " + member.id);
|
||||
api
|
||||
.get("peer/" + member.id)
|
||||
.then(function (controllerResp) {
|
||||
if (!_.isEmpty(controllerResp.data)) {
|
||||
// write lastOnline field in db
|
||||
db.get("networks")
|
||||
.filter({ id: network.id })
|
||||
.map("members")
|
||||
.first()
|
||||
.filter({ id: member.id })
|
||||
.first()
|
||||
.set("lastOnline", new Date().getTime())
|
||||
.write();
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.debug("Couldn't fetch", member.id);
|
||||
return;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
|
@ -1,6 +1,13 @@
|
|||
const api = require("../utils/controller-api");
|
||||
import { api } from "../utils/controller-api.js";
|
||||
|
||||
module.exports = async function () {
|
||||
const res = await api.get("status");
|
||||
return res.data.address;
|
||||
};
|
||||
export async function getZTAddress() {
|
||||
try {
|
||||
const res = await api.get("status");
|
||||
return res.data.address;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
// @ts-ignore
|
||||
"Couldn't connect to the controller on " + err.config.baseURL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
version: "3.9"
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
zerotier:
|
||||
image: dec0dos/zerotier-controller:latest
|
||||
image: zyclonite/zerotier:1.10.6
|
||||
container_name: zu-controller
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/zerotier/Dockerfile
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- controller_data:/var/lib/zerotier-one
|
||||
- ./zerotier-one:/var/lib/zerotier-one
|
||||
environment:
|
||||
- ZT_OVERRIDE_LOCAL_CONF=true
|
||||
- ZT_ALLOW_MANAGEMENT_FROM=0.0.0.0/0
|
||||
expose:
|
||||
- "9993/tcp"
|
||||
ports:
|
||||
|
@ -24,8 +24,8 @@ services:
|
|||
depends_on:
|
||||
- zerotier
|
||||
volumes:
|
||||
- controller_data:/var/lib/zerotier-one
|
||||
- zero-ui_data:/app/backend/data
|
||||
- ./zerotier-one:/var/lib/zerotier-one
|
||||
- ./data:/app/backend/data
|
||||
environment:
|
||||
- ZU_CONTROLLER_ENDPOINT=http://zerotier:9993/
|
||||
- ZU_SECURE_HEADERS=true
|
||||
|
@ -39,14 +39,9 @@ services:
|
|||
restart: unless-stopped
|
||||
depends_on:
|
||||
- zero-ui
|
||||
command: caddy reverse-proxy --from example.com --to zero-ui:4000
|
||||
command: caddy reverse-proxy --from YOURDOMAIN.com --to zero-ui:4000
|
||||
volumes:
|
||||
- caddy_data:/data
|
||||
- ./caddy:/data/caddy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
volumes:
|
||||
zero-ui_data:
|
||||
controller_data:
|
||||
caddy_data:
|
||||
|
|
|
@ -1,30 +1,68 @@
|
|||
FROM node:lts-alpine as build-stage
|
||||
# Stage 1: Build frontend
|
||||
FROM --platform=$BUILDPLATFORM node:lts-alpine AS frontend-build
|
||||
|
||||
ENV INLINE_RUNTIME_CHUNK=false
|
||||
ENV GENERATE_SOURCEMAP=false
|
||||
|
||||
WORKDIR /app/frontend
|
||||
COPY ./frontend/package*.json /app/frontend
|
||||
RUN yarn install --network-timeout 1000000
|
||||
# Enable corepack and create necessary directories in one layer
|
||||
RUN corepack enable && mkdir -p /app/frontend
|
||||
|
||||
COPY ./frontend /app/frontend
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package-related files and install dependencies
|
||||
COPY tsconfig.json package.json yarn.lock* .yarnrc.yml ./
|
||||
COPY .yarn/ ./.yarn
|
||||
|
||||
# Set working directory to frontend and copy package files
|
||||
WORKDIR /app/frontend
|
||||
COPY ./frontend/package*.json /app/frontend/
|
||||
|
||||
# Install frontend dependencies and build the frontend
|
||||
RUN yarn workspaces focus frontend
|
||||
COPY ./frontend /app/frontend/
|
||||
RUN yarn build
|
||||
|
||||
# Stage 2: Build backend
|
||||
FROM node:lts-alpine AS backend-build
|
||||
|
||||
FROM node:lts-alpine
|
||||
# Enable corepack and create necessary directories in one layer
|
||||
RUN corepack enable && mkdir -p /app/backend
|
||||
|
||||
WORKDIR /app/frontend/build
|
||||
COPY --from=build-stage /app/frontend/build /app/frontend/build/
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package-related files and install dependencies
|
||||
COPY package.json yarn.lock* .yarnrc.yml ./
|
||||
COPY .yarn/ ./.yarn
|
||||
|
||||
# Set working directory to backend and copy package files
|
||||
WORKDIR /app/backend
|
||||
COPY ./backend/package*.json /app/backend
|
||||
RUN yarn install
|
||||
COPY ./backend/package*.json /app/backend/
|
||||
|
||||
# Install backend dependencies
|
||||
RUN yarn workspaces focus --production backend && yarn cache clean
|
||||
|
||||
# Copy the backend source files
|
||||
COPY ./backend /app/backend
|
||||
|
||||
EXPOSE 4000
|
||||
# Final Stage: Production
|
||||
FROM node:lts-alpine
|
||||
|
||||
# Set the working directory to /app/backend
|
||||
WORKDIR /app/backend
|
||||
|
||||
# Copy the built frontend from the frontend-build stage
|
||||
COPY --from=frontend-build /app/frontend/build /app/frontend/build
|
||||
|
||||
# Copy the backend files from the backend-build stage
|
||||
COPY --from=backend-build /app/backend /app/backend
|
||||
COPY --from=backend-build /app/node_modules /app/backend/node_modules
|
||||
|
||||
# Environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV ZU_SECURE_HEADERS=true
|
||||
ENV ZU_SERVE_FRONTEND=true
|
||||
|
||||
CMD [ "node", "./bin/www" ]
|
||||
# Expose the application port
|
||||
EXPOSE 4000
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "bin/www.js"]
|
|
@ -1,8 +0,0 @@
|
|||
FROM zyclonite/zerotier:latest
|
||||
|
||||
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"]
|
Binary file not shown.
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 82 KiB |
Binary file not shown.
Before Width: | Height: | Size: 834 KiB After Width: | Height: | Size: 477 KiB |
22
frontend/.eslintrc.json
Normal file
22
frontend/.eslintrc.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": { "browser": true, "es2020": true },
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"ignorePatterns": ["dist", ".eslintrc.cjs"],
|
||||
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
|
||||
"settings": { "react": { "version": "17.0.2" } },
|
||||
"plugins": ["react-refresh"],
|
||||
"rules": {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ "allowConstantExport": true }
|
||||
],
|
||||
"react/prop-types": ["off"],
|
||||
"no-unused-vars": ["off"]
|
||||
}
|
||||
}
|
|
@ -1,19 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="%PUBLIC_URL%/" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="icon" href="/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" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>ZeroUI</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/node_modules/*"],
|
||||
"typeAcquisition": {
|
||||
"exclude": ["dotenv", "harmony-reflect"]
|
||||
}
|
||||
}
|
|
@ -2,49 +2,55 @@
|
|||
"name": "frontend",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^4.5.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/styles": "^4.11.5",
|
||||
"@uiw/react-codemirror": "^3.1.0",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.27.2",
|
||||
"codemirror": "^5.62.3",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"history": "^5.0.1",
|
||||
"date-fns": "^2.29.2",
|
||||
"history": "^5.3.0",
|
||||
"i18next": "^23.5.1",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"i18next-http-backend": "^2.2.2",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^17.0.2",
|
||||
"react-data-table-component": "^6.11.8",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^13.3.0",
|
||||
"react-is": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-use": "^17.2.4",
|
||||
"styled-components": "^5.3.1"
|
||||
"react-use": "^17.4.0",
|
||||
"styled-components": "^5.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"source-map-explorer": "^2.5.2"
|
||||
"@types/codemirror": "^5.60.10",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/react": "^17.0.67",
|
||||
"@types/react-dom": "^17.0.21",
|
||||
"@types/react-is": "^17.0.5",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/styled-components": "^5.1.28",
|
||||
"@vitejs/plugin-react": "^4.1.0",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-static-copy": "^1.0.6"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"clean": "rimraf build",
|
||||
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"typecheck": "tsc --pretty --noEmit -p tsconfig.json",
|
||||
"analyze": "vite build --sourcemap true && source-map-explorer 'build/assets/*.js' --no-border-checks"
|
||||
}
|
||||
}
|
||||
|
|
68
frontend/public/locales/en/common.json
Normal file
68
frontend/public/locales/en/common.json
Normal file
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"flowRules": "Flow Rules",
|
||||
"createNetwork": "Create A Network",
|
||||
"createOneNetwork": "Please create at least one network",
|
||||
"controllerNetworks": "Controller networks",
|
||||
"network_one": "Network",
|
||||
"network_other": "Networks",
|
||||
"controllerAddress": "Network controller address",
|
||||
"loginToContinue": "Please, Log In to continue",
|
||||
"zerouiDesc": "ZeroUI - ZeroTier Controller Web UI - is a web user interface for a self-hosted ZeroTier network controller.",
|
||||
"logIn": "Log In",
|
||||
"logInToken": "Token Log In",
|
||||
"cancel": "Cancel",
|
||||
"management": "Management",
|
||||
"deleteNetwork": "Delete Network",
|
||||
"deleteAlert": "This action cannot be undone.",
|
||||
"deleteNetworkConfirm": "Are you sure you want to delete this network?",
|
||||
"deleteMemberConfirm": "Are you sure you want to delete this member?",
|
||||
"delete": "Delete",
|
||||
"logOut": "Log out",
|
||||
"advancedFeature": "ADVANCED FEATURE",
|
||||
"noDevices": "No devices have joined this network. Use the app on your devices to join",
|
||||
"member_one": "Member",
|
||||
"member_other": "Members",
|
||||
"addMemberManually": "Manually Add Member",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"allowBridging": "Allow Ethernet Bridging",
|
||||
"noAutoIP": "Do Not Auto-Assign IPs",
|
||||
"capabilities": "Capabilities",
|
||||
"noCapDef": "No capabilities defined",
|
||||
"tags": "Tags",
|
||||
"noTagDef": "No tags defined",
|
||||
"authorized": "Authorized",
|
||||
"address": "Address",
|
||||
"managedIPs": "Managed IPs",
|
||||
"lastSeen": "Last seen",
|
||||
"version": "Version",
|
||||
"physIp": "Physical IP",
|
||||
"latency": "Latency",
|
||||
"settings": "Settings",
|
||||
"generalSettings": "General settings",
|
||||
"networkId": "Network ID",
|
||||
"accessControl": "Access control",
|
||||
"public": "Public",
|
||||
"private": "Private",
|
||||
"managedRoutes": "Managed routes",
|
||||
"addRoute": "Add route",
|
||||
"target": "Target",
|
||||
"via": "Via",
|
||||
"start": "Start",
|
||||
"end": "End",
|
||||
"ipv4AutoAssign": "IPv4 Auto-Assign",
|
||||
"autoAssignPool": "IPv4 Auto-Assign",
|
||||
"addIPv4Pool": "Add IPv4 Pool",
|
||||
"multicastLimit": "Multicast Recipient Limit",
|
||||
"enableBroadcast": "Enable Broadcast",
|
||||
"logInFailed": "Invalid username or password",
|
||||
"tooManyAttempts": "Too many login attempts, please try again in 15 minutes.",
|
||||
"language": "Language",
|
||||
"notAuthorized": "You are not authorized. Please Log In.",
|
||||
"saveChanges": "Save changes",
|
||||
"optional": "Optional",
|
||||
"destination": "Destination",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"languageName": "English"
|
||||
}
|
68
frontend/public/locales/es-ES/common.json
Normal file
68
frontend/public/locales/es-ES/common.json
Normal file
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"flowRules": "Reglas de flujo",
|
||||
"createNetwork": "Crear una red",
|
||||
"createOneNetwork": "Por favor, crea al menos una red",
|
||||
"controllerNetworks": "Controlador de redes",
|
||||
"network_one": "Red",
|
||||
"network_other": "Redes",
|
||||
"controllerAddress": "Dirección del controlador",
|
||||
"loginToContinue": "Por favor, inicia sesión para continuar",
|
||||
"zerouiDesc": "ZeroUI - ZeroTier Controller Web UI - es una interfaz de usuario web para un controlador de red ZeroTier self-hosted.",
|
||||
"logIn": "Iniciar sesión",
|
||||
"logInToken": "Iniciar sesión con token",
|
||||
"cancel": "Cancelar",
|
||||
"management": "Gestión",
|
||||
"deleteNetwork": "Borrar red",
|
||||
"deleteAlert": "Esta acción no puede ser revertida.",
|
||||
"deleteNetworkConfirm": "¿Seguro que deseas borrar esta red?",
|
||||
"deleteMemberConfirm": "¿Seguro que deseas borrar este usuario?",
|
||||
"delete": "Borrar",
|
||||
"logOut": "Cerrar sesión",
|
||||
"advancedFeature": "CARACTERÍSTICA AVANZADA",
|
||||
"noDevices": "Ningún dispositivo se ha unido a esta red. Utilice la aplicación en sus dispositivos para unirse",
|
||||
"member_one": "Miembro",
|
||||
"member_other": "Miembros",
|
||||
"addMemberManually": "Añadir miembro manualmente",
|
||||
"name": "Nombre",
|
||||
"description": "Descripción",
|
||||
"allowBridging": "Permitir puente Ethernet",
|
||||
"noAutoIP": "No autoasignar IPs",
|
||||
"capabilities": "Permisos",
|
||||
"noCapDef": "No hay permisos definidos",
|
||||
"tags": "Etiquetas",
|
||||
"noTagDef": "No hay etiquetas definidas",
|
||||
"authorized": "Autorizado",
|
||||
"address": "Dirección",
|
||||
"managedIPs": "IPs asignadas",
|
||||
"lastSeen": "Visto por última vez",
|
||||
"version": "Versión",
|
||||
"physIp": "IP pública",
|
||||
"latency": "Latencia",
|
||||
"settings": "Ajustes",
|
||||
"generalSettings": "Ajustes generales",
|
||||
"networkId": "ID de red",
|
||||
"accessControl": "Control de acceso",
|
||||
"public": "Público",
|
||||
"private": "Privado",
|
||||
"managedRoutes": "Rutas gestionadas",
|
||||
"addRoute": "Añadir ruta",
|
||||
"target": "Objetivo",
|
||||
"via": "Vía",
|
||||
"start": "Inicio",
|
||||
"end": "Final",
|
||||
"autoAssignPool": "Rango de IPv4 autoasignables",
|
||||
"ipv4AutoAssign": "Rangos de IPv4 automáticos",
|
||||
"addIPv4Pool": "Añadir rango IPv4",
|
||||
"multicastLimit": "Límite de destinatarios multicast",
|
||||
"enableBroadcast": "Habilitar broadcast",
|
||||
"logInFailed": "Nombre de usuario o contraseña incorrecto",
|
||||
"tooManyAttempts": "Demasiados intentos de inicio de sesión. Vuelvee a intentarlo en 15 minutos",
|
||||
"language": "Idioma",
|
||||
"notAuthorized": "No estás autorizado. Por favor, inicia sesión.",
|
||||
"saveChanges": "Guardar cambios",
|
||||
"optional": "Opcional",
|
||||
"destination": "Destino",
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"languageName": "Español"
|
||||
}
|
72
frontend/public/locales/ru-RU/common.json
Normal file
72
frontend/public/locales/ru-RU/common.json
Normal file
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"flowRules": "Правила потока",
|
||||
"createNetwork": "Создать сеть",
|
||||
"createOneNetwork": "Пожалуйста, создайте хотя бы одну сеть",
|
||||
"controllerNetworks": "Сети контроллера",
|
||||
"network_one": "Сеть",
|
||||
"network_few": "Сети",
|
||||
"network_many": "Сетей",
|
||||
"network_other": "Сетей",
|
||||
"controllerAddress": "Адрес контроллера сети",
|
||||
"loginToContinue": "Пожалуйста, войдите, чтобы продолжить",
|
||||
"zerouiDesc": "ZeroUI - Веб-интерфейс контроллера ZeroTier - это веб-интерфейс для самостоятельного хостинга контроллера сети ZeroTier.",
|
||||
"logIn": "Войти",
|
||||
"logInToken": "Войти по токену",
|
||||
"cancel": "Отмена",
|
||||
"management": "Управление",
|
||||
"deleteNetwork": "Удалить сеть",
|
||||
"deleteAlert": "Это действие не может быть отменено.",
|
||||
"deleteNetworkConfirm": "Вы уверены, что хотите удалить эту сеть?",
|
||||
"deleteMemberConfirm": "Вы уверены, что хотите удалить это устройство?",
|
||||
"delete": "Удалить",
|
||||
"logOut": "Выйти",
|
||||
"advancedFeature": "РАСШИРЕННАЯ ФУНКЦИЯ",
|
||||
"noDevices": "Ни одно устройство не присоединилось к этой сети. Используйте приложение на ваших устройствах для подключения",
|
||||
"member_one": "Устройство",
|
||||
"member_few": "Устройства",
|
||||
"member_many": "Устройства",
|
||||
"member_other": "Устройств",
|
||||
"addMemberManually": "Добавить устройство вручную",
|
||||
"name": "Имя",
|
||||
"description": "Описание",
|
||||
"allowBridging": "Разрешить Ethernet мост",
|
||||
"noAutoIP": "Не назначать IP автоматически",
|
||||
"capabilities": "Возможности",
|
||||
"noCapDef": "Нет определенных возможностей",
|
||||
"tags": "Теги",
|
||||
"noTagDef": "Нет определенных тегов",
|
||||
"authorized": "Авторизован",
|
||||
"address": "Адрес",
|
||||
"managedIPs": "Управляемые IP",
|
||||
"lastSeen": "Последний раз был онлайн",
|
||||
"version": "Версия",
|
||||
"physIp": "Физический IP",
|
||||
"latency": "Задержка",
|
||||
"settings": "Настройки",
|
||||
"generalSettings": "Общие настройки",
|
||||
"networkId": "ID сети",
|
||||
"accessControl": "Контроль доступа",
|
||||
"public": "Публичный",
|
||||
"private": "Частный",
|
||||
"managedRoutes": "Управляемые маршруты",
|
||||
"addRoute": "Добавить маршрут",
|
||||
"target": "Цель",
|
||||
"via": "Через",
|
||||
"start": "Начало",
|
||||
"end": "Конец",
|
||||
"ipv4AutoAssign": "Автоматическое назначение IPv4",
|
||||
"autoAssignPool": "Автоматическое назначение IPv4",
|
||||
"addIPv4Pool": "Добавить пул IPv4",
|
||||
"multicastLimit": "Ограничение получателей мультикаста",
|
||||
"enableBroadcast": "Включить широковещание",
|
||||
"logInFailed": "Неверное имя пользователя или пароль",
|
||||
"tooManyAttempts": "Слишком много попыток входа, попробуйте снова через 15 минут.",
|
||||
"language": "Язык",
|
||||
"notAuthorized": "Вы не авторизованы. Пожалуйста, войдите.",
|
||||
"saveChanges": "Сохранить изменения",
|
||||
"optional": "Необязательно",
|
||||
"destination": "Назначение",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"languageName": "Русский"
|
||||
}
|
68
frontend/public/locales/zh_CN/common.json
Normal file
68
frontend/public/locales/zh_CN/common.json
Normal file
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"flowRules": "流量规则",
|
||||
"createNetwork": "创建网络",
|
||||
"createOneNetwork": "请至少创建一个网络",
|
||||
"controllerNetworks": "控制器网络",
|
||||
"network_one": "网络",
|
||||
"network_other": "网络们",
|
||||
"controllerAddress": "网络控制器地址",
|
||||
"loginToContinue": "请登录以继续",
|
||||
"zerouiDesc": "ZeroUI - ZeroTier 控制器Web界面 - 是一个用于自托管ZeroTier网络控制器的Web用户界面。",
|
||||
"logIn": "登录",
|
||||
"logInToken": "令牌登录",
|
||||
"cancel": "取消",
|
||||
"management": "管理",
|
||||
"deleteNetwork": "删除网络",
|
||||
"deleteAlert": "此操作无法撤销。",
|
||||
"deleteNetworkConfirm": "您确定要删除此网络吗?",
|
||||
"deleteMemberConfirm": "您确定要删除此成员吗?",
|
||||
"delete": "删除",
|
||||
"logOut": "登出",
|
||||
"advancedFeature": "高级功能",
|
||||
"noDevices": "没有设备加入此网络。请使用您的设备上的应用程序加入。",
|
||||
"member_one": "成员",
|
||||
"member_other": "成员们",
|
||||
"addMemberManually": "手动添加成员",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"allowBridging": "允许以太网桥接",
|
||||
"noAutoIP": "不自动分配IP",
|
||||
"capabilities": "能力",
|
||||
"noCapDef": "未定义能力",
|
||||
"tags": "标签",
|
||||
"noTagDef": "未定义标签",
|
||||
"authorized": "已授权",
|
||||
"address": "地址",
|
||||
"managedIPs": "管理的IP地址",
|
||||
"lastSeen": "最后看到的时间",
|
||||
"version": "版本",
|
||||
"physIp": "物理IP",
|
||||
"latency": "延迟",
|
||||
"settings": "设置",
|
||||
"generalSettings": "常规设置",
|
||||
"networkId": "网络ID",
|
||||
"accessControl": "访问控制",
|
||||
"public": "公共",
|
||||
"private": "私有",
|
||||
"managedRoutes": "管理的路由",
|
||||
"addRoute": "添加路由",
|
||||
"target": "目标",
|
||||
"via": "通过",
|
||||
"start": "开始",
|
||||
"end": "结束",
|
||||
"ipv4AutoAssign": "IPv4 自动分配",
|
||||
"autoAssignPool": "IPv4 自动分配池",
|
||||
"addIPv4Pool": "添加IPv4池",
|
||||
"multicastLimit": "多播接收者限制",
|
||||
"enableBroadcast": "启用广播",
|
||||
"logInFailed": "无效的用户名或密码",
|
||||
"tooManyAttempts": "尝试登录次数过多,请15分钟后重试。",
|
||||
"language": "语言",
|
||||
"notAuthorized": "您没有权限。请登录。",
|
||||
"saveChanges": "保存更改",
|
||||
"optional": "可选",
|
||||
"destination": "目的地",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"languageName": "中文"
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import "@fontsource/roboto";
|
||||
|
||||
import { Suspense } from "react";
|
||||
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
|
||||
|
||||
import Theme from "./components/Theme";
|
||||
|
@ -8,19 +9,27 @@ import Bar from "./components/Bar";
|
|||
import Home from "./routes/Home";
|
||||
import NotFound from "./routes/NotFound";
|
||||
import Network from "./routes/Network/Network";
|
||||
import Settings from "./routes/Settings";
|
||||
|
||||
import Loading from "./components/Loading";
|
||||
|
||||
import "./i18n";
|
||||
|
||||
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>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<BrowserRouter basename="/app">
|
||||
<Bar />
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="/network/:nwid" component={Network} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/404" component={NotFound} />
|
||||
<Redirect to="/404" />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</Suspense>
|
||||
</Theme>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,8 +19,11 @@ import MenuIcon from "@material-ui/icons/Menu";
|
|||
|
||||
import LogIn from "components/LogIn";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function Bar() {
|
||||
const [loggedIn, setLoggedIn] = useLocalStorage("loggedIn", false);
|
||||
const [disabledAuth] = useLocalStorage("disableAuth", false);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const history = useHistory();
|
||||
|
@ -40,17 +43,23 @@ function Bar() {
|
|||
history.go(0);
|
||||
};
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const menuItems = [
|
||||
// TODO: add settings page
|
||||
// {
|
||||
// name: "Settings",
|
||||
// to: "/settings",
|
||||
// },
|
||||
{
|
||||
name: "Log out",
|
||||
divide: true,
|
||||
onClick: onLogOutClick,
|
||||
name: t("settings"),
|
||||
to: "/settings",
|
||||
},
|
||||
...(!disabledAuth
|
||||
? [
|
||||
{
|
||||
name: t("logOut"),
|
||||
divide: true,
|
||||
onClick: onLogOutClick,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -72,8 +81,7 @@ function Bar() {
|
|||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{loggedIn && (
|
||||
{loggedIn && menuItems.length > 0 && (
|
||||
<>
|
||||
<Button color="inherit" onClick={openMenu}>
|
||||
<MenuIcon></MenuIcon>
|
||||
|
@ -86,7 +94,7 @@ function Bar() {
|
|||
>
|
||||
{menuItems.map((menuItem, index) => {
|
||||
if (
|
||||
menuItem.hasOwnProperty("condition") &&
|
||||
Object.prototype.hasOwnProperty.call(menuItem, "condition") &&
|
||||
!menuItem.condition
|
||||
) {
|
||||
return null;
|
||||
|
@ -111,7 +119,6 @@ function Bar() {
|
|||
key={index}
|
||||
onClick={() => {
|
||||
closeMenu();
|
||||
|
||||
menuItem.onClick();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -9,6 +9,8 @@ import NetworkButton from "./components/NetworkButton";
|
|||
import API from "utils/API";
|
||||
import { generateNetworkConfig } from "utils/NetworkConfig";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function HomeLoggedIn() {
|
||||
const [networks, setNetworks] = useState([]);
|
||||
|
||||
|
@ -30,6 +32,8 @@ function HomeLoggedIn() {
|
|||
fetchData();
|
||||
}, []);
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Button
|
||||
|
@ -38,19 +42,19 @@ function HomeLoggedIn() {
|
|||
className={classes.createBtn}
|
||||
onClick={createNetwork}
|
||||
>
|
||||
Create A Network
|
||||
{t("createNetwork")}
|
||||
</Button>
|
||||
<Divider />
|
||||
<Grid container spacing={3} className={classes.container}>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="h5">Controller networks</Typography>
|
||||
{networks[0] && "Network controller address"}
|
||||
<Typography variant="h5">{t("controllerNetworks")}</Typography>
|
||||
{networks[0] && t("controllerAddress")}
|
||||
<Box fontWeight="fontWeightBold">
|
||||
{networks[0] && networks[0]["id"].slice(0, 10)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs="auto">
|
||||
<Typography>Networks</Typography>
|
||||
<Typography>{t("network", { count: networks.length })}</Typography>
|
||||
<Grid item>
|
||||
{networks[0] ? (
|
||||
networks.map((network) => (
|
||||
|
@ -59,7 +63,7 @@ function HomeLoggedIn() {
|
|||
</Grid>
|
||||
))
|
||||
) : (
|
||||
<div>Please create at least one network</div>
|
||||
<div>{t("createOneNetwork")}</div>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
|
@ -20,6 +20,7 @@ function NetworkButton({ network }) {
|
|||
<Hidden mdDown>
|
||||
<ListItem className={classes.cidr}>
|
||||
{network["config"]["ipAssignmentPools"] &&
|
||||
network["config"]["ipAssignmentPools"][0] &&
|
||||
getCIDRAddress(
|
||||
network["config"]["ipAssignmentPools"][0]["ipRangeStart"],
|
||||
network["config"]["ipAssignmentPools"][0]["ipRangeEnd"]
|
||||
|
|
|
@ -1,6 +1,38 @@
|
|||
import { useEffect } from "react";
|
||||
import { Grid, Typography } from "@material-ui/core";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
function HomeLoggedOut() {
|
||||
const [, setLoggedIn] = useLocalStorage("loggedIn", false);
|
||||
const [, setToken] = useLocalStorage("token", null);
|
||||
const [, setDisableAuth] = useLocalStorage("disableAuth", false);
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
axios
|
||||
.get("/auth/login", { withCredentials: true })
|
||||
.then(function (response) {
|
||||
if (!response.data.enabled) {
|
||||
setLoggedIn(true);
|
||||
setDisableAuth(true);
|
||||
setToken("");
|
||||
history.go(0);
|
||||
} else {
|
||||
setDisableAuth(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
fetchData();
|
||||
}, [history, setDisableAuth, setLoggedIn, setToken]);
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
|
@ -14,14 +46,11 @@ function HomeLoggedOut() {
|
|||
>
|
||||
<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>
|
||||
<span>{t("zerouiDesc")}</span>
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
<span>Please Log In to continue</span>
|
||||
<span>{t("loginToContinue")}</span>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
19
frontend/src/components/Loading/Loading.jsx
Normal file
19
frontend/src/components/Loading/Loading.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Typography, Box, CircularProgress } from "@material-ui/core";
|
||||
|
||||
import useStyles from "./Loading.styles";
|
||||
|
||||
function Loading() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<CircularProgress color="primary" />
|
||||
<Typography variant="h6" component="div" className={classes.loadingText}>
|
||||
Loading
|
||||
<span className="loadingDots"></span>
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Loading;
|
32
frontend/src/components/Loading/Loading.styles.jsx
Normal file
32
frontend/src/components/Loading/Loading.styles.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Loading.styles.jsx
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100vh",
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: "16px",
|
||||
position: "relative",
|
||||
"& .loadingDots::after": {
|
||||
content: '"."',
|
||||
position: "absolute",
|
||||
left: "100%",
|
||||
marginLeft: "4px",
|
||||
animation: "$loadingDots 1s infinite",
|
||||
},
|
||||
},
|
||||
"@keyframes loadingDots": {
|
||||
"0%": { content: '"."' },
|
||||
"25%": { content: '".."' },
|
||||
"50%": { content: '"..."' },
|
||||
"75%": { content: '"...."' },
|
||||
"100%": { content: '"."' },
|
||||
},
|
||||
});
|
||||
|
||||
export default useStyles;
|
1
frontend/src/components/Loading/index.jsx
Normal file
1
frontend/src/components/Loading/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./Loading.jsx";
|
|
@ -6,7 +6,7 @@ import LogInToken from "./components/LogInToken";
|
|||
function LogIn() {
|
||||
return (
|
||||
<>
|
||||
{process.env.NODE_ENV === "development" && (
|
||||
{import.meta.env.DEV && (
|
||||
<>
|
||||
<LogInToken />
|
||||
<Divider orientation="vertical" />
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
DialogTitle,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function LogInToken() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [errorText, setErrorText] = useState("");
|
||||
|
@ -41,6 +43,8 @@ function LogInToken() {
|
|||
}
|
||||
};
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const LogIn = () => {
|
||||
if (token.length !== 32) {
|
||||
setErrorText("Token length error");
|
||||
|
@ -55,12 +59,12 @@ function LogInToken() {
|
|||
return (
|
||||
<div>
|
||||
<Button onClick={handleClickOpen} color="inherit" variant="outlined">
|
||||
Token Log In
|
||||
{t("logInToken")}
|
||||
</Button>
|
||||
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
|
||||
<DialogTitle>Log In</DialogTitle>
|
||||
<DialogTitle>{t("logIn")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>ADVANCED FEATURE.</DialogContentText>
|
||||
<DialogContentText>{t("advancedFeature")}</DialogContentText>
|
||||
<TextField
|
||||
value={token}
|
||||
onChange={(e) => {
|
||||
|
@ -76,10 +80,10 @@ function LogInToken() {
|
|||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button onClick={LogIn} color="primary">
|
||||
Log In
|
||||
{t("logIn")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
|
|
@ -13,10 +13,14 @@ import {
|
|||
|
||||
import axios from "axios";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function LogInUser() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
|
@ -65,17 +69,20 @@ function LogInUser() {
|
|||
.catch(function (error) {
|
||||
setPassword("");
|
||||
setSnackbarOpen(true);
|
||||
console.error(error);
|
||||
setError(error.response.data.error);
|
||||
// console.error(error.response.data.error);
|
||||
});
|
||||
};
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleClickOpen} color="primary" variant="contained">
|
||||
Log In
|
||||
{t("logIn")}
|
||||
</Button>
|
||||
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
|
||||
<DialogTitle>Log In</DialogTitle>
|
||||
<DialogTitle>{t("logIn")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
|
@ -84,7 +91,7 @@ function LogInUser() {
|
|||
setUsername(e.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
label="username"
|
||||
label={t("username")}
|
||||
type="username"
|
||||
fullWidth
|
||||
/>
|
||||
|
@ -94,17 +101,17 @@ function LogInUser() {
|
|||
setPassword(e.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
label="password"
|
||||
label={t("password")}
|
||||
type="password"
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button onClick={LogIn} color="primary">
|
||||
Log In
|
||||
{t("logIn")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
@ -114,7 +121,7 @@ function LogInUser() {
|
|||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
message="Invalid username or password"
|
||||
message={t(error)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -18,6 +18,8 @@ import DeleteIcon from "@material-ui/icons/Delete";
|
|||
|
||||
import API from "utils/API";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function NetworkManagement() {
|
||||
const { nwid } = useParams();
|
||||
const history = useHistory();
|
||||
|
@ -42,10 +44,12 @@ function NetworkManagement() {
|
|||
history.go(0);
|
||||
};
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>Management</Typography>
|
||||
<Typography>{t("management")}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Button
|
||||
|
@ -54,21 +58,19 @@ function NetworkManagement() {
|
|||
startIcon={<DeleteIcon />}
|
||||
onClick={handleClickOpen}
|
||||
>
|
||||
Delete Network
|
||||
{t("deleteNetwork")}
|
||||
</Button>
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>
|
||||
{"Are you sure you want to delete this network?"}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("deleteNetworkConfirm")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>This action cannot be undone.</DialogContentText>
|
||||
<DialogContentText>{t("deleteAlert")}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button onClick={deleteNetwork} color="secondary">
|
||||
Delete
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
|
|
@ -14,12 +14,15 @@ import DataTable from "react-data-table-component";
|
|||
import { useParams } from "react-router-dom";
|
||||
import API from "utils/API";
|
||||
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
|
||||
import { formatDistance } from "date-fns";
|
||||
import AddMember from "./components/AddMember";
|
||||
import DeleteMember from "./components/DeleteMember";
|
||||
import ManagedIP from "./components/ManagedIP";
|
||||
import MemberName from "./components/MemberName";
|
||||
import MemberSettings from "./components/MemberSettings";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function NetworkMembers({ network }) {
|
||||
const { nwid } = useParams();
|
||||
const [members, setMembers] = useState([]);
|
||||
|
@ -45,6 +48,8 @@ function NetworkMembers({ network }) {
|
|||
console.log("Action:", req);
|
||||
};
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const handleChange =
|
||||
(member, key1, key2 = null, mode = "text", id = null) =>
|
||||
(event) => {
|
||||
|
@ -66,7 +71,7 @@ function NetworkMembers({ network }) {
|
|||
const columns = [
|
||||
{
|
||||
id: "auth",
|
||||
name: "Authorized",
|
||||
name: t("authorized"),
|
||||
minWidth: "80px",
|
||||
cell: (row) => (
|
||||
<Checkbox
|
||||
|
@ -78,7 +83,7 @@ function NetworkMembers({ network }) {
|
|||
},
|
||||
{
|
||||
id: "address",
|
||||
name: "Address",
|
||||
name: t("address"),
|
||||
minWidth: "150px",
|
||||
cell: (row) => (
|
||||
<Typography variant="body2">{row.config.address}</Typography>
|
||||
|
@ -86,52 +91,52 @@ function NetworkMembers({ network }) {
|
|||
},
|
||||
{
|
||||
id: "name",
|
||||
name: "Name / Description",
|
||||
name: t("name") + "/" + t("description"),
|
||||
minWidth: "250px",
|
||||
cell: (row) => <MemberName member={row} handleChange={handleChange} />,
|
||||
},
|
||||
{
|
||||
id: "ips",
|
||||
name: "Managed IPs",
|
||||
name: t("managedIPs"),
|
||||
minWidth: "220px",
|
||||
cell: (row) => <ManagedIP member={row} handleChange={handleChange} />,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
name: "Peer status",
|
||||
id: "lastSeen",
|
||||
name: t("lastSeen"),
|
||||
minWidth: "100px",
|
||||
cell: (row) =>
|
||||
row.online === 0 ? (
|
||||
<Typography color="error">OFFLINE</Typography>
|
||||
) : row.online === 1 ? (
|
||||
<Typography style={{ color: "#008000" }}>
|
||||
{"ONLINE (v" +
|
||||
row.config.vMajor +
|
||||
"." +
|
||||
row.config.vMinor +
|
||||
"." +
|
||||
row.config.vRev +
|
||||
")"}
|
||||
row.online === 1 ? (
|
||||
<Typography style={{ color: "#008000" }}>{"ONLINE"}</Typography>
|
||||
) : row.controllerId === row.config.address ? (
|
||||
<Typography style={{ color: "#c5e31e" }}>{"CONTROLLER"}</Typography>
|
||||
) : row.online === 0 ? (
|
||||
<Typography color="error">
|
||||
{row.lastOnline !== 0
|
||||
? formatDistance(row.lastOnline, row.clock, {
|
||||
includeSeconds: false,
|
||||
addSuffix: true,
|
||||
})
|
||||
: "OFFLINE"}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography style={{ color: "#f1c232" }}>
|
||||
{"RELAYED (v" +
|
||||
row.config.vMajor +
|
||||
"." +
|
||||
row.config.vMinor +
|
||||
"." +
|
||||
row.config.vRev +
|
||||
")"}
|
||||
</Typography>
|
||||
<Typography style={{ color: "#f1c232" }}>{"RELAYED"}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "physicalip",
|
||||
name: "Physical IP / Latency",
|
||||
name: t("version") + " / " + t("physIp") + " / " + t("latency"),
|
||||
minWidth: "220px",
|
||||
cell: (row) =>
|
||||
row.online === 1 ? (
|
||||
<p>
|
||||
{"v" +
|
||||
row.config.vMajor +
|
||||
"." +
|
||||
row.config.vMinor +
|
||||
"." +
|
||||
row.config.vRev}
|
||||
<br />
|
||||
{row.physicalAddress + "/" + row.physicalPort}
|
||||
<br />
|
||||
{"(" + row.latency + " ms)"}
|
||||
|
@ -142,7 +147,7 @@ function NetworkMembers({ network }) {
|
|||
},
|
||||
{
|
||||
id: "delete",
|
||||
name: "",
|
||||
name: t("settings"),
|
||||
minWidth: "50px",
|
||||
right: true,
|
||||
cell: (row) => (
|
||||
|
@ -161,7 +166,7 @@ function NetworkMembers({ network }) {
|
|||
return (
|
||||
<Accordion defaultExpanded={true}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>Members</Typography>
|
||||
<Typography>{t("member", { count: members.length })}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container direction="column" spacing={3}>
|
||||
|
@ -181,14 +186,13 @@ function NetworkMembers({ network }) {
|
|||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
justifyContent="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>.
|
||||
{t("noDevices")} <b>{nwid}</b>.
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
|
|
@ -5,6 +5,8 @@ import AddIcon from "@material-ui/icons/Add";
|
|||
|
||||
import API from "utils/API";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function AddMember({ nwid, callback }) {
|
||||
const [member, setMember] = useState("");
|
||||
|
||||
|
@ -24,9 +26,11 @@ function AddMember({ nwid, callback }) {
|
|||
setMember("");
|
||||
};
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>Manually Add Member</Typography>
|
||||
<Typography>{t("addMemberManually")}</Typography>
|
||||
<List
|
||||
disablePadding={true}
|
||||
style={{
|
||||
|
|
|
@ -12,8 +12,10 @@ import {
|
|||
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
|
||||
|
||||
import API from "utils/API";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function DeleteMember({ nwid, mid, callback }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClickOpen = () => {
|
||||
|
@ -37,18 +39,16 @@ function DeleteMember({ nwid, mid, callback }) {
|
|||
<DeleteOutlineIcon color="secondary" style={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>
|
||||
{"Are you sure you want to delete this member?"}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("deleteMemberConfirm")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>This action cannot be undone.</DialogContentText>
|
||||
<DialogContentText>{t("deleteAlert")}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button onClick={deleteMemberReq} color="secondary">
|
||||
Delete
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { Grid, TextField } from "@material-ui/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function MemberName({ member, handleChange }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
return (
|
||||
<Grid>
|
||||
<TextField
|
||||
value={member.name}
|
||||
onChange={handleChange(member, "name")}
|
||||
label="Name"
|
||||
label={t("name")}
|
||||
variant="filled"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
|
@ -15,7 +17,7 @@ function MemberName({ member, handleChange }) {
|
|||
<TextField
|
||||
value={member.description}
|
||||
onChange={handleChange(member, "description")}
|
||||
label="Description"
|
||||
label={t("description")}
|
||||
variant="filled"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
|
|
|
@ -13,7 +13,10 @@ import BuildIcon from "@material-ui/icons/Build";
|
|||
import { useState } from "react";
|
||||
import Tag from "./components/Tag";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function MemberSettings({ member, network, handleChange }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClickOpen = () => {
|
||||
|
@ -30,7 +33,9 @@ function MemberSettings({ member, network, handleChange }) {
|
|||
<BuildIcon style={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>{"Member " + member.config.id + " settings"}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{t("member") + member.config.id + t("settings")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid item>
|
||||
<Checkbox
|
||||
|
@ -43,7 +48,7 @@ function MemberSettings({ member, network, handleChange }) {
|
|||
"checkbox"
|
||||
)}
|
||||
/>
|
||||
<span>Allow Ethernet Bridging</span>
|
||||
<span>{t("allowBridging")}</span>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Checkbox
|
||||
|
@ -56,17 +61,17 @@ function MemberSettings({ member, network, handleChange }) {
|
|||
"checkbox"
|
||||
)}
|
||||
/>
|
||||
<span>Do Not Auto-Assign IPs</span>
|
||||
<span>{t("noAutoIP")}</span>
|
||||
</Grid>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Capabilities</Typography>
|
||||
<Typography variant="h6">{t("capabilities")}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Paper style={{ padding: 20 }}>
|
||||
{Object.entries(network["capabilitiesByName"] || []).length ===
|
||||
0
|
||||
? "No capabilities defined"
|
||||
? t("noCapDef")
|
||||
: ""}
|
||||
{Object.entries(network["capabilitiesByName"] || []).map(
|
||||
([capName, capId]) => (
|
||||
|
@ -96,11 +101,11 @@ function MemberSettings({ member, network, handleChange }) {
|
|||
</Grid>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Tags</Typography>
|
||||
<Typography variant="h6">{t("tags")}</Typography>
|
||||
</Grid>
|
||||
{Object.entries(network["tagsByName"] || []).length === 0 ? (
|
||||
<Grid item xs={12}>
|
||||
<Paper style={{ padding: 20 }}>No tags defined</Paper>
|
||||
<Paper style={{ padding: 20 }}>{t("noTagDef")}</Paper>
|
||||
</Grid>
|
||||
) : (
|
||||
""
|
||||
|
|
|
@ -17,7 +17,11 @@ import debounce from "lodash/debounce";
|
|||
import { useState } from "react";
|
||||
import API from "utils/API";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function NetworkRules({ network, callback }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [flowData, setFlowData] = useState({
|
||||
rules: [...network.config.rules],
|
||||
|
@ -87,12 +91,12 @@ function NetworkRules({ network, callback }) {
|
|||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>Flow Rules</Typography>
|
||||
<Typography>{t("flowRules")}</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
|
||||
No need to update on every user character input Flow Rules
|
||||
*/}
|
||||
<CodeMirror
|
||||
value={network["rulesSource"]}
|
||||
|
@ -124,13 +128,13 @@ function NetworkRules({ network, callback }) {
|
|||
width: "250px",
|
||||
}}
|
||||
>
|
||||
{!!errors.length ? (
|
||||
{errors.length ? (
|
||||
<Typography color="error">
|
||||
{"[" + errors[0] + ":" + errors[1] + "] " + errors[2]}
|
||||
</Typography>
|
||||
) : (
|
||||
<Button variant="contained" color="primary" onClick={saveChanges}>
|
||||
Save Changes
|
||||
{t("saveChanges")}
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
|
|
|
@ -17,7 +17,10 @@ import IPv4AutoAssign from "./components/IPv4AutoAssign";
|
|||
import API from "utils/API";
|
||||
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function NetworkSettings({ network, setNetwork }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const sendReq = async (data) => {
|
||||
try {
|
||||
const req = await API.post("/network/" + network["config"]["id"], data);
|
||||
|
@ -43,12 +46,12 @@ function NetworkSettings({ network, setNetwork }) {
|
|||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>General settings</Typography>
|
||||
<Typography>{t("generalSettings")}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container direction="column" spacing={3}>
|
||||
<Grid item>
|
||||
<Typography>Network ID</Typography>
|
||||
<Typography>{t("networkId")}</Typography>
|
||||
<Typography variant="h5">
|
||||
<span>{network["config"]["id"]}</span>
|
||||
</Typography>
|
||||
|
@ -57,7 +60,7 @@ function NetworkSettings({ network, setNetwork }) {
|
|||
<TextField
|
||||
value={network["config"]["name"]}
|
||||
onChange={handleChange("config", "name")}
|
||||
label="Name"
|
||||
label={t("name")}
|
||||
variant="filled"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
|
@ -69,9 +72,9 @@ function NetworkSettings({ network, setNetwork }) {
|
|||
value={network["description"]}
|
||||
onChange={handleChange("description")}
|
||||
multiline
|
||||
rows={2}
|
||||
rowsMax={Infinity}
|
||||
label="Description"
|
||||
minRows={2}
|
||||
maxRows={Infinity}
|
||||
label={t("description")}
|
||||
variant="filled"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
|
@ -80,14 +83,14 @@ function NetworkSettings({ network, setNetwork }) {
|
|||
</Grid>
|
||||
<Divider />
|
||||
<Grid item>
|
||||
<Typography>Access Control</Typography>
|
||||
<Typography>{t("accessControl")}</Typography>
|
||||
<Select
|
||||
native
|
||||
value={network["config"]["private"]}
|
||||
onChange={handleChange("config", "private", "json")}
|
||||
>
|
||||
<option value={true}>Private</option>
|
||||
<option value={false}>Public</option>
|
||||
<option value={1}>{t("private")}</option>
|
||||
<option value={0}>{t("public")}</option>
|
||||
</Select>
|
||||
</Grid>
|
||||
<Divider />
|
||||
|
@ -111,7 +114,7 @@ function NetworkSettings({ network, setNetwork }) {
|
|||
<Divider />
|
||||
<Grid item>
|
||||
<TextField
|
||||
label="Multicast Recipient Limit"
|
||||
label={t("multicastLimit")}
|
||||
type="number"
|
||||
value={network["config"]["multicastLimit"]}
|
||||
onChange={handleChange("config", "multicastLimit", "json")}
|
||||
|
@ -126,7 +129,7 @@ function NetworkSettings({ network, setNetwork }) {
|
|||
color="primary"
|
||||
onChange={handleChange("config", "enableBroadcast", "checkbox")}
|
||||
/>
|
||||
<span>Enable Broadcast</span>
|
||||
<span>{t("enableBroadcast")}</span>
|
||||
</Grid>
|
||||
{/* TODO: */}
|
||||
{/* <Grid item>
|
||||
|
|
|
@ -18,7 +18,10 @@ import DataTable from "react-data-table-component";
|
|||
import { addressPool } from "utils/NetworkConfig";
|
||||
import { getCIDRAddress, validateIP, normilizeIP } from "utils/IP";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [start, setStart] = useState("");
|
||||
const [end, setEnd] = useState("");
|
||||
|
||||
|
@ -89,19 +92,19 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
|
|||
},
|
||||
{
|
||||
id: "Start",
|
||||
name: "Start",
|
||||
name: t("start"),
|
||||
cell: (row) => row["ipRangeStart"],
|
||||
},
|
||||
{
|
||||
id: "End",
|
||||
name: "End",
|
||||
name: t("end"),
|
||||
cell: (row) => row["ipRangeEnd"],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>IPv4 Auto-Assign</Typography>
|
||||
<Typography>{t("ipv4AutoAssign")}</Typography>
|
||||
<div
|
||||
style={{
|
||||
padding: "30px",
|
||||
|
@ -122,7 +125,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
|
|||
</Grid>
|
||||
</div>
|
||||
<Typography style={{ paddingBottom: "10px" }}>
|
||||
Auto-Assign Pools
|
||||
{t("autoAssignPool")}
|
||||
</Typography>
|
||||
<Box border={1} borderColor="grey.300">
|
||||
<Grid item style={{ margin: "10px" }}>
|
||||
|
@ -132,7 +135,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
|
|||
data={ipAssignmentPools}
|
||||
/>
|
||||
<Divider />
|
||||
<Typography>Add IPv4 Pool</Typography>
|
||||
<Typography>{t("addIPv4Pool")}</Typography>
|
||||
<List
|
||||
style={{
|
||||
display: "flex",
|
||||
|
@ -142,7 +145,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
|
|||
<TextField
|
||||
value={start}
|
||||
onChange={handleStartInput}
|
||||
placeholder={"Start"}
|
||||
placeholder={t("start")}
|
||||
/>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
|
@ -154,7 +157,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
|
|||
<TextField
|
||||
value={end}
|
||||
onChange={handleEndInput}
|
||||
placeholder={"End"}
|
||||
placeholder={t("end")}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
|
|
|
@ -16,7 +16,10 @@ import DataTable from "react-data-table-component";
|
|||
|
||||
import { validateIP, normilizeIP, validateCIDR } from "utils/IP";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function ManagedRoutes({ routes, handleChange }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [destination, setDestination] = useState("");
|
||||
const [via, setVia] = useState("");
|
||||
|
||||
|
@ -71,12 +74,12 @@ function ManagedRoutes({ routes, handleChange }) {
|
|||
},
|
||||
{
|
||||
id: "target",
|
||||
name: "Target",
|
||||
name: t("target"),
|
||||
cell: (row) => row["target"],
|
||||
},
|
||||
{
|
||||
id: "via",
|
||||
name: "via",
|
||||
name: t("via"),
|
||||
cell: (row) => (row["via"] ? row["via"] : "(LAN)"),
|
||||
},
|
||||
];
|
||||
|
@ -84,13 +87,13 @@ function ManagedRoutes({ routes, handleChange }) {
|
|||
return (
|
||||
<>
|
||||
<Typography style={{ paddingBottom: "10px" }}>
|
||||
Managed Routes ({routes.length + "/32"})
|
||||
{t("managedRoutes")} ({routes.length + "/128"})
|
||||
</Typography>
|
||||
<Box border={1} borderColor="grey.300">
|
||||
<Grid item style={{ margin: "10px" }}>
|
||||
<DataTable noHeader={true} columns={columns} data={routes} />
|
||||
<Divider />
|
||||
<Typography>Add Routes</Typography>
|
||||
<Typography>{t("addRoute")}</Typography>
|
||||
<List
|
||||
style={{
|
||||
display: "flex",
|
||||
|
@ -100,7 +103,7 @@ function ManagedRoutes({ routes, handleChange }) {
|
|||
<TextField
|
||||
value={destination}
|
||||
onChange={handleDestinationInput}
|
||||
placeholder={"Destination (CIDR)"}
|
||||
placeholder={t("destination") + " (CIDR)"}
|
||||
/>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
|
@ -112,7 +115,7 @@ function ManagedRoutes({ routes, handleChange }) {
|
|||
<TextField
|
||||
value={via}
|
||||
onChange={handleViaInput}
|
||||
placeholder={"Via (Optional)"}
|
||||
placeholder={t("via") + " (" + t("optional") + ")"}
|
||||
/>
|
||||
<IconButton size="small" color="primary" onClick={addRouteReq}>
|
||||
<AddIcon
|
||||
|
|
41
frontend/src/components/Settings/Settings.jsx
Normal file
41
frontend/src/components/Settings/Settings.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Grid,
|
||||
Typography,
|
||||
Select,
|
||||
} from "@material-ui/core";
|
||||
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import localesList from "generated/localesList.json";
|
||||
|
||||
function Settings() {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const handleChange = () => (event) => {
|
||||
i18n.changeLanguage(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>{t("language")}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid item>
|
||||
<Select native value={i18n.language} onChange={handleChange()}>
|
||||
{localesList.map((locale) => (
|
||||
<option key={locale.code} value={locale.code}>
|
||||
{locale.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
1
frontend/src/components/Settings/index.jsx
Normal file
1
frontend/src/components/Settings/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./Settings";
|
1
frontend/src/external/RuleCompiler.js
vendored
1
frontend/src/external/RuleCompiler.js
vendored
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
// rule-compiler.js from ZeroTierOne before its relicensing
|
||||
// source:
|
||||
|
|
18
frontend/src/generated/localesList.json
Normal file
18
frontend/src/generated/localesList.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
[
|
||||
{
|
||||
"code": "en",
|
||||
"name": "English"
|
||||
},
|
||||
{
|
||||
"code": "es-ES",
|
||||
"name": "Español"
|
||||
},
|
||||
{
|
||||
"code": "ru-RU",
|
||||
"name": "Русский"
|
||||
},
|
||||
{
|
||||
"code": "zh_CN",
|
||||
"name": "中文"
|
||||
}
|
||||
]
|
35
frontend/src/i18n.js
Normal file
35
frontend/src/i18n.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import i18n from "i18next";
|
||||
import languageDetector from "i18next-browser-languagedetector";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
|
||||
import localesList from "./utils/localesList.json";
|
||||
const supportedLngs = localesList.map((locale) => locale.code);
|
||||
|
||||
i18n
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.use(Backend)
|
||||
.init({
|
||||
compatibilityJSON: "v4",
|
||||
fallbackLng: "en",
|
||||
detection: {
|
||||
order: ["path", "cookie", "localStorage", "htmlTag"],
|
||||
caches: ["localStorage", "cookie"],
|
||||
},
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: true,
|
||||
},
|
||||
react: {
|
||||
useSuspense: true,
|
||||
},
|
||||
supportedLngs,
|
||||
backend: {
|
||||
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||
},
|
||||
ns: ["common"],
|
||||
defaultNS: "common",
|
||||
});
|
||||
|
||||
export default i18n;
|
|
@ -1,8 +1,17 @@
|
|||
body {
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
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;
|
||||
|
|
|
@ -11,7 +11,10 @@ import { useLocalStorage } from "react-use";
|
|||
import API from "utils/API";
|
||||
import useStyles from "./Network.styles";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function Network() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { nwid } = useParams();
|
||||
const [loggedIn] = useLocalStorage("loggedIn", false);
|
||||
const [network, setNetwork] = useState({});
|
||||
|
@ -39,10 +42,12 @@ function Network() {
|
|||
if (loggedIn) {
|
||||
return (
|
||||
<>
|
||||
<Link color="inherit" component={RouterLink} to="/" underline="none">
|
||||
<ArrowBackIcon className={classes.backIcon}></ArrowBackIcon>
|
||||
Networks
|
||||
</Link>
|
||||
<div className={classes.breadcrumbs}>
|
||||
<Link color="inherit" component={RouterLink} to="/" underline="none">
|
||||
<ArrowBackIcon className={classes.backIcon}></ArrowBackIcon>
|
||||
{t("network", { count: 2 })}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.container}>
|
||||
{network["config"] && (
|
||||
<>
|
||||
|
@ -71,9 +76,7 @@ function Network() {
|
|||
}}
|
||||
>
|
||||
<Grid item xs={10}>
|
||||
<Typography variant="h5">
|
||||
You are not authorized. Please Log In
|
||||
</Typography>
|
||||
<Typography variant="h5">{t("notAuthorized")}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,11 @@ const useStyles = makeStyles((theme) => ({
|
|||
fontSize: 12,
|
||||
},
|
||||
container: {
|
||||
margin: "1%",
|
||||
margin: "3%",
|
||||
},
|
||||
breadcrumbs: {
|
||||
paddingTop: "2%",
|
||||
paddingLeft: "2%",
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
52
frontend/src/routes/Settings/Settings.jsx
Normal file
52
frontend/src/routes/Settings/Settings.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Grid, Link, Typography } from "@material-ui/core";
|
||||
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
|
||||
import SettingsComponent from "components/Settings";
|
||||
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { useLocalStorage } from "react-use";
|
||||
|
||||
import useStyles from "./Settings.styles";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function Settings() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [loggedIn] = useLocalStorage("loggedIn", false);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
if (loggedIn) {
|
||||
return (
|
||||
<>
|
||||
<div className={classes.breadcrumbs}>
|
||||
<Link color="inherit" component={RouterLink} to="/" underline="none">
|
||||
<ArrowBackIcon className={classes.backIcon}></ArrowBackIcon>
|
||||
{t("settings")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.container}>
|
||||
<SettingsComponent />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{
|
||||
minHeight: "50vh",
|
||||
}}
|
||||
>
|
||||
<Grid item xs={10}>
|
||||
<Typography variant="h5">{t("notAuthorized")}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Settings;
|
16
frontend/src/routes/Settings/Settings.styles.jsx
Normal file
16
frontend/src/routes/Settings/Settings.styles.jsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
backIcon: {
|
||||
fontSize: 12,
|
||||
},
|
||||
container: {
|
||||
margin: "3%",
|
||||
},
|
||||
breadcrumbs: {
|
||||
paddingTop: "2%",
|
||||
paddingLeft: "2%",
|
||||
},
|
||||
}));
|
||||
|
||||
export default useStyles;
|
1
frontend/src/routes/Settings/index.jsx
Normal file
1
frontend/src/routes/Settings/index.jsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "./Settings";
|
1
frontend/src/types.d.ts
vendored
Normal file
1
frontend/src/types.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module "*.png";
|
|
@ -5,7 +5,11 @@ const baseURL = "/api/";
|
|||
export default axios.create({
|
||||
baseURL: baseURL,
|
||||
responseType: "json",
|
||||
headers: {
|
||||
Authorization: `Bearer ${JSON.parse(localStorage.getItem("token"))}`,
|
||||
},
|
||||
withCredentials: "true",
|
||||
headers:
|
||||
localStorage.getItem("disableAuth") === "true"
|
||||
? {}
|
||||
: {
|
||||
Authorization: `token ${JSON.parse(localStorage.getItem("token"))}`,
|
||||
},
|
||||
});
|
||||
|
|
14
frontend/src/utils/localesList.json
Normal file
14
frontend/src/utils/localesList.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"code": "en",
|
||||
"name": "English"
|
||||
},
|
||||
{
|
||||
"code": "es-ES",
|
||||
"name": "Español"
|
||||
},
|
||||
{
|
||||
"code": "ru-RU",
|
||||
"name": "Русский"
|
||||
}
|
||||
]
|
12
frontend/tsconfig.json
Normal file
12
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"baseUrl": "src",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
51
frontend/vite-plugin-generate-locales.js
Normal file
51
frontend/vite-plugin-generate-locales.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import * as url from "url";
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default function generateLocalesPlugin() {
|
||||
return {
|
||||
name: "generate-locales",
|
||||
buildStart() {
|
||||
const localesDir = path.resolve(__dirname, "public", "locales");
|
||||
|
||||
if (fs.existsSync(localesDir)) {
|
||||
const localesList = fs
|
||||
.readdirSync(localesDir)
|
||||
.filter((file) => {
|
||||
return fs.statSync(path.join(localesDir, file)).isDirectory();
|
||||
})
|
||||
.map((locale) => {
|
||||
const commonFilePath = path.join(localesDir, locale, "common.json");
|
||||
if (fs.existsSync(commonFilePath)) {
|
||||
const commonFile = JSON.parse(
|
||||
fs.readFileSync(commonFilePath, "utf-8")
|
||||
);
|
||||
return {
|
||||
code: locale,
|
||||
name: commonFile.languageName || locale,
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: locale,
|
||||
name: locale,
|
||||
};
|
||||
});
|
||||
|
||||
// Save the array to a JSON file
|
||||
const outputPath = path.resolve(
|
||||
__dirname,
|
||||
"src",
|
||||
"generated",
|
||||
"localesList.json"
|
||||
);
|
||||
fs.writeFileSync(outputPath, JSON.stringify(localesList, null, 2));
|
||||
|
||||
console.log(`Locales list saved to ${outputPath}`);
|
||||
} else {
|
||||
console.error("Locales directory not found.");
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
45
frontend/vite.config.mjs
Normal file
45
frontend/vite.config.mjs
Normal file
|
@ -0,0 +1,45 @@
|
|||
import process from "node:process";
|
||||
import { defineConfig, searchForWorkspaceRoot } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import generateLocalesPlugin from "./vite-plugin-generate-locales.js";
|
||||
|
||||
export default defineConfig({
|
||||
base: "/app",
|
||||
server: {
|
||||
port: 3000,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
"/auth": "http://127.0.0.1:4000",
|
||||
"/api": "http://127.0.0.1:4000",
|
||||
"/controller": "http://127.0.0.1:4000",
|
||||
},
|
||||
fs: {
|
||||
allow: [searchForWorkspaceRoot(process.cwd()), "../node_modules"],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
components: "/src/components",
|
||||
utils: "/src/utils",
|
||||
external: "/src/external",
|
||||
generated: "/src/generated",
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "build",
|
||||
chunkSizeWarningLimit: 1000,
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
generateLocalesPlugin(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: "public/locales",
|
||||
dest: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
40
package.json
40
package.json
|
@ -1,35 +1,39 @@
|
|||
{
|
||||
"name": "zero-ui",
|
||||
"version": "1.2.1",
|
||||
"version": "1.5.8",
|
||||
"workspaces": [
|
||||
"backend",
|
||||
"frontend"
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "husky install",
|
||||
"installDeps": "cd frontend && yarn install && cd ../backend && yarn install",
|
||||
"upgradeDeps": "yarn upgrade-interactive",
|
||||
"cleanDeps": "cd frontend && rimraf node_modules && cd ../backend && rimraf node_modules && cd .. && rimraf node_modules",
|
||||
"lint": "yarn prettier --write .",
|
||||
"upgrade:deps": "yarn upgrade-interactive",
|
||||
"upgrade:yarn": "yarn set version latest",
|
||||
"clean:deps": "cd frontend && rimraf node_modules && cd ../backend && rimraf node_modules && cd .. && rimraf node_modules",
|
||||
"format": "yarn prettier --check .",
|
||||
"format:fix": "yarn prettier --write .",
|
||||
"lint": "yarn workspaces foreach --all --parallel run lint",
|
||||
"dev": "concurrently \"cd frontend && cross-env FAST_REFRESH=true yarn start\" \"cd backend && cross-env NODE_ENV=development ZU_DEFAULT_USERNAME=admin ZU_DEFAULT_PASSWORD=zero-ui nodemon ./bin/www --ignore data/db.json\"",
|
||||
"build": "cd frontend && cross-env INLINE_RUNTIME_CHUNK=false GENERATE_SOURCEMAP=false yarn build",
|
||||
"build": "cd frontend && cross-env GENERATE_SOURCEMAP=false yarn build",
|
||||
"prod": "cd backend && cross-env NODE_ENV=production ZU_SECURE_HEADERS=false yarn start",
|
||||
"release": "standard-version && git push --follow-tags origin main",
|
||||
"docker:build": "docker build . -t dec0dos/zero-ui -f docker/zero-ui/Dockerfile --progress=plain",
|
||||
"docker:run": "docker run --rm --env-file .env -e ZU_CONTROLLER_ENDPOINT=http://host.docker.internal:9993 -p 4000:4000 --name zero-ui dec0dos/zero-ui",
|
||||
"release": "standard-version && git push --follow-tags origin main && git add CHANGELOG.md",
|
||||
"commit": "yarn git-cz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^15.0.0",
|
||||
"@commitlint/config-conventional": "^15.0.0",
|
||||
"commitizen": "^4.2.4",
|
||||
"concurrently": "^6.2.1",
|
||||
"@commitlint/cli": "^17.7.2",
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"concurrently": "^8.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.1.3",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "^2.5.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"standard-version": "^9.3.1"
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"standard-version": "^9.5.0"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
|
@ -52,5 +56,5 @@
|
|||
"yarn prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@3.2.0-rc.8"
|
||||
"packageManager": "yarn@4.3.1"
|
||||
}
|
||||
|
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext", "dom"],
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue