Compare commits

...

138 commits
v1.4.0 ... main

Author SHA1 Message Date
Aliaksei
6c2654ea8c
Merge pull request #217 from chenxudong2020/main
Add Chinese language support
2024-10-24 15:07:36 +01:00
chenxudong2020
c510ed3e0b add zh_CN language 2024-10-24 21:31:39 +08:00
dec0dOS
94581a8999 fix: locales serving 2024-07-09 19:58:54 +01:00
dec0dOS
2b79e9dd80 chore: improve Dockerfile 2024-07-09 19:57:30 +01:00
dec0dOS
568b411b28 chore: add initial logging info 2024-07-09 19:31:41 +01:00
dec0dOS
95767e8373 fix: fix russian translation 2024-07-09 16:23:12 +01:00
dec0dOS
3ca5aa1898 feat: add russian translation 2024-07-09 16:19:17 +01:00
dec0dOS
ff708f5f20 chore: improve error handler for token 2024-07-09 16:18:52 +01:00
dec0dOS
85e8a0265b chore: upgrade yarn 2024-07-09 16:18:17 +01:00
dec0dOS
00592d9ad4 chore: fix prettier check script 2024-05-01 14:17:23 +01:00
dec0dOS
bdd2f068fe chore: run prettier 2024-05-01 14:16:43 +01:00
dec0dOS
29127f0433 chore: fix merge errors 2024-05-01 14:14:31 +01:00
Aliaksei
0fb92649df
Merge pull request #188 from aruznieto/i18n
feat: i18n - dynamic import on build time
2024-05-01 14:11:34 +01:00
dec0dOS
d826cda2a0 chore: move localesList to generated folder 2024-05-01 14:11:05 +01:00
dec0dOS
5b1ad30e98 chore: fix path to localesList 2024-05-01 14:05:58 +01:00
dec0dOS
6f7bc6c2c2 chore: upgrade yarn to v4 2023-10-25 17:13:21 +01:00
dec0dOS
ba4e5f2001 fix: vite-plugin-generate-locales.js 2023-10-21 14:48:53 +01:00
Andres
beec9d7d78 feat: i18n - yourLanguage - languageName 2023-10-21 07:38:18 +02:00
Andres
f5e257e9c8 feat: i18n package.json 2023-10-21 07:34:02 +02:00
Andres
7db15c26ab feat: i18n - vite plugin 2023-10-21 07:32:15 +02:00
Aliaksei
fa1a687147
Merge pull request #187 from aruznieto/loading-page
feat: loading-page for suspense
2023-10-20 18:05:39 +01:00
dec0dOS
5be92007b7 fix: apply theme before showing the loading screen 2023-10-20 18:00:33 +01:00
Andres
6978495963 feat: dynamic import on build time 2023-10-20 11:10:45 +02:00
Andres
b8a5a37fc7 feat: loading-page for suspense 2023-10-20 10:47:19 +02:00
Aliaksei
4a5f8469b3
Merge pull request #182 from aruznieto/i18n
feat: i18n
2023-10-19 19:52:38 +01:00
Andres
ee73374c29 feat: i18n 2023-10-19 15:15:19 +02:00
Andres
2cf3a64cc8 feat: i18n managedroutes 2023-10-19 14:43:18 +02:00
Andres
83502755d7 feat: i18n words 2023-10-19 14:31:20 +02:00
Andres
3d67022b38 feat: i18n fix 2023-10-19 13:18:38 +02:00
Andres
d19bf07382 feat: i18n persistence on logout 2023-10-17 20:24:02 +02:00
Andres
80f1ab4185 feat: i18n disabling debug mode 2023-10-17 18:06:49 +02:00
Andres
48485fc546 feat: i18n add settings page 2023-10-17 18:05:02 +02:00
dec0dOS
213c9499f2 fix: login limiter, make it opt it by default 2023-10-16 17:47:32 +01:00
dec0dOS
f17067f832 chore: some fixes after esm module migration 2023-10-16 17:45:07 +01:00
Andres
61fd0e7186 feat: i18n 2023-10-16 17:34:52 +02:00
Aliaksei
60ddb2f9dd
Merge pull request #179 from aruznieto/feat-rateLimiter
feat: rate-limiter
2023-10-15 21:45:29 +01:00
dec0dOS
41f12ad2f3 chore: small changes 2023-10-15 21:41:52 +01:00
Andres
d65e6fb71a feat: rate-limiter 2023-10-15 18:33:17 +02:00
Andres
569384038a feat: rate-limiter 2023-10-15 18:12:50 +02:00
Aliaksei
af9d6447e7
Merge branch 'main' into feat-rateLimiter 2023-10-15 16:44:01 +01:00
dec0dOS
9b1ac15b46 chore: update .gitignore 2023-10-15 16:39:00 +01:00
Andres
7dd6f3729b feat: login-limiter 2023-10-15 10:41:19 +02:00
Aliaksei
efb37caab2
Merge pull request #178 from aruznieto/imgbot
[ImgBot] Optimize images
2023-10-15 04:44:20 +01:00
Andrés Ruz Nieto
60777f5165 feat:rateLimiter 2023-10-13 17:02:50 +02:00
dec0dOS
559a98850e chore: add missing frontend types 2023-10-08 17:12:54 +01:00
dec0dOS
91176a2ead chore: fix backend types 2023-10-08 17:12:31 +01:00
dec0dOS
986be8d8a0 chore: add missing backend types 2023-10-08 17:10:11 +01:00
dec0dOS
8a2ba8d1b1 chore: upgrade deps 2023-10-08 17:07:39 +01:00
dec0dOS
e83bbef742 docs: update README 2023-10-08 17:03:03 +01:00
Aliaksei
0521c9dea0
Merge pull request #170 from aruznieto/es6-visual-mods
dev: es6 + visual mods + migrate from node-cron to croner
2023-10-08 16:41:22 +01:00
dec0dOS
c53beabd66 chore: fix yarn.lock 2023-10-08 16:37:29 +01:00
Andres
a22e69e6cb feat: remove files 2023-10-08 17:26:38 +02:00
Andres
ea828c326f feat: es6+mods and replace node-cron with croner 2023-10-08 16:37:26 +02:00
ImgBotApp
4592671d21
[ImgBot] Optimize images
*Total -- 997.02kb -> 558.88kb (43.94%)

/docs/images/homepage.png -- 162.54kb -> 81.46kb (49.88%)
/docs/images/network.png -- 834.49kb -> 477.42kb (42.79%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2023-10-07 15:09:34 +00:00
dec0dOS
b85bb7299b docs: update README link 2023-10-07 01:13:09 +01:00
dec0dOS
abcf031f81 chore: fix vite assets resolution 2023-10-06 23:07:52 +01:00
Andres
19c92ed244 feat: es6+mods 2023-10-06 22:11:40 +02:00
dec0dOS
4dd6c3d91d chore: cleanup from CRA 2023-10-06 16:19:14 +01:00
dec0dOS
773b64ea30 chore(release): 1.5.8 2023-10-04 22:46:46 +01:00
dec0dOS
48a3e8acb5 ci: fix dockerfile 2023-10-04 22:46:42 +01:00
dec0dOS
c87da554d2 chore(release): 1.5.7 2023-10-04 22:42:41 +01:00
dec0dOS
3f6098c162 ci: fix dockerfile 2023-10-04 22:42:35 +01:00
dec0dOS
704595bbc2 chore(release): 1.5.6 2023-10-04 22:39:45 +01:00
dec0dOS
7e8ca3247f ci: fix dockerfile 2023-10-04 22:39:42 +01:00
dec0dOS
81d5405b88 chore(release): 1.5.5 2023-10-04 22:37:15 +01:00
dec0dOS
bc99b5e70b ci: fix dockerfile 2023-10-04 22:37:10 +01:00
dec0dOS
4ae93f95f7 chore(release): 1.5.4 2023-10-04 22:29:34 +01:00
dec0dOS
4825289f03 ci: fix dockerfile 2023-10-04 22:29:15 +01:00
dec0dOS
78520c344e chore(release): 1.5.3 2023-10-04 22:01:30 +01:00
dec0dOS
f915d7584a ci: fix dockerfile 2023-10-04 22:01:01 +01:00
dec0dOS
a18d8a2146 chore(release): 1.5.2 2023-10-04 21:58:03 +01:00
Aliaksei
02384d4d01
Merge pull request #168 from dec0dOS/modernize
Modernize codebase
2023-10-04 21:57:37 +01:00
dec0dOS
a731b8b3af chore: small changes dockerfile 2023-10-04 21:56:47 +01:00
dec0dOS
26cb077a70 chore: small changes dockerfile 2023-10-04 21:43:45 +01:00
dec0dOS
306112de5d ci: small changes 2023-10-04 21:40:02 +01:00
dec0dOS
f6a90c3483 docs: fix typo README 2023-10-04 21:34:03 +01:00
dec0dOS
a0a6997206 docs: full update README 2023-10-04 21:30:24 +01:00
dec0dOS
6ae44ed99c docs: update README 2023-10-04 21:20:35 +01:00
dec0dOS
e88c95454e chore: update dockerfile 2023-10-04 21:20:20 +01:00
dec0dOS
69d4d2288f chore: remove node options from dockerfile 2023-10-04 20:17:21 +01:00
dec0dOS
1131b7794a chore: update typecheck 2023-10-04 20:15:12 +01:00
dec0dOS
c10ffbe378 chore: update typecheck 2023-10-04 20:09:38 +01:00
dec0dOS
473c61b78b chore: upgrade deps 2023-10-04 20:03:15 +01:00
dec0dOS
bdf406f99f chore: add typecheck and linting for backend 2023-10-04 20:02:57 +01:00
dec0dOS
70c580474c chore: add Typescript linting 2023-10-04 19:30:01 +01:00
dec0dOS
5c69694505 refactor: migrate to vite 2023-10-04 19:17:02 +01:00
Aliaksei
5d041f6db6
fix: revert fix for 1.12.0 2023-10-04 14:48:24 +01:00
Aliaksei
6df591654e
Merge pull request #166 from dand-oss/main
chore: bump zerotier container to 1.10.6
2023-09-12 03:41:00 +01:00
Dan Dees
7ae94db630 Bump zerotier container to 1.10.6 2023-09-12 10:22:19 +08:00
Syrone Wong
856682bad1 fix: fix network member api resp handling
filter controller peer
handle undefined
write exp msg to error msg
2023-07-31 15:09:16 +01:00
Syrone Wong
db8f4979e6 fix: ping peer response handling
causes treating all members online

fix async/await usage
2023-07-31 15:09:00 +01:00
dec0dOS
8785b94392 chore: upgrade yarn 2022-12-30 17:19:35 +00:00
dec0dOS
9e6af9705d Merge remote-tracking branch 'refs/remotes/origin/main' 2022-12-28 22:05:28 +00:00
dec0dOS
fbd10cb142
Merge pull request #131 from gadamo/main
Fix docker image build failure after node:lts-alpine changed to 18.
2022-12-28 22:03:24 +00:00
George Adamopoulos
44d0f888c3 Bump zerotier container to version 1.10.2 2022-12-22 22:32:54 +02:00
George Adamopoulos
d52cf1ca63 Fix docker image build failure after node:lts-alpine changed to 18.
Base image for building the frontend is referenced with its tag on
the Dockerfile (FROM node:lts-alpine). The tag has been recently
changed, and now points to version 18 of nodejs.

Attempting to build with new lts (v.18), results in an error when
building the frontend:
 > [frontend-build 8/8] RUN yarn build:

Adding the ENV var has fixed the build issue, and the container image
is now produced without errors.
2022-12-22 22:23:50 +02:00
dec0dOS
90ab2ec140 chore: upgrade yarn 2022-12-05 22:14:51 +00:00
dec0dOS
076b496bf6 chore(release): 1.5.1 2022-10-09 16:25:38 +01:00
dec0dOS
450a6ad194 fix(backend/services/member.js): correctly delete members with unset additionalData 2022-10-09 16:25:30 +01:00
dec0dOS
15e4051625 fix(backend/app.js): fix internal error handler 2022-10-09 16:12:27 +01:00
dec0dOS
7b51fbc8c6 docs: update README.md 2022-09-05 16:59:48 +01:00
dec0dOS
c9b847c1da chore: new docker-compose.yml example 2022-09-05 14:53:39 +01:00
dec0dOS
4715b557f6 chore: new docker-compose.yml example 2022-09-05 14:43:30 +01:00
dec0dOS
aaaf01aa51 docs: update README.md 2022-09-05 14:32:42 +01:00
dec0dOS
60130dd168 chore: new docker-compose.yml example 2022-09-05 14:24:59 +01:00
dec0dOS
358c07dfe0 chore: upgrade dev deps 2022-08-26 12:03:51 +03:00
dec0dOS
19e7c9847a docs: add last seen envs info 2022-08-26 11:56:17 +03:00
dec0dOS
d6f05af126 chore(release): 1.5.0 2022-08-26 02:32:32 +03:00
dec0dOS
1e84548dd9
Merge pull request #107 from dec0dOS/dev
v2 release
2022-08-26 02:31:54 +03:00
dec0dOS
fdf5794e15 chore: small format fix 2022-08-26 02:26:56 +03:00
dec0dOS
40f98cc9df feat: last online 2022-08-26 02:25:16 +03:00
dec0dOS
206d12ded3 chore: upgrade yarn 2022-08-26 02:01:29 +03:00
dec0dOS
bc873675f1 chore: upgrade yarn 2022-08-26 02:01:07 +03:00
dec0dOS
d13297e638 chore: upgrade dev deps 2022-08-25 19:59:50 +03:00
dec0dOS
6bf0f2975e
Merge pull request #106 from NoobTW/main
add cron and ping members to get last seen time
2022-08-25 19:10:08 +03:00
NoobTW
45244b4433
style: fix lgtm lint 2022-08-25 08:02:56 +08:00
NoobTW
d3fdac61bd
feat(backend): add cron and ping members 2022-08-25 07:52:23 +08:00
dec0dOS
54ec7677db chore: update zyclonite/zerotier to 1.10.1 2022-08-17 12:02:08 +03:00
dec0dOS
af3e7cab7f style: remove space 2022-08-17 12:00:27 +03:00
dec0dOS
0d9feaa8d8
Merge pull request #104 from OvrAp3x/patch-1
remove "limit" on route amount as it is not enforced
2022-08-17 11:59:15 +03:00
Einar Stenberg
6d69831bc6
Update ManagedRoutes.jsx 2022-08-17 07:48:14 +02:00
Einar Stenberg
3f8b45dd8d
remove "limit" on route amount as it is not enforced 2022-08-16 15:28:20 +02:00
dec0dOS
e2c651ad05 docs: small typo fix 2022-07-01 13:24:48 +03:00
dec0dOS
5a35bdd93d docs: add duckdns notice 2022-07-01 13:20:31 +03:00
dec0dOS
69cddeaf39 docs: add Ansible role notice 2022-06-28 16:40:57 +03:00
dec0dOS
646e2b09e7 docs: small changes 2022-06-27 20:17:42 +03:00
dec0dOS
cb24c16c1d docs: fix typo 2022-06-27 20:16:08 +03:00
dec0dOS
7672e0a233 docs: update README.md (https://github.com/dec0dOS/zero-ui/issues/88) 2022-06-27 20:15:30 +03:00
dec0dOS
4444d35128 chore: use v3 version in docker-compose.yml 2022-06-27 15:30:52 +03:00
dec0dOS
6b3eacabf6 ci: update codeql 2022-06-23 22:41:48 +03:00
dec0dOS
96fd0b70aa chore(release): 1.4.1 2022-06-22 20:15:41 +03:00
dec0dOS
d17b81c52f ci: mute YN0013 warning 2022-06-22 20:04:53 +03:00
dec0dOS
a9ec7dc4fa ci: revert some changes in ci 2022-06-22 19:58:37 +03:00
dec0dOS
838cf4a567 ci: fix yarn v3 in ci 2022-06-22 19:32:08 +03:00
dec0dOS
838220520a ci: fix yarn v3 in ci 2022-06-22 19:27:51 +03:00
dec0dOS
eae12ccc53 ci: use yarn v3 in ci 2022-06-22 19:21:21 +03:00
dec0dOS
252ade73e0 ci: speed up ci 2022-06-22 19:07:17 +03:00
dec0dOS
13976c3c46 docs: misc changes 2022-06-18 16:13:06 +03:00
82 changed files with 6434 additions and 15829 deletions

View file

@ -3,7 +3,7 @@
*Dockerfile* *Dockerfile*
*docker-compose* *docker-compose*
node_modules node_modules
jsconfig.js .eslintrc.json
.DS_Store .DS_Store
tmp tmp
temp temp
@ -13,4 +13,6 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.yarn/cache
db.json
backend/data/db.json backend/data/db.json

View file

@ -7,7 +7,7 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
schedule: schedule:
- cron: "27 2 * * 4" - cron: "30 2 * * 6"
jobs: jobs:
analyze: analyze:
@ -25,12 +25,12 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

View file

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

View file

@ -4,20 +4,21 @@ on:
push: push:
tags: tags:
- "v*.*.*" - "v*.*.*"
workflow_dispatch:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Prepare zero-ui - name: Prepare zero-ui
id: prep_zero-ui id: prep_zero-ui
@ -34,14 +35,14 @@ jobs:
echo ::set-output name=tags::${TAGS} echo ::set-output name=tags::${TAGS}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push zero-ui - name: Build and push zero-ui
id: docker_build_zero-ui id: docker_build_zero-ui
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: ./ context: ./
file: ./docker/zero-ui/Dockerfile file: ./docker/zero-ui/Dockerfile

8
.gitignore vendored
View file

@ -150,15 +150,9 @@ sketch
.yarn/* .yarn/*
!.yarn/releases !.yarn/releases
!.yarn/patches
!.yarn/plugins !.yarn/plugins
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,5 @@
enableGlobalCache: true
enableTelemetry: false
nodeLinker: node-modules nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.1.cjs

View file

@ -2,6 +2,42 @@
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. 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) ## [1.4.0](https://github.com/dec0dOS/zero-ui/compare/v1.3.2...v1.4.0) (2022-06-18)
### Features ### Features

206
README.md
View file

@ -10,9 +10,9 @@
<a href="https://github.com/dec0dOS/zero-ui/blob/main/docs/SCREENSHOTS.md"><strong>Explore the screenshots »</strong></a> <a href="https://github.com/dec0dOS/zero-ui/blob/main/docs/SCREENSHOTS.md"><strong>Explore the screenshots »</strong></a>
<br /> <br />
<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> <a href="https://github.com/dec0dOS/zero-ui/discussions">Ask a Question</a>
</p> </p>
@ -43,34 +43,25 @@
## About ## About
<table> 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:
<tr>
<td>
This project is highly inspired by [ztncui](https://github.com/key-networks/ztncui) and was developed to address the current limitations of applying the self-hosted [network controllers](https://github.com/zerotier/ZeroTierOne/tree/master/controller). Some [ztncui](https://github.com/key-networks/ztncui) problems cannot be fixed because of the core architecture of the project. ZeroUI tries to solve them and implements the following features: - 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.
- Full React-powered lightweight [SPA](https://en.wikipedia.org/wiki/Single-page_application) that brings better user experience, and ZeroUI is mobile-friendly. - 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 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 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.
- 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). - Deploying ZeroUI is straightforward; refer to the [installation](#installation) section for more information.
- 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.
<details> <details>
<summary>Wait, I haven't heard about ZeroTier yet...</summary> <summary>Curious about ZeroTier?</summary>
<br> <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/). [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.
Most of your hard networking problems could be solved with ZeroTier. It could replace all your complex VPN setups. You can place all your devices on a virtual LAN and manage it easily.
To sum up, ZeroTier combines the capabilities of VPN and SD-WAN, simplifying network management. In essence, ZeroTier combines the capabilities of VPN and SD-WAN, simplifying network management.
</details> </details>
</td> ## Built With
</tr>
</table>
### Built With
Frontend: Frontend:
@ -83,7 +74,7 @@ Backend:
- [Express](https://expressjs.com) - [Express](https://expressjs.com)
- [Lowdb](https://github.com/typicode/lowdb) - [Lowdb](https://github.com/typicode/lowdb)
Deploy: Ready-to-use deployment solution:
- [Docker](https://www.docker.com) - [Docker](https://www.docker.com)
- [Docker Compose](https://docs.docker.com/compose/) - [Docker Compose](https://docs.docker.com/compose/)
@ -93,53 +84,74 @@ Deploy:
### Prerequisites ### Prerequisites
The recommended method to install ZeroUI is by using Docker and Docker Compose. 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).
To install [Docker](https://docs.docker.com/get-docker) and [Docker Compose](https://docs.docker.com/compose/install) on your system, please follow the installation guide from the [official Docker documentation](https://docs.docker.com/get-docker).
For HTTPS setup you will need a domain name. For HTTPS setup, you will need a domain name. You can obtain one for free at https://www.duckdns.org.
### Installation ### 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. Create a project directory
```sh
mkdir -p /srv/zero-ui/
cd /srv/zero-ui/
```
2. Download the `docker-compose.yml` file
1. Download the `docker-compose.yml` file
```sh ```sh
wget https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml wget https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml
``` ```
or or
```sh ```sh
curl -L -O https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml curl -L -O https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml
``` ```
2. Replace `YOURDOMAIN.com` with your domain name in `docker-compose.yml`
3. Pull the images 3. Replace `YOURDOMAIN.com` with your domain name and set admin credentials (`ZU_DEFAULT_PASSWORD`) in `docker-compose.yml`
4. Pull the image
```sh ```sh
docker-compose pull docker pull dec0dos/zero-ui
``` ```
4. Run the containers
5. Run the containers
```sh ```sh
docker-compose up -d --no-build docker-compose up -d --no-build
``` ```
5. Check if everything is okay (CTRL-C to stop log preview)
6. Check if everything is okay (`CTRL-C` to stop log preview)
```sh ```sh
docker-compose logs -f 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: 7. Disable your firewall for the following ports: `80/tcp`, `443/tcp`, and `9993/udp`
- On Ubuntu/Debian with ufw installed:
```sh ```sh
ufw allow 80/tcp ufw allow 80/tcp
ufw allow 443/tcp ufw allow 443/tcp
ufw allow 9993/udp ufw allow 9993/udp
``` ```
- or you may use the old good iptables:
- Or you can use iptables:
```sh ```sh
iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p udp --dport 9993 -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.
> To disable HTTPS, remove the `https-proxy` from `docker-compose.yml`, set `ZU_SECURE_HEADERS` to `false` and change zero-ui port `expose` to `ports`. 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: Advanced manual setups are also supported. Check the following environment variables as a reference:
| Name | Default value | Description | | Name | Default value | Description |
@ -153,139 +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_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_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_DATAPATH | `data/db.json` | ZeroUI data storage path |
| ZU_DISABLE_AUTH | unset | 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_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> <details>
<summary>Controller setup tips (outside the Docker)</summary> <summary>Controller Setup Tips (Outside Docker)</summary>
<br> <br>
If you are using the existing controller on the host, it may be necessary to allow connection from the Docker container. 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:
You could do it in two ways:
1. Allowing controller management from any IP address: 1. Allow controller management from any IP address:
```sh ```sh
echo "{\"settings\": {\"portMappingEnabled\": true,\"softwareUpdate\": \"disable\",\"allowManagementFrom\": [\"0.0.0.0/0\"]}}" > /var/lib/zerotier-one/local.conf 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> </details>
## Usage ## 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 ### Update
To get the latest version just run To get the latest version, simply run
```sh
docker-compose pull && docker-compose up -d --no-build 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. 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.
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
### Backup ### 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 ```sh
docker run --rm --volumes-from zu-main -v $(pwd):/backup ubuntu tar cvf /backup/backup-ui.tar /app/backend/data tar cvf backup-ui.tar data/
tar cvf backup-zt.tar zerotier-one/
```
## Roadmap ## 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 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 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 votes using the 👍 reaction)
- [Newest Bugs](https://github.com/dec0dOS/zero-ui/issues?q=is%3Aopen+is%3Aissue+label%3Abug) - [Newest Bugs](https://github.com/dec0dOS/zero-ui/issues?q=is%3Aopen+is%3Aissue+label%3Abug)
[![GitHub issues open](https://img.shields.io/github/issues/dec0dOS/zero-ui.svg?maxAge=2592000)](https://github.com/dec0dOS/zero-ui/issues) [![GitHub issues open](https://img.shields.io/github/issues/dec0dOS/zero-ui.svg?maxAge=2592000)](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. - _Reproducible._ Include steps to reproduce the problem.
- _Specific._ Include as much detail as possible: which version, what environment, etc. - _Specific._ Provide as much detail as possible, including version, environment, etc.
- _Unique._ Do not duplicate existing opened issues. - _Unique._ Avoid duplicating existing open issues.
- _Scoped to a Single Bug._ One bug per report. - _Scoped to a Single Bug._ Report one bug per issue.
## Contributing ## 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 To contribute:
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Commit your changes (`git commit -m 'feat: add amazing_feature'`) 1. Fork the project.
4. Push to the branch (`git push origin feat/amazing_feature`) 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) 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. 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.
Run `yarn commit` to open [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 1. Clone the repo
```sh ```sh
git clone https://github.com/dec0dOS/zero-ui.git git clone https://github.com/dec0dOS/zero-ui.git
cd zero-ui
``` ```
2. Install packages 2. Install packages
```sh ```sh
yarn install yarn install
``` ```
3. Start the development server 3. Start the development server
```sh ```sh
yarn dev yarn dev
``` ```
4. Navigate to http://localhost:3000 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:
```sh
sudo cat "/Library/Application Support/ZeroTier/One/authtoken.secret" sudo cat "/Library/Application Support/ZeroTier/One/authtoken.secret"
```
After you could start ZeroUI development environment: Afterward, you can start the ZeroUI development environment:
```sh
ZU_CONTROLLER_TOKEN=TOKEN_FROM_authtoken.secret yarn dev 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 ## Support
Reach out to me at one of the following places: If you need assistance or have questions, reach out through [GitHub Discussions](https://github.com/dec0dOS/zero-ui/discussions).
- [GitHub Discussions](https://github.com/dec0dOS/zero-ui/discussions)
- Email in [GitHub profile](https://github.com/dec0dOS)
## Security ## 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 ## License

63
backend/.eslintrc.json Normal file
View 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"
}
}

View file

@ -1,19 +1,23 @@
const express = require("express"); import path from "path";
const path = require("path"); import * as url from "url";
const logger = require("morgan"); import express from "express";
const compression = require("compression"); import logger from "morgan";
const bearerToken = require("express-bearer-token"); import compression from "compression";
const helmet = require("helmet"); import bearerToken from "express-bearer-token";
import helmet from "helmet";
import { Cron } from "croner";
const db = require("./utils/db"); import { db } from "./utils/db.js";
const initAdmin = require("./utils/init-admin"); import { initAdmin } from "./utils/init-admin.js";
import { pingAll } from "./utils/ping.js";
const authRoutes = require("./routes/auth"); import authRoutes from "./routes/auth.js";
const networkRoutes = require("./routes/network"); import networkRoutes from "./routes/network.js";
const memberRoutes = require("./routes/member"); import memberRoutes from "./routes/member.js";
const controllerRoutes = require("./routes/controller"); import controllerRoutes from "./routes/controller.js";
const app = express(); const app = express();
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
app.use(logger("dev")); app.use(logger("dev"));
app.use(express.json()); app.use(express.json());
@ -21,15 +25,20 @@ app.use(express.urlencoded({ extended: false }));
if (process.env.ZU_DISABLE_AUTH !== "true") { if (process.env.ZU_DISABLE_AUTH !== "true") {
app.use( app.use(
bearerToken({ bearerToken({
headerKey: "Bearer", headerKey: "token",
}) })
); );
} }
if (process.env.NODE_ENV === "production") {
console.debug = function () {};
}
if ( if (
process.env.NODE_ENV === "production" && process.env.NODE_ENV === "production" &&
process.env.ZU_SECURE_HEADERS !== "false" process.env.ZU_SECURE_HEADERS !== "false"
) { ) {
// @ts-ignore
app.use(helmet()); app.use(helmet());
} }
@ -42,6 +51,10 @@ if (
["/app", "/app/*"], ["/app", "/app/*"],
express.static(path.join(__dirname, "..", "frontend", "build")) 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) { app.get(["/app/network/*"], function (req, res) {
res.sendFile(path.join(__dirname, "..", "frontend", "build", "index.html")); res.sendFile(path.join(__dirname, "..", "frontend", "build", "index.html"));
}); });
@ -54,6 +67,18 @@ initAdmin().then(function (admin) {
db.defaults({ users: [admin], networks: [] }).write(); 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 routerAPI = express.Router();
const routerController = express.Router(); const routerController = express.Router();
@ -69,9 +94,9 @@ app.use("/controller", routerController); // other controller-specific routes
app.get("*", async function (req, res) { app.get("*", async function (req, res) {
res.status(404).json({ error: "404 Not found" }); res.status(404).json({ error: "404 Not found" });
}); });
app.use(async function (err, req, res) { app.use(function (err, req, res, next) {
console.error(err.stack); // TODO: replace with production logger console.error(err.stack);
res.status(500).json({ error: "500 Internal server error" }); res.status(500).json({ error: "500 Internal server error" });
}); });
module.exports = app; export default app;

View file

@ -1,13 +1,15 @@
#!/usr/bin/env node #!/usr/bin/env node
require("dotenv").config(); import dotenv from "dotenv";
dotenv.config();
/** /**
* Module dependencies. * Module dependencies.
*/ */
var app = require("../app"); import app from "../app.js";
var debug = require("debug")("zero-ui:server");
var http = require("http"); console.log("zero-ui:server");
import http from "node:http";
/** /**
* Get port from environment and store in Express. * Get port from environment and store in Express.
@ -82,6 +84,6 @@ function onError(error) {
function onListening() { function onListening() {
var addr = server.address(); var addr = server.address();
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr?.port;
debug("Listening on " + bind); console.log("Listening on " + bind);
} }

1
backend/global.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module "axios";

View file

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

View file

@ -1,20 +1,43 @@
{ {
"name": "backend", "name": "backend",
"private": true, "private": true,
"type": "module",
"scripts": { "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": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
"compression": "^1.7.4", "compression": "^1.7.4",
"debug": "~4.3.4", "croner": "^7.0.2",
"dotenv": "^16.0.1", "debug": "^4.3.4",
"express": "~4.18.1", "dotenv": "^16.3.1",
"express": "^4.18.2",
"express-bearer-token": "^2.4.0", "express-bearer-token": "^2.4.0",
"helmet": "^5.1.0", "express-rate-limit": "^7.1.1",
"helmet": "^5.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lowdb": "^1.0.0", "lowdb": "^1.0.0",
"morgan": "~1.10.0", "morgan": "^1.10.0",
"pbkdf2-wrapper": "^1.3.4" "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"
} }
} }

View file

@ -1,7 +1,29 @@
const express = require("express"); import express from "express";
import rateLimit from "express-rate-limit";
const router = express.Router(); const router = express.Router();
const auth = require("../services/auth"); import * as auth from "../services/auth.js";
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) { router.get("/login", async function (req, res) {
if (process.env.ZU_DISABLE_AUTH === "true") { if (process.env.ZU_DISABLE_AUTH === "true") {
@ -11,7 +33,7 @@ router.get("/login", async function (req, res) {
} }
}); });
router.post("/login", async function (req, res) { router.post("/login", loginLimiterWrapper, async function (req, res) {
if (req.body.username && req.body.password) { if (req.body.username && req.body.password) {
auth.authorize(req.body.username, req.body.password, function (err, user) { auth.authorize(req.body.username, req.body.password, function (err, user) {
if (user) { if (user) {
@ -27,4 +49,4 @@ router.post("/login", async function (req, res) {
} }
}); });
module.exports = router; export default router;

View file

@ -1,8 +1,8 @@
const express = require("express"); import express from "express";
const router = express.Router(); const router = express.Router();
const auth = require("../services/auth"); import * as auth from "../services/auth.js";
const api = require("../utils/controller-api"); import { api } from "../utils/controller-api.js";
router.get("/status", auth.isAuthorized, async function (req, res) { router.get("/status", auth.isAuthorized, async function (req, res) {
api.get("status").then(function (controllerRes) { 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;

View file

@ -1,13 +1,14 @@
const express = require("express"); import express from "express";
const router = express.Router({ mergeParams: true }); const router = express.Router({ mergeParams: true });
const auth = require("../services/auth"); import * as auth from "../services/auth.js";
const member = require("../services/member"); import * as member from "../services/member.js";
const api = require("../utils/controller-api"); import { api } from "../utils/controller-api.js";
// get all members // get all members
router.get("/", auth.isAuthorized, async function (req, res) { router.get("/", auth.isAuthorized, async function (req, res) {
// @ts-ignore
const nwid = req.params.nwid; const nwid = req.params.nwid;
api api
.get("controller/network/" + nwid + "/member") .get("controller/network/" + nwid + "/member")
@ -16,13 +17,14 @@ router.get("/", auth.isAuthorized, async function (req, res) {
const data = await member.getMembersData(nwid, mids); const data = await member.getMembersData(nwid, mids);
res.send(data); res.send(data);
}) })
.catch(function () { .catch(function (err) {
res.status(404).send({ error: "Network not found" }); res.status(404).send({ error: `Network not found ${err}` });
}); });
}); });
// get member // get member
router.get("/:mid", auth.isAuthorized, async function (req, res) { router.get("/:mid", auth.isAuthorized, async function (req, res) {
// @ts-ignore
const nwid = req.params.nwid; const nwid = req.params.nwid;
const mid = req.params.mid; const mid = req.params.mid;
const data = await member.getMembersData(nwid, [mid]); const data = await member.getMembersData(nwid, [mid]);
@ -35,6 +37,7 @@ router.get("/:mid", auth.isAuthorized, async function (req, res) {
// update member // update member
router.post("/:mid", auth.isAuthorized, async function (req, res) { router.post("/:mid", auth.isAuthorized, async function (req, res) {
// @ts-ignore
const nwid = req.params.nwid; const nwid = req.params.nwid;
const mid = req.params.mid; const mid = req.params.mid;
member.updateMemberAdditionalData(nwid, mid, req.body); member.updateMemberAdditionalData(nwid, mid, req.body);
@ -56,6 +59,7 @@ router.post("/:mid", auth.isAuthorized, async function (req, res) {
// delete member // delete member
router.delete("/:mid", auth.isAuthorized, async function (req, res) { router.delete("/:mid", auth.isAuthorized, async function (req, res) {
// @ts-ignore
const nwid = req.params.nwid; const nwid = req.params.nwid;
const mid = req.params.mid; const mid = req.params.mid;
member.deleteMemberAdditionalData(nwid, mid); member.deleteMemberAdditionalData(nwid, mid);
@ -82,4 +86,4 @@ router.delete("/:mid", auth.isAuthorized, async function (req, res) {
}); });
}); });
module.exports = router; export default router;

View file

@ -1,12 +1,12 @@
const express = require("express"); import express from "express";
const router = express.Router(); const router = express.Router();
const auth = require("../services/auth"); import * as auth from "../services/auth.js";
const network = require("../services/network"); import * as network from "../services/network.js";
const api = require("../utils/controller-api"); import { api } from "../utils/controller-api.js";
const constants = require("../utils/constants"); import { defaultRules } from "../utils/constants.js";
const getZTAddress = require("../utils/zt-address"); import { getZTAddress } from "../utils/zt-address.js";
let ZT_ADDRESS = null; let ZT_ADDRESS = null;
getZTAddress().then(function (address) { getZTAddress().then(function (address) {
@ -40,7 +40,7 @@ router.post("/", auth.isAuthorized, async function (req, res) {
const config = reqData.config; const config = reqData.config;
delete reqData.config; delete reqData.config;
reqData = config; reqData = config;
reqData.rules = JSON.parse(constants.defaultRules); reqData.rules = JSON.parse(defaultRules);
} else { } else {
res.status(400).send({ error: "Bad request" }); 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;

View file

@ -1,25 +1,23 @@
const db = require("../utils/db"); import { db } from "../utils/db.js";
const verifyHash = require("pbkdf2-wrapper/verifyHash"); import verifyHash from "pbkdf2-wrapper/verifyHash.js";
exports.authorize = authorize; export async function authorize(username, password, callback) {
async function authorize(username, password, callback) {
try { try {
var users = await db.get("users"); var users = await db.get("users");
} catch (err) { } catch (err) {
throw err; throw err;
} }
const user = users.find({ username: username }); 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"]); const verified = await verifyHash(password, user.value()["password_hash"]);
if (verified) { if (verified) {
return callback(null, user.value()); return callback(null, user.value());
} else { } else {
return callback(new Error("Invalid password")); return callback(new Error("logInFailed"));
} }
} }
exports.isAuthorized = isAuthorized; export async function isAuthorized(req, res, next) {
async function isAuthorized(req, res, next) {
if (process.env.ZU_DISABLE_AUTH === "true") { if (process.env.ZU_DISABLE_AUTH === "true") {
next(); next();
} else { } else {

View file

@ -1,9 +1,9 @@
const _ = require("lodash"); import _ from "lodash";
const axios = require("axios"); import axios from "axios";
const api = require("../utils/controller-api"); import { api } from "../utils/controller-api.js";
const db = require("../utils/db"); import { db } from "../utils/db.js";
const getZTAddress = require("../utils/zt-address"); import { getZTAddress } from "../utils/zt-address.js";
let ZT_ADDRESS = null; let ZT_ADDRESS = null;
getZTAddress().then(function (address) { getZTAddress().then(function (address) {
@ -25,25 +25,35 @@ async function getMemberAdditionalData(data) {
network.defaults({ members: [] }).get("members").write(); network.defaults({ members: [] }).get("members").write();
// END MIGRATION SECTION // END MIGRATION SECTION
const additionalData = db const member = db
.get("networks") .get("networks")
.find({ id: data.nwid }) .find({ id: data.nwid })
.get("members") .get("members")
.find({ id: data.id }) .find({ id: data.id });
.get("additionalConfig")
.value(); const additionalData = member.get("additionalConfig").value() || {};
const lastOnline = member.get("lastOnline").value() || 0;
const peer = await getPeer(data.id); const peer = await getPeer(data.id);
let peerData = {}; let peerData = {};
if (peer) { if (peer && !_.isEmpty(peer)) {
peerData.latency = peer.latency; peerData.latency = peer.latency;
if (peer.latency !== -1) peerData.online = 1; if (peer.latency !== -1) peerData.online = 1;
if (peer.latency == -1) peerData.online = 2; if (peer.latency == -1) peerData.online = 2;
peerData.clientVersion = peer.version; peerData.clientVersion = peer.version;
if (peer.paths[0]) { if (peer.paths.length > 0) {
peerData.lastOnline = peer.paths[0].lastReceive; let path = peer.paths.filter((p) => {
peerData.physicalAddress = peer.paths[0].address.split("/")[0]; let ret = p.active && !p.expired;
peerData.physicalPort = peer.paths[0].address.split("/")[1]; 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 { } else {
peerData.online = 0; peerData.online = 0;
@ -57,11 +67,12 @@ async function getMemberAdditionalData(data) {
return { return {
id: data.nwid + "-" + data.id, id: data.nwid + "-" + data.id,
type: "Member", clock: new Date().getTime(),
clock: Math.floor(new Date().getTime() / 1000),
networkId: data.nwid, networkId: data.nwid,
nodeId: data.id, nodeId: data.id,
controllerId: ZT_ADDRESS, controllerId: ZT_ADDRESS,
// @ts-ignore
lastOnline: lastOnline,
...additionalData, ...additionalData,
...peerData, ...peerData,
config: data, config: data,
@ -75,12 +86,12 @@ async function filterDeleted(nwid, mid) {
.get("members") .get("members")
.find({ id: mid }); .find({ id: mid });
if (!member.get("deleted").value()) return mid; let deleted = member.get("deleted").value() || false;
if (!deleted) return mid;
else return; else return;
} }
exports.getMembersData = getMembersData; export async function getMembersData(nwid, mids) {
async function getMembersData(nwid, mids) {
const prefix = "/controller/network/" + nwid + "/member/"; const prefix = "/controller/network/" + nwid + "/member/";
const filtered = ( const filtered = (
await Promise.all(mids.map(async (mid) => await filterDeleted(nwid, mid))) await Promise.all(mids.map(async (mid) => await filterDeleted(nwid, mid)))
@ -94,7 +105,7 @@ async function getMembersData(nwid, mids) {
return res; return res;
}) })
) )
.catch(function () { .catch(function (err) {
return []; return [];
}); });
@ -107,8 +118,7 @@ async function getMembersData(nwid, mids) {
return data; return data;
} }
exports.updateMemberAdditionalData = updateMemberAdditionalData; export async function updateMemberAdditionalData(nwid, mid, data) {
async function updateMemberAdditionalData(nwid, mid, data) {
if (data.config && data.config.authorized) { if (data.config && data.config.authorized) {
db.get("networks") db.get("networks")
.filter({ id: nwid }) .filter({ id: nwid })
@ -160,8 +170,7 @@ async function updateMemberAdditionalData(nwid, mid, data) {
} }
} }
exports.deleteMemberAdditionalData = deleteMemberAdditionalData; export async function deleteMemberAdditionalData(nwid, mid) {
async function deleteMemberAdditionalData(nwid, mid) {
// ZT controller bug // ZT controller bug
/* db.get("networks") /* db.get("networks")
.find({ id: nwid }) .find({ id: nwid })
@ -170,6 +179,8 @@ async function deleteMemberAdditionalData(nwid, mid) {
.write(); .write();
*/ */
await updateMemberAdditionalData(nwid, mid, {});
db.get("networks") db.get("networks")
.filter({ id: nwid }) .filter({ id: nwid })
.map("members") .map("members")

View file

@ -1,11 +1,11 @@
const _ = require("lodash"); import _ from "lodash";
const axios = require("axios"); import axios from "axios";
const api = require("../utils/controller-api"); import { api } from "../utils/controller-api.js";
const db = require("../utils/db"); import { db } from "../utils/db.js";
const constants = require("../utils/constants"); import { defaultRulesSource } from "../utils/constants.js";
async function getNetworkAdditionalData(data) { export async function getNetworkAdditionalData(data) {
let additionalData = db let additionalData = db
.get("networks") .get("networks")
.find({ id: data.id }) .find({ id: data.id })
@ -23,15 +23,13 @@ async function getNetworkAdditionalData(data) {
return { return {
id: data.id, id: data.id,
type: "Network", clock: new Date().getTime(),
clock: Math.floor(new Date().getTime() / 1000),
...additionalData.value(), ...additionalData.value(),
config: data, config: data,
}; };
} }
exports.getNetworksData = getNetworksData; export async function getNetworksData(nwids) {
async function getNetworksData(nwids) {
const prefix = "/controller/network/"; const prefix = "/controller/network/";
const links = nwids.map((nwid) => prefix + nwid); const links = nwids.map((nwid) => prefix + nwid);
@ -55,13 +53,12 @@ async function getNetworksData(nwids) {
return data; return data;
} }
exports.createNetworkAdditionalData = createNetworkAdditionalData; export async function createNetworkAdditionalData(nwid) {
async function createNetworkAdditionalData(nwid) {
const saveData = { const saveData = {
id: nwid, id: nwid,
additionalConfig: { additionalConfig: {
description: "", description: "",
rulesSource: constants.defaultRulesSource, rulesSource: defaultRulesSource,
tagsByName: {}, tagsByName: {},
capabilitiesByName: {}, capabilitiesByName: {},
}, },
@ -71,8 +68,7 @@ async function createNetworkAdditionalData(nwid) {
db.get("networks").push(saveData).write(); db.get("networks").push(saveData).write();
} }
exports.updateNetworkAdditionalData = updateNetworkAdditionalData; export async function updateNetworkAdditionalData(nwid, data) {
async function updateNetworkAdditionalData(nwid, data) {
let additionalData = {}; let additionalData = {};
if (data.hasOwnProperty("description")) { if (data.hasOwnProperty("description")) {
@ -97,7 +93,6 @@ async function updateNetworkAdditionalData(nwid, data) {
} }
} }
exports.deleteNetworkAdditionalData = deleteNetworkAdditionalData; export async function deleteNetworkAdditionalData(nwid) {
async function deleteNetworkAdditionalData(nwid) {
db.get("networks").remove({ id: nwid }).write(); db.get("networks").remove({ id: nwid }).write();
} }

10
backend/tsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"baseUrl": ".",
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"include": ["."]
}

View file

@ -1,4 +1,4 @@
exports.defaultRulesSource = ` export const defaultRulesSource = `
# This is a default rule set that allows IPv4 and IPv6 traffic but otherwise # This is a default rule set that allows IPv4 and IPv6 traffic but otherwise
# behaves like a standard Ethernet switch. # behaves like a standard Ethernet switch.
@ -26,7 +26,7 @@ drop
accept; accept;
`; `;
exports.defaultRules = ` export const defaultRules = `
[ [
{ {
"type": "MATCH_ETHERTYPE", "type": "MATCH_ETHERTYPE",

View file

@ -1,16 +1,19 @@
const axios = require("axios"); import axios from "axios";
const fs = require("fs"); import fs from "node:fs";
import os from "node:os";
const baseURL = process.env.ZU_CONTROLLER_ENDPOINT || "http://localhost:9993/"; const baseURL = process.env.ZU_CONTROLLER_ENDPOINT || "http://localhost:9993/";
var token; var token;
if (process.env.ZU_CONTROLLER_TOKEN) { if (process.env.ZU_CONTROLLER_TOKEN) {
token = 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"); 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, baseURL: baseURL,
responseType: "json", responseType: "json",
headers: { headers: {

View file

@ -1,8 +1,6 @@
const low = require("lowdb"); import low from "lowdb";
const FileSync = require("lowdb/adapters/FileSync"); import FileSync from "lowdb/adapters/FileSync.js";
const adapter = new FileSync(process.env.ZU_DATAPATH || "data/db.json"); const adapter = new FileSync(process.env.ZU_DATAPATH || "data/db.json");
const db = low(adapter); export const db = low(adapter);
module.exports = db;

View file

@ -1,7 +1,7 @@
const crypto = require("crypto"); import crypto from "crypto";
const hashPassword = require("pbkdf2-wrapper/hashText"); 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) { if (!process.env.ZU_DEFAULT_PASSWORD || !process.env.ZU_DEFAULT_USERNAME) {
console.error("ZU_DEFAULT_PASSWORD or ZU_DEFAULT_USERNAME not found!"); console.error("ZU_DEFAULT_PASSWORD or ZU_DEFAULT_USERNAME not found!");
process.exit(1); process.exit(1);
@ -13,4 +13,4 @@ module.exports = async function () {
password_hash: hash, password_hash: hash,
token: crypto.randomBytes(16).toString("hex"), token: crypto.randomBytes(16).toString("hex"),
}; };
}; }

31
backend/utils/ping.js Normal file
View 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;
});
})
);
}

View file

@ -1,10 +1,13 @@
const api = require("../utils/controller-api"); import { api } from "../utils/controller-api.js";
module.exports = async function () { export async function getZTAddress() {
try { try {
const res = await api.get("status"); const res = await api.get("status");
return res.data.address; return res.data.address;
} catch (err) { } catch (err) {
console.error(err); console.error(
// @ts-ignore
"Couldn't connect to the controller on " + err.config.baseURL
);
}
} }
};

View file

@ -1,12 +1,12 @@
version: "3.3" version: "3"
services: services:
zerotier: zerotier:
image: zyclonite/zerotier:1.10.0 image: zyclonite/zerotier:1.10.6
container_name: zu-controller container_name: zu-controller
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- controller_data:/var/lib/zerotier-one - ./zerotier-one:/var/lib/zerotier-one
environment: environment:
- ZT_OVERRIDE_LOCAL_CONF=true - ZT_OVERRIDE_LOCAL_CONF=true
- ZT_ALLOW_MANAGEMENT_FROM=0.0.0.0/0 - ZT_ALLOW_MANAGEMENT_FROM=0.0.0.0/0
@ -24,8 +24,8 @@ services:
depends_on: depends_on:
- zerotier - zerotier
volumes: volumes:
- controller_data:/var/lib/zerotier-one - ./zerotier-one:/var/lib/zerotier-one
- zero-ui_data:/app/backend/data - ./data:/app/backend/data
environment: environment:
- ZU_CONTROLLER_ENDPOINT=http://zerotier:9993/ - ZU_CONTROLLER_ENDPOINT=http://zerotier:9993/
- ZU_SECURE_HEADERS=true - ZU_SECURE_HEADERS=true
@ -41,12 +41,7 @@ services:
- zero-ui - zero-ui
command: caddy reverse-proxy --from YOURDOMAIN.com --to zero-ui:4000 command: caddy reverse-proxy --from YOURDOMAIN.com --to zero-ui:4000
volumes: volumes:
- caddy_data:/data - ./caddy:/data/caddy
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
volumes:
zero-ui_data:
controller_data:
caddy_data:

View file

@ -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 ENV GENERATE_SOURCEMAP=false
WORKDIR /app/frontend # Enable corepack and create necessary directories in one layer
COPY ./frontend/package*.json /app/frontend RUN corepack enable && mkdir -p /app/frontend
RUN yarn install --network-timeout 1000000
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 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 WORKDIR /app
COPY --from=build-stage /app/frontend/build /app/frontend/build/
# 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 WORKDIR /app/backend
COPY ./backend/package*.json /app/backend COPY ./backend/package*.json /app/backend/
RUN yarn install
# Install backend dependencies
RUN yarn workspaces focus --production backend && yarn cache clean
# Copy the backend source files
COPY ./backend /app/backend 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 NODE_ENV=production
ENV ZU_SECURE_HEADERS=true ENV ZU_SECURE_HEADERS=true
ENV ZU_SERVE_FRONTEND=true ENV ZU_SERVE_FRONTEND=true
CMD [ "node", "./bin/www" ] # Expose the application port
EXPOSE 4000
# Start the application
CMD ["node", "bin/www.js"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Before After
Before After

22
frontend/.eslintrc.json Normal file
View 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"]
}
}

View file

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

View file

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

View file

@ -2,48 +2,55 @@
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fontsource/roboto": "^4.5.7", "@fontsource/roboto": "^4.5.8",
"@material-ui/core": "^4.12.4", "@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3", "@material-ui/icons": "^4.11.3",
"@material-ui/styles": "^4.11.5", "@material-ui/styles": "^4.11.5",
"@uiw/react-codemirror": "^3.1.0", "@uiw/react-codemirror": "^3.1.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"codemirror": "^5.62.3", "codemirror": "^5.62.3",
"date-fns": "^2.29.2",
"history": "^5.3.0", "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", "ipaddr.js": "^2.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^17.0.2", "react": "^17.0.2",
"react-data-table-component": "^6.11.8", "react-data-table-component": "^6.11.8",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-i18next": "^13.3.0",
"react-is": "^17.0.2", "react-is": "^17.0.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"styled-components": "^5.3.1" "styled-components": "^5.3.11"
}, },
"devDependencies": { "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": { "scripts": {
"start": "BROWSER=none react-scripts start", "start": "vite",
"build": "react-scripts build", "build": "vite build",
"analyze": "source-map-explorer 'build/static/js/*.js'" "serve": "vite preview",
}, "clean": "rimraf build",
"homepage": "/app", "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"proxy": "http://127.0.0.1:4000", "typecheck": "tsc --pretty --noEmit -p tsconfig.json",
"eslintConfig": { "analyze": "vite build --sourcemap true && source-map-explorer 'build/assets/*.js' --no-border-checks"
"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"
]
} }
} }

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

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

View 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": "Русский"
}

View 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": "中文"
}

View file

@ -1,5 +1,6 @@
import "@fontsource/roboto"; import "@fontsource/roboto";
import { Suspense } from "react";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom"; import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import Theme from "./components/Theme"; import Theme from "./components/Theme";
@ -8,19 +9,27 @@ import Bar from "./components/Bar";
import Home from "./routes/Home"; import Home from "./routes/Home";
import NotFound from "./routes/NotFound"; import NotFound from "./routes/NotFound";
import Network from "./routes/Network/Network"; import Network from "./routes/Network/Network";
import Settings from "./routes/Settings";
import Loading from "./components/Loading";
import "./i18n";
function App() { function App() {
return ( return (
<Theme> <Theme>
<Suspense fallback={<Loading />}>
<BrowserRouter basename="/app"> <BrowserRouter basename="/app">
<Bar /> <Bar />
<Switch> <Switch>
<Route exact path="/" component={Home} /> <Route exact path="/" component={Home} />
<Route path="/network/:nwid" component={Network} /> <Route path="/network/:nwid" component={Network} />
<Route path="/settings" component={Settings} />
<Route path="/404" component={NotFound} /> <Route path="/404" component={NotFound} />
<Redirect to="/404" /> <Redirect to="/404" />
</Switch> </Switch>
</BrowserRouter> </BrowserRouter>
</Suspense>
</Theme> </Theme>
); );
} }

View file

@ -19,6 +19,8 @@ import MenuIcon from "@material-ui/icons/Menu";
import LogIn from "components/LogIn"; import LogIn from "components/LogIn";
import { useTranslation } from "react-i18next";
function Bar() { function Bar() {
const [loggedIn, setLoggedIn] = useLocalStorage("loggedIn", false); const [loggedIn, setLoggedIn] = useLocalStorage("loggedIn", false);
const [disabledAuth] = useLocalStorage("disableAuth", false); const [disabledAuth] = useLocalStorage("disableAuth", false);
@ -41,16 +43,18 @@ function Bar() {
history.go(0); history.go(0);
}; };
const { t, i18n } = useTranslation();
const menuItems = [ const menuItems = [
// TODO: add settings page // TODO: add settings page
// { {
// name: "Settings", name: t("settings"),
// to: "/settings", to: "/settings",
// }, },
...(!disabledAuth ...(!disabledAuth
? [ ? [
{ {
name: "Log out", name: t("logOut"),
divide: true, divide: true,
onClick: onLogOutClick, onClick: onLogOutClick,
}, },
@ -90,7 +94,7 @@ function Bar() {
> >
{menuItems.map((menuItem, index) => { {menuItems.map((menuItem, index) => {
if ( if (
menuItem.hasOwnProperty("condition") && Object.prototype.hasOwnProperty.call(menuItem, "condition") &&
!menuItem.condition !menuItem.condition
) { ) {
return null; return null;
@ -115,7 +119,6 @@ function Bar() {
key={index} key={index}
onClick={() => { onClick={() => {
closeMenu(); closeMenu();
menuItem.onClick(); menuItem.onClick();
}} }}
> >

View file

@ -9,6 +9,8 @@ import NetworkButton from "./components/NetworkButton";
import API from "utils/API"; import API from "utils/API";
import { generateNetworkConfig } from "utils/NetworkConfig"; import { generateNetworkConfig } from "utils/NetworkConfig";
import { useTranslation } from "react-i18next";
function HomeLoggedIn() { function HomeLoggedIn() {
const [networks, setNetworks] = useState([]); const [networks, setNetworks] = useState([]);
@ -30,6 +32,8 @@ function HomeLoggedIn() {
fetchData(); fetchData();
}, []); }, []);
const { t, i18n } = useTranslation();
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Button <Button
@ -38,19 +42,19 @@ function HomeLoggedIn() {
className={classes.createBtn} className={classes.createBtn}
onClick={createNetwork} onClick={createNetwork}
> >
Create A Network {t("createNetwork")}
</Button> </Button>
<Divider /> <Divider />
<Grid container spacing={3} className={classes.container}> <Grid container spacing={3} className={classes.container}>
<Grid item xs={6}> <Grid item xs={6}>
<Typography variant="h5">Controller networks</Typography> <Typography variant="h5">{t("controllerNetworks")}</Typography>
{networks[0] && "Network controller address"} {networks[0] && t("controllerAddress")}
<Box fontWeight="fontWeightBold"> <Box fontWeight="fontWeightBold">
{networks[0] && networks[0]["id"].slice(0, 10)} {networks[0] && networks[0]["id"].slice(0, 10)}
</Box> </Box>
</Grid> </Grid>
<Grid item xs="auto"> <Grid item xs="auto">
<Typography>Networks</Typography> <Typography>{t("network", { count: networks.length })}</Typography>
<Grid item> <Grid item>
{networks[0] ? ( {networks[0] ? (
networks.map((network) => ( networks.map((network) => (
@ -59,7 +63,7 @@ function HomeLoggedIn() {
</Grid> </Grid>
)) ))
) : ( ) : (
<div>Please create at least one network</div> <div>{t("createOneNetwork")}</div>
)} )}
</Grid> </Grid>
</Grid> </Grid>

View file

@ -3,6 +3,8 @@ import { Grid, Typography } from "@material-ui/core";
import { useLocalStorage } from "react-use"; import { useLocalStorage } from "react-use";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import axios from "axios"; import axios from "axios";
function HomeLoggedOut() { function HomeLoggedOut() {
@ -29,6 +31,8 @@ function HomeLoggedOut() {
fetchData(); fetchData();
}, [history, setDisableAuth, setLoggedIn, setToken]); }, [history, setDisableAuth, setLoggedIn, setToken]);
const { t, i18n } = useTranslation();
return ( return (
<Grid <Grid
container container
@ -42,14 +46,11 @@ function HomeLoggedOut() {
> >
<Grid item xs={10}> <Grid item xs={10}>
<Typography variant="h5"> <Typography variant="h5">
<span> <span>{t("zerouiDesc")}</span>
ZeroUI - ZeroTier Controller Web UI - is a web user interface for a
self-hosted ZeroTier network controller.
</span>
</Typography> </Typography>
<Typography> <Typography>
<span>Please Log In to continue</span> <span>{t("loginToContinue")}</span>
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>

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

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

View file

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

View file

@ -6,7 +6,7 @@ import LogInToken from "./components/LogInToken";
function LogIn() { function LogIn() {
return ( return (
<> <>
{process.env.NODE_ENV === "development" && ( {import.meta.env.DEV && (
<> <>
<LogInToken /> <LogInToken />
<Divider orientation="vertical" /> <Divider orientation="vertical" />

View file

@ -12,6 +12,8 @@ import {
DialogTitle, DialogTitle,
} from "@material-ui/core"; } from "@material-ui/core";
import { useTranslation } from "react-i18next";
function LogInToken() { function LogInToken() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
@ -41,6 +43,8 @@ function LogInToken() {
} }
}; };
const { t, i18n } = useTranslation();
const LogIn = () => { const LogIn = () => {
if (token.length !== 32) { if (token.length !== 32) {
setErrorText("Token length error"); setErrorText("Token length error");
@ -55,12 +59,12 @@ function LogInToken() {
return ( return (
<div> <div>
<Button onClick={handleClickOpen} color="inherit" variant="outlined"> <Button onClick={handleClickOpen} color="inherit" variant="outlined">
Token Log In {t("logInToken")}
</Button> </Button>
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}> <Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
<DialogTitle>Log In</DialogTitle> <DialogTitle>{t("logIn")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText>ADVANCED FEATURE.</DialogContentText> <DialogContentText>{t("advancedFeature")}</DialogContentText>
<TextField <TextField
value={token} value={token}
onChange={(e) => { onChange={(e) => {
@ -76,10 +80,10 @@ function LogInToken() {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} color="primary"> <Button onClick={handleClose} color="primary">
Cancel {t("cancel")}
</Button> </Button>
<Button onClick={LogIn} color="primary"> <Button onClick={LogIn} color="primary">
Log In {t("logIn")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View file

@ -13,10 +13,14 @@ import {
import axios from "axios"; import axios from "axios";
import { useTranslation } from "react-i18next";
function LogInUser() { function LogInUser() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false); const [snackbarOpen, setSnackbarOpen] = useState(false);
const [error, setError] = useState("");
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@ -65,17 +69,20 @@ function LogInUser() {
.catch(function (error) { .catch(function (error) {
setPassword(""); setPassword("");
setSnackbarOpen(true); setSnackbarOpen(true);
console.error(error); setError(error.response.data.error);
// console.error(error.response.data.error);
}); });
}; };
const { t, i18n } = useTranslation();
return ( return (
<> <>
<Button onClick={handleClickOpen} color="primary" variant="contained"> <Button onClick={handleClickOpen} color="primary" variant="contained">
Log In {t("logIn")}
</Button> </Button>
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}> <Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
<DialogTitle>Log In</DialogTitle> <DialogTitle>{t("logIn")}</DialogTitle>
<DialogContent> <DialogContent>
<TextField <TextField
autoFocus autoFocus
@ -84,7 +91,7 @@ function LogInUser() {
setUsername(e.target.value); setUsername(e.target.value);
}} }}
margin="dense" margin="dense"
label="username" label={t("username")}
type="username" type="username"
fullWidth fullWidth
/> />
@ -94,17 +101,17 @@ function LogInUser() {
setPassword(e.target.value); setPassword(e.target.value);
}} }}
margin="dense" margin="dense"
label="password" label={t("password")}
type="password" type="password"
fullWidth fullWidth
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} color="primary"> <Button onClick={handleClose} color="primary">
Cancel {t("cancel")}
</Button> </Button>
<Button onClick={LogIn} color="primary"> <Button onClick={LogIn} color="primary">
Log In {t("logIn")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@ -114,7 +121,7 @@ function LogInUser() {
vertical: "top", vertical: "top",
horizontal: "center", horizontal: "center",
}} }}
message="Invalid username or password" message={t(error)}
/> />
</> </>
); );

View file

@ -18,6 +18,8 @@ import DeleteIcon from "@material-ui/icons/Delete";
import API from "utils/API"; import API from "utils/API";
import { useTranslation } from "react-i18next";
function NetworkManagement() { function NetworkManagement() {
const { nwid } = useParams(); const { nwid } = useParams();
const history = useHistory(); const history = useHistory();
@ -42,10 +44,12 @@ function NetworkManagement() {
history.go(0); history.go(0);
}; };
const { t, i18n } = useTranslation();
return ( return (
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Management</Typography> <Typography>{t("management")}</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Button <Button
@ -54,21 +58,19 @@ function NetworkManagement() {
startIcon={<DeleteIcon />} startIcon={<DeleteIcon />}
onClick={handleClickOpen} onClick={handleClickOpen}
> >
Delete Network {t("deleteNetwork")}
</Button> </Button>
<Dialog open={open} onClose={handleClose}> <Dialog open={open} onClose={handleClose}>
<DialogTitle> <DialogTitle>{t("deleteNetworkConfirm")}</DialogTitle>
{"Are you sure you want to delete this network?"}
</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText>This action cannot be undone.</DialogContentText> <DialogContentText>{t("deleteAlert")}</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} color="primary"> <Button onClick={handleClose} color="primary">
Cancel {t("cancel")}
</Button> </Button>
<Button onClick={deleteNetwork} color="secondary"> <Button onClick={deleteNetwork} color="secondary">
Delete {t("delete")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View file

@ -14,12 +14,15 @@ import DataTable from "react-data-table-component";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import API from "utils/API"; import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper"; import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
import { formatDistance } from "date-fns";
import AddMember from "./components/AddMember"; import AddMember from "./components/AddMember";
import DeleteMember from "./components/DeleteMember"; import DeleteMember from "./components/DeleteMember";
import ManagedIP from "./components/ManagedIP"; import ManagedIP from "./components/ManagedIP";
import MemberName from "./components/MemberName"; import MemberName from "./components/MemberName";
import MemberSettings from "./components/MemberSettings"; import MemberSettings from "./components/MemberSettings";
import { useTranslation } from "react-i18next";
function NetworkMembers({ network }) { function NetworkMembers({ network }) {
const { nwid } = useParams(); const { nwid } = useParams();
const [members, setMembers] = useState([]); const [members, setMembers] = useState([]);
@ -45,6 +48,8 @@ function NetworkMembers({ network }) {
console.log("Action:", req); console.log("Action:", req);
}; };
const { t, i18n } = useTranslation();
const handleChange = const handleChange =
(member, key1, key2 = null, mode = "text", id = null) => (member, key1, key2 = null, mode = "text", id = null) =>
(event) => { (event) => {
@ -66,7 +71,7 @@ function NetworkMembers({ network }) {
const columns = [ const columns = [
{ {
id: "auth", id: "auth",
name: "Authorized", name: t("authorized"),
minWidth: "80px", minWidth: "80px",
cell: (row) => ( cell: (row) => (
<Checkbox <Checkbox
@ -78,7 +83,7 @@ function NetworkMembers({ network }) {
}, },
{ {
id: "address", id: "address",
name: "Address", name: t("address"),
minWidth: "150px", minWidth: "150px",
cell: (row) => ( cell: (row) => (
<Typography variant="body2">{row.config.address}</Typography> <Typography variant="body2">{row.config.address}</Typography>
@ -86,52 +91,52 @@ function NetworkMembers({ network }) {
}, },
{ {
id: "name", id: "name",
name: "Name / Description", name: t("name") + "/" + t("description"),
minWidth: "250px", minWidth: "250px",
cell: (row) => <MemberName member={row} handleChange={handleChange} />, cell: (row) => <MemberName member={row} handleChange={handleChange} />,
}, },
{ {
id: "ips", id: "ips",
name: "Managed IPs", name: t("managedIPs"),
minWidth: "220px", minWidth: "220px",
cell: (row) => <ManagedIP member={row} handleChange={handleChange} />, cell: (row) => <ManagedIP member={row} handleChange={handleChange} />,
}, },
{ {
id: "status", id: "lastSeen",
name: "Peer status", name: t("lastSeen"),
minWidth: "100px", minWidth: "100px",
cell: (row) => cell: (row) =>
row.online === 0 ? ( row.online === 1 ? (
<Typography color="error">OFFLINE</Typography> <Typography style={{ color: "#008000" }}>{"ONLINE"}</Typography>
) : row.online === 1 ? ( ) : row.controllerId === row.config.address ? (
<Typography style={{ color: "#008000" }}> <Typography style={{ color: "#c5e31e" }}>{"CONTROLLER"}</Typography>
{"ONLINE (v" + ) : row.online === 0 ? (
row.config.vMajor + <Typography color="error">
"." + {row.lastOnline !== 0
row.config.vMinor + ? formatDistance(row.lastOnline, row.clock, {
"." + includeSeconds: false,
row.config.vRev + addSuffix: true,
")"} })
: "OFFLINE"}
</Typography> </Typography>
) : ( ) : (
<Typography style={{ color: "#f1c232" }}> <Typography style={{ color: "#f1c232" }}>{"RELAYED"}</Typography>
{"RELAYED (v" +
row.config.vMajor +
"." +
row.config.vMinor +
"." +
row.config.vRev +
")"}
</Typography>
), ),
}, },
{ {
id: "physicalip", id: "physicalip",
name: "Physical IP / Latency", name: t("version") + " / " + t("physIp") + " / " + t("latency"),
minWidth: "220px", minWidth: "220px",
cell: (row) => cell: (row) =>
row.online === 1 ? ( row.online === 1 ? (
<p> <p>
{"v" +
row.config.vMajor +
"." +
row.config.vMinor +
"." +
row.config.vRev}
<br />
{row.physicalAddress + "/" + row.physicalPort} {row.physicalAddress + "/" + row.physicalPort}
<br /> <br />
{"(" + row.latency + " ms)"} {"(" + row.latency + " ms)"}
@ -142,7 +147,7 @@ function NetworkMembers({ network }) {
}, },
{ {
id: "delete", id: "delete",
name: "", name: t("settings"),
minWidth: "50px", minWidth: "50px",
right: true, right: true,
cell: (row) => ( cell: (row) => (
@ -161,7 +166,7 @@ function NetworkMembers({ network }) {
return ( return (
<Accordion defaultExpanded={true}> <Accordion defaultExpanded={true}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Members</Typography> <Typography>{t("member", { count: members.length })}</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Grid container direction="column" spacing={3}> <Grid container direction="column" spacing={3}>
@ -181,14 +186,13 @@ function NetworkMembers({ network }) {
spacing={0} spacing={0}
direction="column" direction="column"
alignItems="center" alignItems="center"
justify="center" justifyContent="center"
style={{ style={{
minHeight: "50vh", minHeight: "50vh",
}} }}
> >
<Typography variant="h6" style={{ padding: "10%" }}> <Typography variant="h6" style={{ padding: "10%" }}>
No devices have joined this network. Use the app on your {t("noDevices")} <b>{nwid}</b>.
devices to join <b>{nwid}</b>.
</Typography> </Typography>
</Grid> </Grid>
)} )}

View file

@ -5,6 +5,8 @@ import AddIcon from "@material-ui/icons/Add";
import API from "utils/API"; import API from "utils/API";
import { useTranslation } from "react-i18next";
function AddMember({ nwid, callback }) { function AddMember({ nwid, callback }) {
const [member, setMember] = useState(""); const [member, setMember] = useState("");
@ -24,9 +26,11 @@ function AddMember({ nwid, callback }) {
setMember(""); setMember("");
}; };
const { t, i18n } = useTranslation();
return ( return (
<> <>
<Typography>Manually Add Member</Typography> <Typography>{t("addMemberManually")}</Typography>
<List <List
disablePadding={true} disablePadding={true}
style={{ style={{

View file

@ -12,8 +12,10 @@ import {
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import API from "utils/API"; import API from "utils/API";
import { useTranslation } from "react-i18next";
function DeleteMember({ nwid, mid, callback }) { function DeleteMember({ nwid, mid, callback }) {
const { t, i18n } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClickOpen = () => { const handleClickOpen = () => {
@ -37,18 +39,16 @@ function DeleteMember({ nwid, mid, callback }) {
<DeleteOutlineIcon color="secondary" style={{ fontSize: 20 }} /> <DeleteOutlineIcon color="secondary" style={{ fontSize: 20 }} />
</IconButton> </IconButton>
<Dialog open={open} onClose={handleClose}> <Dialog open={open} onClose={handleClose}>
<DialogTitle> <DialogTitle>{t("deleteMemberConfirm")}</DialogTitle>
{"Are you sure you want to delete this member?"}
</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText>This action cannot be undone.</DialogContentText> <DialogContentText>{t("deleteAlert")}</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} color="primary"> <Button onClick={handleClose} color="primary">
Cancel {t("cancel")}
</Button> </Button>
<Button onClick={deleteMemberReq} color="secondary"> <Button onClick={deleteMemberReq} color="secondary">
Delete {t("delete")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View file

@ -1,12 +1,14 @@
import { Grid, TextField } from "@material-ui/core"; import { Grid, TextField } from "@material-ui/core";
import { useTranslation } from "react-i18next";
function MemberName({ member, handleChange }) { function MemberName({ member, handleChange }) {
const { t, i18n } = useTranslation();
return ( return (
<Grid> <Grid>
<TextField <TextField
value={member.name} value={member.name}
onChange={handleChange(member, "name")} onChange={handleChange(member, "name")}
label="Name" label={t("name")}
variant="filled" variant="filled"
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
@ -15,7 +17,7 @@ function MemberName({ member, handleChange }) {
<TextField <TextField
value={member.description} value={member.description}
onChange={handleChange(member, "description")} onChange={handleChange(member, "description")}
label="Description" label={t("description")}
variant="filled" variant="filled"
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,

View file

@ -13,7 +13,10 @@ import BuildIcon from "@material-ui/icons/Build";
import { useState } from "react"; import { useState } from "react";
import Tag from "./components/Tag"; import Tag from "./components/Tag";
import { useTranslation } from "react-i18next";
function MemberSettings({ member, network, handleChange }) { function MemberSettings({ member, network, handleChange }) {
const { t, i18n } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClickOpen = () => { const handleClickOpen = () => {
@ -30,7 +33,9 @@ function MemberSettings({ member, network, handleChange }) {
<BuildIcon style={{ fontSize: 20 }} /> <BuildIcon style={{ fontSize: 20 }} />
</IconButton> </IconButton>
<Dialog open={open} onClose={handleClose}> <Dialog open={open} onClose={handleClose}>
<DialogTitle>{"Member " + member.config.id + " settings"}</DialogTitle> <DialogTitle>
{t("member") + member.config.id + t("settings")}
</DialogTitle>
<DialogContent> <DialogContent>
<Grid item> <Grid item>
<Checkbox <Checkbox
@ -43,7 +48,7 @@ function MemberSettings({ member, network, handleChange }) {
"checkbox" "checkbox"
)} )}
/> />
<span>Allow Ethernet Bridging</span> <span>{t("allowBridging")}</span>
</Grid> </Grid>
<Grid item> <Grid item>
<Checkbox <Checkbox
@ -56,17 +61,17 @@ function MemberSettings({ member, network, handleChange }) {
"checkbox" "checkbox"
)} )}
/> />
<span>Do Not Auto-Assign IPs</span> <span>{t("noAutoIP")}</span>
</Grid> </Grid>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h6">Capabilities</Typography> <Typography variant="h6">{t("capabilities")}</Typography>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Paper style={{ padding: 20 }}> <Paper style={{ padding: 20 }}>
{Object.entries(network["capabilitiesByName"] || []).length === {Object.entries(network["capabilitiesByName"] || []).length ===
0 0
? "No capabilities defined" ? t("noCapDef")
: ""} : ""}
{Object.entries(network["capabilitiesByName"] || []).map( {Object.entries(network["capabilitiesByName"] || []).map(
([capName, capId]) => ( ([capName, capId]) => (
@ -96,11 +101,11 @@ function MemberSettings({ member, network, handleChange }) {
</Grid> </Grid>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h6">Tags</Typography> <Typography variant="h6">{t("tags")}</Typography>
</Grid> </Grid>
{Object.entries(network["tagsByName"] || []).length === 0 ? ( {Object.entries(network["tagsByName"] || []).length === 0 ? (
<Grid item xs={12}> <Grid item xs={12}>
<Paper style={{ padding: 20 }}>No tags defined</Paper> <Paper style={{ padding: 20 }}>{t("noTagDef")}</Paper>
</Grid> </Grid>
) : ( ) : (
"" ""

View file

@ -17,7 +17,11 @@ import debounce from "lodash/debounce";
import { useState } from "react"; import { useState } from "react";
import API from "utils/API"; import API from "utils/API";
import { useTranslation } from "react-i18next";
function NetworkRules({ network, callback }) { function NetworkRules({ network, callback }) {
const { t, i18n } = useTranslation();
const [editor, setEditor] = useState(null); const [editor, setEditor] = useState(null);
const [flowData, setFlowData] = useState({ const [flowData, setFlowData] = useState({
rules: [...network.config.rules], rules: [...network.config.rules],
@ -87,12 +91,12 @@ function NetworkRules({ network, callback }) {
return ( return (
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Flow Rules</Typography> <Typography>{t("flowRules")}</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{/* Important note: value in CodeMirror instance means INITAIL VALUE {/* Important note: value in CodeMirror instance means INITAIL VALUE
or it could be used to replace editor state with the new 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 <CodeMirror
value={network["rulesSource"]} value={network["rulesSource"]}
@ -124,13 +128,13 @@ function NetworkRules({ network, callback }) {
width: "250px", width: "250px",
}} }}
> >
{!!errors.length ? ( {errors.length ? (
<Typography color="error"> <Typography color="error">
{"[" + errors[0] + ":" + errors[1] + "] " + errors[2]} {"[" + errors[0] + ":" + errors[1] + "] " + errors[2]}
</Typography> </Typography>
) : ( ) : (
<Button variant="contained" color="primary" onClick={saveChanges}> <Button variant="contained" color="primary" onClick={saveChanges}>
Save Changes {t("saveChanges")}
</Button> </Button>
)} )}
</Grid> </Grid>

View file

@ -17,7 +17,10 @@ import IPv4AutoAssign from "./components/IPv4AutoAssign";
import API from "utils/API"; import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper"; import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
import { useTranslation } from "react-i18next";
function NetworkSettings({ network, setNetwork }) { function NetworkSettings({ network, setNetwork }) {
const { t, i18n } = useTranslation();
const sendReq = async (data) => { const sendReq = async (data) => {
try { try {
const req = await API.post("/network/" + network["config"]["id"], data); const req = await API.post("/network/" + network["config"]["id"], data);
@ -43,12 +46,12 @@ function NetworkSettings({ network, setNetwork }) {
return ( return (
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>General settings</Typography> <Typography>{t("generalSettings")}</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Grid container direction="column" spacing={3}> <Grid container direction="column" spacing={3}>
<Grid item> <Grid item>
<Typography>Network ID</Typography> <Typography>{t("networkId")}</Typography>
<Typography variant="h5"> <Typography variant="h5">
<span>{network["config"]["id"]}</span> <span>{network["config"]["id"]}</span>
</Typography> </Typography>
@ -57,7 +60,7 @@ function NetworkSettings({ network, setNetwork }) {
<TextField <TextField
value={network["config"]["name"]} value={network["config"]["name"]}
onChange={handleChange("config", "name")} onChange={handleChange("config", "name")}
label="Name" label={t("name")}
variant="filled" variant="filled"
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
@ -69,9 +72,9 @@ function NetworkSettings({ network, setNetwork }) {
value={network["description"]} value={network["description"]}
onChange={handleChange("description")} onChange={handleChange("description")}
multiline multiline
rows={2} minRows={2}
rowsMax={Infinity} maxRows={Infinity}
label="Description" label={t("description")}
variant="filled" variant="filled"
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
@ -80,14 +83,14 @@ function NetworkSettings({ network, setNetwork }) {
</Grid> </Grid>
<Divider /> <Divider />
<Grid item> <Grid item>
<Typography>Access Control</Typography> <Typography>{t("accessControl")}</Typography>
<Select <Select
native native
value={network["config"]["private"]} value={network["config"]["private"]}
onChange={handleChange("config", "private", "json")} onChange={handleChange("config", "private", "json")}
> >
<option value={true}>Private</option> <option value={1}>{t("private")}</option>
<option value={false}>Public</option> <option value={0}>{t("public")}</option>
</Select> </Select>
</Grid> </Grid>
<Divider /> <Divider />
@ -111,7 +114,7 @@ function NetworkSettings({ network, setNetwork }) {
<Divider /> <Divider />
<Grid item> <Grid item>
<TextField <TextField
label="Multicast Recipient Limit" label={t("multicastLimit")}
type="number" type="number"
value={network["config"]["multicastLimit"]} value={network["config"]["multicastLimit"]}
onChange={handleChange("config", "multicastLimit", "json")} onChange={handleChange("config", "multicastLimit", "json")}
@ -126,7 +129,7 @@ function NetworkSettings({ network, setNetwork }) {
color="primary" color="primary"
onChange={handleChange("config", "enableBroadcast", "checkbox")} onChange={handleChange("config", "enableBroadcast", "checkbox")}
/> />
<span>Enable Broadcast</span> <span>{t("enableBroadcast")}</span>
</Grid> </Grid>
{/* TODO: */} {/* TODO: */}
{/* <Grid item> {/* <Grid item>

View file

@ -18,7 +18,10 @@ import DataTable from "react-data-table-component";
import { addressPool } from "utils/NetworkConfig"; import { addressPool } from "utils/NetworkConfig";
import { getCIDRAddress, validateIP, normilizeIP } from "utils/IP"; import { getCIDRAddress, validateIP, normilizeIP } from "utils/IP";
import { useTranslation } from "react-i18next";
function IPv4AutoAssign({ ipAssignmentPools, handleChange }) { function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
const { t, i18n } = useTranslation();
const [start, setStart] = useState(""); const [start, setStart] = useState("");
const [end, setEnd] = useState(""); const [end, setEnd] = useState("");
@ -89,19 +92,19 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
}, },
{ {
id: "Start", id: "Start",
name: "Start", name: t("start"),
cell: (row) => row["ipRangeStart"], cell: (row) => row["ipRangeStart"],
}, },
{ {
id: "End", id: "End",
name: "End", name: t("end"),
cell: (row) => row["ipRangeEnd"], cell: (row) => row["ipRangeEnd"],
}, },
]; ];
return ( return (
<> <>
<Typography>IPv4 Auto-Assign</Typography> <Typography>{t("ipv4AutoAssign")}</Typography>
<div <div
style={{ style={{
padding: "30px", padding: "30px",
@ -122,7 +125,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
</Grid> </Grid>
</div> </div>
<Typography style={{ paddingBottom: "10px" }}> <Typography style={{ paddingBottom: "10px" }}>
Auto-Assign Pools {t("autoAssignPool")}
</Typography> </Typography>
<Box border={1} borderColor="grey.300"> <Box border={1} borderColor="grey.300">
<Grid item style={{ margin: "10px" }}> <Grid item style={{ margin: "10px" }}>
@ -132,7 +135,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
data={ipAssignmentPools} data={ipAssignmentPools}
/> />
<Divider /> <Divider />
<Typography>Add IPv4 Pool</Typography> <Typography>{t("addIPv4Pool")}</Typography>
<List <List
style={{ style={{
display: "flex", display: "flex",
@ -142,7 +145,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
<TextField <TextField
value={start} value={start}
onChange={handleStartInput} onChange={handleStartInput}
placeholder={"Start"} placeholder={t("start")}
/> />
<Divider <Divider
orientation="vertical" orientation="vertical"
@ -154,7 +157,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
<TextField <TextField
value={end} value={end}
onChange={handleEndInput} onChange={handleEndInput}
placeholder={"End"} placeholder={t("end")}
/> />
<IconButton <IconButton
size="small" size="small"

View file

@ -16,7 +16,10 @@ import DataTable from "react-data-table-component";
import { validateIP, normilizeIP, validateCIDR } from "utils/IP"; import { validateIP, normilizeIP, validateCIDR } from "utils/IP";
import { useTranslation } from "react-i18next";
function ManagedRoutes({ routes, handleChange }) { function ManagedRoutes({ routes, handleChange }) {
const { t, i18n } = useTranslation();
const [destination, setDestination] = useState(""); const [destination, setDestination] = useState("");
const [via, setVia] = useState(""); const [via, setVia] = useState("");
@ -71,12 +74,12 @@ function ManagedRoutes({ routes, handleChange }) {
}, },
{ {
id: "target", id: "target",
name: "Target", name: t("target"),
cell: (row) => row["target"], cell: (row) => row["target"],
}, },
{ {
id: "via", id: "via",
name: "via", name: t("via"),
cell: (row) => (row["via"] ? row["via"] : "(LAN)"), cell: (row) => (row["via"] ? row["via"] : "(LAN)"),
}, },
]; ];
@ -84,13 +87,13 @@ function ManagedRoutes({ routes, handleChange }) {
return ( return (
<> <>
<Typography style={{ paddingBottom: "10px" }}> <Typography style={{ paddingBottom: "10px" }}>
Managed Routes ({routes.length + "/32"}) {t("managedRoutes")} ({routes.length + "/128"})
</Typography> </Typography>
<Box border={1} borderColor="grey.300"> <Box border={1} borderColor="grey.300">
<Grid item style={{ margin: "10px" }}> <Grid item style={{ margin: "10px" }}>
<DataTable noHeader={true} columns={columns} data={routes} /> <DataTable noHeader={true} columns={columns} data={routes} />
<Divider /> <Divider />
<Typography>Add Routes</Typography> <Typography>{t("addRoute")}</Typography>
<List <List
style={{ style={{
display: "flex", display: "flex",
@ -100,7 +103,7 @@ function ManagedRoutes({ routes, handleChange }) {
<TextField <TextField
value={destination} value={destination}
onChange={handleDestinationInput} onChange={handleDestinationInput}
placeholder={"Destination (CIDR)"} placeholder={t("destination") + " (CIDR)"}
/> />
<Divider <Divider
orientation="vertical" orientation="vertical"
@ -112,7 +115,7 @@ function ManagedRoutes({ routes, handleChange }) {
<TextField <TextField
value={via} value={via}
onChange={handleViaInput} onChange={handleViaInput}
placeholder={"Via (Optional)"} placeholder={t("via") + " (" + t("optional") + ")"}
/> />
<IconButton size="small" color="primary" onClick={addRouteReq}> <IconButton size="small" color="primary" onClick={addRouteReq}>
<AddIcon <AddIcon

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

View file

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

View file

@ -1,3 +1,4 @@
// @ts-nocheck
/* eslint-disable */ /* eslint-disable */
// rule-compiler.js from ZeroTierOne before its relicensing // rule-compiler.js from ZeroTierOne before its relicensing
// source: // source:

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

View file

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

View file

@ -11,7 +11,10 @@ import { useLocalStorage } from "react-use";
import API from "utils/API"; import API from "utils/API";
import useStyles from "./Network.styles"; import useStyles from "./Network.styles";
import { useTranslation } from "react-i18next";
function Network() { function Network() {
const { t, i18n } = useTranslation();
const { nwid } = useParams(); const { nwid } = useParams();
const [loggedIn] = useLocalStorage("loggedIn", false); const [loggedIn] = useLocalStorage("loggedIn", false);
const [network, setNetwork] = useState({}); const [network, setNetwork] = useState({});
@ -39,10 +42,12 @@ function Network() {
if (loggedIn) { if (loggedIn) {
return ( return (
<> <>
<div className={classes.breadcrumbs}>
<Link color="inherit" component={RouterLink} to="/" underline="none"> <Link color="inherit" component={RouterLink} to="/" underline="none">
<ArrowBackIcon className={classes.backIcon}></ArrowBackIcon> <ArrowBackIcon className={classes.backIcon}></ArrowBackIcon>
Networks {t("network", { count: 2 })}
</Link> </Link>
</div>
<div className={classes.container}> <div className={classes.container}>
{network["config"] && ( {network["config"] && (
<> <>
@ -71,9 +76,7 @@ function Network() {
}} }}
> >
<Grid item xs={10}> <Grid item xs={10}>
<Typography variant="h5"> <Typography variant="h5">{t("notAuthorized")}</Typography>
You are not authorized. Please Log In
</Typography>
</Grid> </Grid>
</Grid> </Grid>
); );

View file

@ -5,7 +5,11 @@ const useStyles = makeStyles((theme) => ({
fontSize: 12, fontSize: 12,
}, },
container: { container: {
margin: "1%", margin: "3%",
},
breadcrumbs: {
paddingTop: "2%",
paddingLeft: "2%",
}, },
})); }));

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

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

View file

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

1
frontend/src/types.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module "*.png";

View file

@ -10,6 +10,6 @@ export default axios.create({
localStorage.getItem("disableAuth") === "true" localStorage.getItem("disableAuth") === "true"
? {} ? {}
: { : {
Authorization: `Bearer ${JSON.parse(localStorage.getItem("token"))}`, Authorization: `token ${JSON.parse(localStorage.getItem("token"))}`,
}, },
}); });

View 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
View file

@ -0,0 +1,12 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"baseUrl": "src",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "preserve",
"resolveJsonModule": true
},
"include": ["src"]
}

View 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
View 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: "",
},
],
}),
],
});

View file

@ -1,33 +1,38 @@
{ {
"name": "zero-ui", "name": "zero-ui",
"version": "1.4.0", "version": "1.5.8",
"workspaces": [ "workspaces": [
"backend", "backend",
"frontend" "frontend"
], ],
"scripts": { "scripts": {
"postinstall": "husky install", "postinstall": "husky install",
"upgradeDeps": "yarn upgrade-interactive", "upgrade:deps": "yarn upgrade-interactive",
"cleanDeps": "cd frontend && rimraf node_modules && cd ../backend && rimraf node_modules && cd .. && rimraf node_modules", "upgrade:yarn": "yarn set version latest",
"lint": "yarn prettier --write .", "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\"", "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", "prod": "cd backend && cross-env NODE_ENV=production ZU_SECURE_HEADERS=false yarn start",
"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", "release": "standard-version && git push --follow-tags origin main && git add CHANGELOG.md",
"commit": "yarn git-cz" "commit": "yarn git-cz"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.0.2", "@commitlint/cli": "^17.7.2",
"@commitlint/config-conventional": "^17.0.2", "@commitlint/config-conventional": "^17.7.0",
"commitizen": "^4.2.4", "commitizen": "^4.3.0",
"concurrently": "^7.2.2", "concurrently": "^8.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"husky": "^8.0.1", "husky": "^8.0.3",
"lint-staged": "^13.0.2", "lint-staged": "^14.0.1",
"nodemon": "^2.0.16", "nodemon": "^3.0.1",
"prettier": "^2.7.1", "prettier": "^3.0.3",
"rimraf": "^3.0.2", "rimraf": "^5.0.5",
"standard-version": "^9.5.0" "standard-version": "^9.5.0"
}, },
"prettier": { "prettier": {
@ -51,5 +56,5 @@
"yarn prettier --write" "yarn prettier --write"
] ]
}, },
"packageManager": "yarn@3.2.1" "packageManager": "yarn@4.3.1"
} }

13
tsconfig.json Normal file
View 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
}
}

19118
yarn.lock

File diff suppressed because it is too large Load diff