├── .dockerignore
├── .github
    ├── CODEOWNERS
    ├── ISSUE_TEMPLATE
    │   ├── 01_BUG_REPORT.md
    │   ├── 02_FEATURE_REQUEST.md
    │   ├── 03_CODEBASE_IMPROVEMENT.md
    │   └── config.yml
    ├── PULL_REQUEST_TEMPLATE.md
    ├── labels.yml
    └── workflows
    │   ├── codeql-analysis.yml
    │   ├── labels.yml
    │   └── main.yml
├── .gitignore
├── .husky
    ├── .gitignore
    ├── commit-msg
    └── pre-commit
├── .prettierignore
├── .yarnrc.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── backend
    ├── .eslintrc.json
    ├── .gitignore
    ├── app.js
    ├── bin
    │   └── www.js
    ├── data
    │   └── .keep
    ├── global.d.ts
    ├── package.json
    ├── routes
    │   ├── auth.js
    │   ├── controller.js
    │   ├── member.js
    │   └── network.js
    ├── services
    │   ├── auth.js
    │   ├── member.js
    │   └── network.js
    ├── tsconfig.json
    └── utils
    │   ├── constants.js
    │   ├── controller-api.js
    │   ├── db.js
    │   ├── init-admin.js
    │   ├── ping.js
    │   └── zt-address.js
├── docker-compose.yml
├── docker
    └── zero-ui
    │   └── Dockerfile
├── docs
    ├── SCREENSHOTS.md
    ├── SECURITY.md
    └── images
    │   ├── homepage.png
    │   ├── logo.png
    │   └── network.png
├── frontend
    ├── .eslintrc.json
    ├── .gitignore
    ├── index.html
    ├── package.json
    ├── public
    │   ├── favicon.ico
    │   ├── locales
    │   │   ├── en
    │   │   │   └── common.json
    │   │   ├── es-ES
    │   │   │   └── common.json
    │   │   ├── ru-RU
    │   │   │   └── common.json
    │   │   └── zh_CN
    │   │   │   └── common.json
    │   ├── manifest.json
    │   └── robots.txt
    ├── src
    │   ├── App.jsx
    │   ├── components
    │   │   ├── Bar
    │   │   │   ├── Bar.jsx
    │   │   │   ├── assets
    │   │   │   │   └── logo.png
    │   │   │   └── index.jsx
    │   │   ├── HomeLoggedIn
    │   │   │   ├── HomeLoggedIn.jsx
    │   │   │   ├── HomeLoggedIn.styles.jsx
    │   │   │   ├── components
    │   │   │   │   └── NetworkButton
    │   │   │   │   │   ├── NetworkButton.css
    │   │   │   │   │   ├── NetworkButton.jsx
    │   │   │   │   │   ├── NetworkButton.styles.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   └── index.jsx
    │   │   ├── HomeLoggedOut
    │   │   │   ├── HomeLoggedOut.jsx
    │   │   │   └── index.jsx
    │   │   ├── Loading
    │   │   │   ├── Loading.jsx
    │   │   │   ├── Loading.styles.jsx
    │   │   │   └── index.jsx
    │   │   ├── LogIn
    │   │   │   ├── LogIn.jsx
    │   │   │   ├── components
    │   │   │   │   ├── LogInToken
    │   │   │   │   │   ├── LogInToken.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   │   └── LogInUser
    │   │   │   │   │   ├── LogInUser.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   └── index.jsx
    │   │   ├── NetworkHeader
    │   │   │   ├── NetworkHeader.jsx
    │   │   │   └── index.jsx
    │   │   ├── NetworkManagement
    │   │   │   ├── NetworkManagement.jsx
    │   │   │   └── index.jsx
    │   │   ├── NetworkMembers
    │   │   │   ├── NetworkMembers.jsx
    │   │   │   ├── components
    │   │   │   │   ├── AddMember
    │   │   │   │   │   ├── AddMember.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   │   ├── DeleteMember
    │   │   │   │   │   ├── DeleteMember.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   │   ├── ManagedIP
    │   │   │   │   │   ├── ManagedIP.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   │   ├── MemberName
    │   │   │   │   │   ├── MemberName.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   │   └── MemberSettings
    │   │   │   │   │   ├── MemberSettings.jsx
    │   │   │   │   │   ├── components
    │   │   │   │   │       └── Tag
    │   │   │   │   │       │   ├── Tag.jsx
    │   │   │   │   │       │   └── index.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   └── index.jsx
    │   │   ├── NetworkRules
    │   │   │   ├── NetworkRules.jsx
    │   │   │   └── index.jsx
    │   │   ├── NetworkSettings
    │   │   │   ├── NetworkSettings.jsx
    │   │   │   ├── components
    │   │   │   │   ├── IPv4AutoAssign
    │   │   │   │   │   ├── IPv4AutoAssign.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   │   └── ManagedRoutes
    │   │   │   │   │   ├── ManagedRoutes.jsx
    │   │   │   │   │   └── index.jsx
    │   │   │   └── index.jsx
    │   │   ├── Settings
    │   │   │   ├── Settings.jsx
    │   │   │   └── index.jsx
    │   │   └── Theme
    │   │   │   ├── Theme.jsx
    │   │   │   └── index.jsx
    │   ├── external
    │   │   └── RuleCompiler.js
    │   ├── generated
    │   │   └── localesList.json
    │   ├── i18n.js
    │   ├── index.css
    │   ├── index.jsx
    │   ├── routes
    │   │   ├── Home
    │   │   │   ├── Home.jsx
    │   │   │   └── index.jsx
    │   │   ├── Network
    │   │   │   ├── Network.jsx
    │   │   │   ├── Network.styles.jsx
    │   │   │   └── index.jsx
    │   │   ├── NotFound
    │   │   │   ├── NotFound.jsx
    │   │   │   └── index.jsx
    │   │   └── Settings
    │   │   │   ├── Settings.jsx
    │   │   │   ├── Settings.styles.jsx
    │   │   │   └── index.jsx
    │   ├── types.d.ts
    │   └── utils
    │   │   ├── API.js
    │   │   ├── ChangeHelper.js
    │   │   ├── IP.js
    │   │   ├── NetworkConfig.js
    │   │   └── localesList.json
    ├── tsconfig.json
    ├── vite-plugin-generate-locales.js
    └── vite.config.mjs
├── package.json
├── tsconfig.json
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
 1 | .git
 2 | .github
 3 | *Dockerfile*
 4 | *docker-compose*
 5 | node_modules
 6 | .eslintrc.json
 7 | .DS_Store
 8 | tmp
 9 | temp
10 | logs
11 | *.log
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 | 
16 | .yarn/cache
17 | db.json
18 | backend/data/db.json
19 | 
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @dec0dOS
2 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/01_BUG_REPORT.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug Report
 3 | about: Create a report to help ZeroUI to improve
 4 | title: "bug: "
 5 | labels: "bug"
 6 | assignees: ""
 7 | ---
 8 | 
 9 | # Bug Report
10 | 
11 | **ZeroUI version:**
12 | 
13 | 
14 | 
15 | latest
16 | 
17 | **Current behavior:**
18 | 
19 | 
20 | 
21 | **Expected behavior:**
22 | 
23 | 
24 | 
25 | **Steps to reproduce:**
26 | 
27 | 
28 | 
29 | **Related code:**
30 | 
31 | 
32 | 
33 | ```
34 | insert short code snippets here
35 | ```
36 | 
37 | **Other information:**
38 | 
39 | 
40 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Feature Request
 3 | about: Suggest an idea for this project
 4 | title: "feat: "
 5 | labels: "new-feature"
 6 | assignees: ""
 7 | ---
 8 | 
 9 | # Feature Request
10 | 
11 | **Describe the Feature Request**
12 | 
13 | 
14 | 
15 | **Describe Preferred Solution**
16 | 
17 | 
18 | 
19 | **Describe Alternatives**
20 | 
21 | 
22 | 
23 | **Related Code**
24 | 
25 | 
26 | 
27 | **Additional Context**
28 | 
29 | 
30 | 
31 | **If the feature request is approved, would you be willing to submit a PR?**
32 | Yes / No _(Help can be provided if you need assistance submitting a PR)_
33 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Codebase improvement
3 | about: Provide your feedback for the existing codebase. Suggest a better solution for algorithms, development tools, etc.
4 | title: "dev: "
5 | labels: "enhancement"
6 | assignees: ""
7 | ---
8 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 |   - name: ZeroUI Community Support
4 |     url: https://github.com/dec0dOS/zero-ui/discussions
5 |     about: Please ask and answer questions here.
6 | 
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | ## Pull Request type
 4 | 
 5 | 
 6 | 
 7 | Please check the type of change your PR introduces:
 8 | 
 9 | - [ ] Bugfix
10 | - [ ] Feature
11 | - [ ] Code style update (formatting, renaming)
12 | - [ ] Refactoring (no functional changes, no API changes)
13 | - [ ] Build-related changes
14 | - [ ] Documentation content changes
15 | - [ ] Other (please describe):
16 | 
17 | ## What is the current behavior?
18 | 
19 | 
20 | 
21 | Issue Number: N/A
22 | 
23 | ## What is the new behavior?
24 | 
25 | 
26 | 
27 | -
28 | -
29 | -
30 | 
31 | ## Does this introduce a breaking change?
32 | 
33 | - [ ] Yes
34 | - [ ] No
35 | 
36 | 
37 | 
38 | ## Other information
39 | 
40 | 
41 | 
--------------------------------------------------------------------------------
/.github/labels.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | - name: "breaking-change"
 3 |   color: ee0701
 4 |   description: "A change that changes the API or breaks backward compatibility for users."
 5 | - name: "bug"
 6 |   color: ee0701
 7 |   description: "Inconsistencies or issues which will cause a problem for users or implementors."
 8 | - name: "documentation"
 9 |   color: 0052cc
10 |   description: "Solely about the documentation of the project."
11 | - name: "enhancement"
12 |   color: 1d76db
13 |   description: "Enhancement of the code, not introducing new features."
14 | - name: "refactor"
15 |   color: 1d76db
16 |   description: "Updating the code with simpler or more efficient syntax or methods. Not introducing new features."
17 | - name: "performance"
18 |   color: 1d76db
19 |   description: "Improving performance of the project, not introducing new features."
20 | - name: "new-feature"
21 |   color: 0e8a16
22 |   description: "New features or options."
23 | - name: "maintenance"
24 |   color: 2af79e
25 |   description: "Generic maintenance tasks."
26 | - name: "ci"
27 |   color: 1d76db
28 |   description: "Work that improves the continuous integration."
29 | - name: "dependencies"
30 |   color: 1d76db
31 |   description: "Change in project dependencies."
32 | 
33 | - name: "in-progress"
34 |   color: fbca04
35 |   description: "Issue is currently being worked on by a developer."
36 | 
37 | - name: "security"
38 |   color: ee0701
39 |   description: "Addressing a vulnerability or security risk in this project."
40 | - name: "incomplete"
41 |   color: fef2c0
42 |   description: "Missing information."
43 | - name: "invalid"
44 |   color: fef2c0
45 |   description: "This is off-topic, spam, or otherwise doesn't apply to this project."
46 | 
47 | - name: "beginner-friendly"
48 |   color: 0e8a16
49 |   description: "Good first issue for people wanting to contribute to this project."
50 | - name: "help-wanted"
51 |   color: 0e8a16
52 |   description: "We need some extra helping hands or expertise in order to resolve this!"
53 | 
54 | - name: "priority-critical"
55 |   color: ee0701
56 |   description: "Must be addressed as soon as possible."
57 | - name: "priority-high"
58 |   color: b60205
59 |   description: "After critical issues are fixed, these should be dealt with before any further issues."
60 | - name: "priority-medium"
61 |   color: 0e8a16
62 |   description: "This issue may be useful, and needs some attention."
63 | - name: "priority-low"
64 |   color: e4ea8a
65 |   description: "Nice addition, maybe... someday..."
66 | 
67 | - name: "major"
68 |   color: b60205
69 |   description: "This PR causes a major bump in the version number."
70 | - name: "minor"
71 |   color: 0e8a16
72 |   description: "This PR causes a minor bump in the version number."
73 | 
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
 1 | name: CodeQL
 2 | 
 3 | on:
 4 |   push:
 5 |     tags:
 6 |       - "v*.*.*"
 7 |   pull_request:
 8 |     branches: [main]
 9 |   schedule:
10 |     - cron: "30 2 * * 6"
11 | 
12 | jobs:
13 |   analyze:
14 |     name: Analyze
15 |     runs-on: ubuntu-latest
16 |     permissions:
17 |       actions: read
18 |       contents: read
19 |       security-events: write
20 | 
21 |     strategy:
22 |       fail-fast: false
23 |       matrix:
24 |         language: ["javascript"]
25 | 
26 |     steps:
27 |       - name: Checkout repository
28 |         uses: actions/checkout@v3
29 | 
30 |       - name: Initialize CodeQL
31 |         uses: github/codeql-action/init@v2
32 |         with:
33 |           languages: ${{ matrix.language }}
34 | 
35 |       - name: Perform CodeQL Analysis
36 |         uses: github/codeql-action/analyze@v2
37 | 
--------------------------------------------------------------------------------
/.github/workflows/labels.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Sync labels
 3 | 
 4 | on:
 5 |   push:
 6 |     branches:
 7 |       - main
 8 |     paths:
 9 |       - .github/labels.yml
10 | 
11 | jobs:
12 |   labels:
13 |     name: ♻️ Sync labels
14 |     runs-on: ubuntu-latest
15 |     steps:
16 |       - name: ⤵️ Check out code from GitHub
17 |         uses: actions/checkout@v2
18 |       - name: 🚀 Run Label Syncer
19 |         uses: micnncim/action-label-syncer@v1
20 |         env:
21 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 | 
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
 1 | name: CI to Docker Hub
 2 | 
 3 | on:
 4 |   push:
 5 |     tags:
 6 |       - "v*.*.*"
 7 |   workflow_dispatch:
 8 | 
 9 | jobs:
10 |   build:
11 |     runs-on: ubuntu-latest
12 |     steps:
13 |       - name: Checkout
14 |         uses: actions/checkout@v3
15 | 
16 |       - name: Set up QEMU
17 |         uses: docker/setup-qemu-action@v2
18 | 
19 |       - name: Set up Docker Buildx
20 |         id: buildx
21 |         uses: docker/setup-buildx-action@v2
22 | 
23 |       - name: Prepare zero-ui
24 |         id: prep_zero-ui
25 |         run: |
26 |           DOCKER_IMAGE=${{ secrets.DOCKER_HUB_USERNAME }}/zero-ui
27 |           VERSION=edge
28 |           if [[ $GITHUB_REF == refs/tags/* ]]; then
29 |             VERSION=${GITHUB_REF#refs/tags/v}
30 |           fi
31 |           TAGS="${DOCKER_IMAGE}:${VERSION}"
32 |           if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
33 |             TAGS="$TAGS,${DOCKER_IMAGE}:latest"
34 |           fi
35 |           echo ::set-output name=tags::${TAGS}
36 | 
37 |       - name: Login to Docker Hub
38 |         uses: docker/login-action@v2
39 |         with:
40 |           username: ${{ secrets.DOCKER_HUB_USERNAME }}
41 |           password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
42 | 
43 |       - name: Build and push zero-ui
44 |         id: docker_build_zero-ui
45 |         uses: docker/build-push-action@v3
46 |         with:
47 |           context: ./
48 |           file: ./docker/zero-ui/Dockerfile
49 |           builder: ${{ steps.buildx.outputs.name }}
50 |           platforms: linux/amd64,linux/arm64,linux/arm
51 |           push: ${{ github.event_name != 'pull_request' }}
52 |           tags: ${{ steps.prep_zero-ui.outputs.tags }}
53 | 
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
  1 | # Misc
  2 | tmp
  3 | temp
  4 | .DS_Store
  5 | .env.local
  6 | .env.development.local
  7 | .env.test.local
  8 | .env.production.local
  9 | 
 10 | # Created by https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node
 11 | # Edit at https://www.toptal.com/developers/gitignore?templates=vscode,yarn,react,node
 12 | 
 13 | ### Node ###
 14 | # Logs
 15 | logs
 16 | *.log
 17 | npm-debug.log*
 18 | yarn-debug.log*
 19 | yarn-error.log*
 20 | lerna-debug.log*
 21 | 
 22 | # Diagnostic reports (https://nodejs.org/api/report.html)
 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 24 | 
 25 | # Runtime data
 26 | pids
 27 | *.pid
 28 | *.seed
 29 | *.pid.lock
 30 | 
 31 | # Directory for instrumented libs generated by jscoverage/JSCover
 32 | lib-cov
 33 | 
 34 | # Coverage directory used by tools like istanbul
 35 | coverage
 36 | *.lcov
 37 | 
 38 | # nyc test coverage
 39 | .nyc_output
 40 | 
 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 42 | .grunt
 43 | 
 44 | # Bower dependency directory (https://bower.io/)
 45 | bower_components
 46 | 
 47 | # node-waf configuration
 48 | .lock-wscript
 49 | 
 50 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 51 | build/Release
 52 | 
 53 | # Dependency directories
 54 | node_modules/
 55 | jspm_packages/
 56 | 
 57 | # TypeScript v1 declaration files
 58 | typings/
 59 | 
 60 | # TypeScript cache
 61 | *.tsbuildinfo
 62 | 
 63 | # Optional npm cache directory
 64 | .npm
 65 | 
 66 | # Optional eslint cache
 67 | .eslintcache
 68 | 
 69 | # Optional stylelint cache
 70 | .stylelintcache
 71 | 
 72 | # Microbundle cache
 73 | .rpt2_cache/
 74 | .rts2_cache_cjs/
 75 | .rts2_cache_es/
 76 | .rts2_cache_umd/
 77 | 
 78 | # Optional REPL history
 79 | .node_repl_history
 80 | 
 81 | # Output of 'npm pack'
 82 | *.tgz
 83 | 
 84 | # Yarn Integrity file
 85 | .yarn-integrity
 86 | 
 87 | # dotenv environment variables file
 88 | .env
 89 | .env.test
 90 | .env*.local
 91 | 
 92 | # parcel-bundler cache (https://parceljs.org/)
 93 | .cache
 94 | .parcel-cache
 95 | 
 96 | # Next.js build output
 97 | .next
 98 | 
 99 | # Nuxt.js build / generate output
100 | .nuxt
101 | dist
102 | 
103 | # Gatsby files
104 | .cache/
105 | # Comment in the public line in if your project uses Gatsby and not Next.js
106 | # https://nextjs.org/blog/next-9-1#public-directory-support
107 | # public
108 | 
109 | # vuepress build output
110 | .vuepress/dist
111 | 
112 | # Serverless directories
113 | .serverless/
114 | 
115 | # FuseBox cache
116 | .fusebox/
117 | 
118 | # DynamoDB Local files
119 | .dynamodb/
120 | 
121 | # TernJS port file
122 | .tern-port
123 | 
124 | # Stores VSCode versions used for testing VSCode extensions
125 | .vscode-test
126 | 
127 | ### react ###
128 | .DS_*
129 | **/*.backup.*
130 | **/*.back.*
131 | 
132 | node_modules
133 | 
134 | *.sublime*
135 | 
136 | psd
137 | thumb
138 | sketch
139 | 
140 | ### vscode ###
141 | .vscode/*
142 | !.vscode/settings.json
143 | !.vscode/tasks.json
144 | !.vscode/launch.json
145 | !.vscode/extensions.json
146 | *.code-workspace
147 | 
148 | ### yarn ###
149 | # https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored
150 | 
151 | .yarn/*
152 | !.yarn/releases
153 | !.yarn/patches
154 | !.yarn/plugins
155 | !.yarn/sdks
156 | !.yarn/versions
157 | 
158 | # End of https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node
159 | 
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 | 
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 | 
4 | yarn commitlint --edit $1
5 | 
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 | 
4 | yarn lint-staged
5 | 
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | frontend/build
2 | .yarn
3 | tmp
4 | 
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | enableGlobalCache: true
2 | 
3 | enableTelemetry: false
4 | 
5 | nodeLinker: node-modules
6 | 
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
  1 | # Changelog
  2 | 
  3 | 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.
  4 | 
  5 | ### [1.5.8](https://github.com/dec0dOS/zero-ui/compare/v1.5.7...v1.5.8) (2023-10-04)
  6 | 
  7 | ### [1.5.7](https://github.com/dec0dOS/zero-ui/compare/v1.5.6...v1.5.7) (2023-10-04)
  8 | 
  9 | ### [1.5.6](https://github.com/dec0dOS/zero-ui/compare/v1.5.5...v1.5.6) (2023-10-04)
 10 | 
 11 | ### [1.5.5](https://github.com/dec0dOS/zero-ui/compare/v1.5.4...v1.5.5) (2023-10-04)
 12 | 
 13 | ### [1.5.4](https://github.com/dec0dOS/zero-ui/compare/v1.5.3...v1.5.4) (2023-10-04)
 14 | 
 15 | ### [1.5.3](https://github.com/dec0dOS/zero-ui/compare/v1.5.2...v1.5.3) (2023-10-04)
 16 | 
 17 | ### [1.5.2](https://github.com/dec0dOS/zero-ui/compare/v1.5.1...v1.5.2) (2023-10-04)
 18 | 
 19 | ### Bug Fixes
 20 | 
 21 | - fix network member api resp handling ([856682b](https://github.com/dec0dOS/zero-ui/commit/856682bad1ccd46970681e45bea8a992043c38f4))
 22 | - ping peer response handling ([db8f497](https://github.com/dec0dOS/zero-ui/commit/db8f4979e65d23d93de99ffa428c9b9a3d3fd952))
 23 | - revert fix for 1.12.0 ([5d041f6](https://github.com/dec0dOS/zero-ui/commit/5d041f6db63345950cb5782d586c71e0402b7ce7))
 24 | 
 25 | ### [1.5.1](https://github.com/dec0dOS/zero-ui/compare/v1.5.0...v1.5.1) (2022-10-09)
 26 | 
 27 | ### Bug Fixes
 28 | 
 29 | - **backend/app.js:** fix internal error handler ([15e4051](https://github.com/dec0dOS/zero-ui/commit/15e405162590b2e79dfc32751625f5425613bc52))
 30 | - **backend/services/member.js:** correctly delete members with unset additionalData ([450a6ad](https://github.com/dec0dOS/zero-ui/commit/450a6ad19414723ce00c48caba98743143a3041f))
 31 | 
 32 | ## [1.5.0](https://github.com/dec0dOS/zero-ui/compare/v1.4.1...v1.5.0) (2022-08-25)
 33 | 
 34 | ### Features
 35 | 
 36 | - **backend:** add cron and ping members ([d3fdac6](https://github.com/dec0dOS/zero-ui/commit/d3fdac61bdd95c7ff42e7db373cd3973d42ca8ce))
 37 | - last online ([40f98cc](https://github.com/dec0dOS/zero-ui/commit/40f98cc9df322f2b8b4c4a8baed96c96fd2c56d7))
 38 | 
 39 | ### [1.4.1](https://github.com/dec0dOS/zero-ui/compare/v1.4.0...v1.4.1) (2022-06-22)
 40 | 
 41 | ## [1.4.0](https://github.com/dec0dOS/zero-ui/compare/v1.3.2...v1.4.0) (2022-06-18)
 42 | 
 43 | ### Features
 44 | 
 45 | - support http basic auth ([2396e97](https://github.com/dec0dOS/zero-ui/commit/2396e973dc4e40f247cb5fef75d0403ccf0a285a))
 46 | 
 47 | ### [1.3.2](https://github.com/dec0dOS/zero-ui/compare/v1.3.1...v1.3.2) (2022-06-18)
 48 | 
 49 | ### Bug Fixes
 50 | 
 51 | - better zt controller error handling ([d7f2e15](https://github.com/dec0dOS/zero-ui/commit/d7f2e153286f9e1dacf4d9fa993321fd6fbc3836))
 52 | - correct conditional for enabling bearer token ([f30dec6](https://github.com/dec0dOS/zero-ui/commit/f30dec6eacfe0d2ac0031861b4f22f34dbab32c7))
 53 | - disable authentication properly ([75933d7](https://github.com/dec0dOS/zero-ui/commit/75933d7e59838f7c8728ca08cf39659f24a6cac6))
 54 | - simplify code and check login status on home page load ([ddb3f44](https://github.com/dec0dOS/zero-ui/commit/ddb3f442f85991db4fa0721f0d7c2b004a9ea12d))
 55 | - stop redundant fetching /auth/login ([ce9f794](https://github.com/dec0dOS/zero-ui/commit/ce9f7943c04d117b0ace3025cd9f84d7b14cf5f3))
 56 | - update disableAuth in localStorage if server config changes ([036e577](https://github.com/dec0dOS/zero-ui/commit/036e5779ba319a63c9d749c32fcbd5452d2bd2d2))
 57 | 
 58 | ### [1.3.1](https://github.com/dec0dOS/zero-ui/compare/v1.3.0...v1.3.1) (2022-06-12)
 59 | 
 60 | ## [1.3.0](https://github.com/dec0dOS/zero-ui/compare/v1.2.2...v1.3.0) (2022-05-24)
 61 | 
 62 | ### Features
 63 | 
 64 | - disable auth ([#59](https://github.com/dec0dOS/zero-ui/issues/59)) ([e7fb4d0](https://github.com/dec0dOS/zero-ui/commit/e7fb4d0aa84c26493b58a1cd3349fd98a2861191))
 65 | 
 66 | ### [1.2.2](https://github.com/dec0dOS/zero-ui/compare/v1.2.1...v1.2.2) (2022-04-26)
 67 | 
 68 | ### Bug Fixes
 69 | 
 70 | - fix crash when network had no IP ranges ([8095d2b](https://github.com/dec0dOS/zero-ui/commit/8095d2bea235e348baf3bac515d8aa9eb7adb8cf))
 71 | 
 72 | ### [1.2.1](https://github.com/dec0dOS/zero-ui/compare/v1.2.0...v1.2.1) (2021-12-19)
 73 | 
 74 | ## [1.2.0](https://github.com/dec0dOS/zero-ui/compare/v1.1.5...v1.2.0) (2021-12-19)
 75 | 
 76 | ### Features
 77 | 
 78 | - update_frontend_online_parsing ([cd6699d](https://github.com/dec0dOS/zero-ui/commit/cd6699d9b7d90c514dd2b870da4b61b8a5ea4ea0))
 79 | 
 80 | ### [1.1.5](https://github.com/dec0dOS/zero-ui/compare/v1.1.4...v1.1.5) (2021-12-16)
 81 | 
 82 | ### [1.1.4](https://github.com/dec0dOS/zero-ui/compare/v1.1.3...v1.1.4) (2021-12-16)
 83 | 
 84 | ### [1.1.3](https://github.com/dec0dOS/zero-ui/compare/v1.1.2...v1.1.3) (2021-12-16)
 85 | 
 86 | ### [1.1.2](https://github.com/dec0dOS/zero-ui/compare/v1.1.1...v1.1.2) (2021-12-16)
 87 | 
 88 | ### [1.1.1](https://github.com/dec0dOS/zero-ui/compare/v1.1.0...v1.1.1) (2021-12-16)
 89 | 
 90 | ### Bug Fixes
 91 | 
 92 | - migrate to yarn v3 ([781a48d](https://github.com/dec0dOS/zero-ui/commit/781a48d341bf386cfbc917c78789f802227bfdef))
 93 | 
 94 | ## [1.1.0](https://github.com/dec0dOS/zero-ui/compare/v1.0.21...v1.1.0) (2021-12-16)
 95 | 
 96 | ### Features
 97 | 
 98 | - add capabilities and tags support ([8f89174](https://github.com/dec0dOS/zero-ui/commit/8f891747d6d98c0957405954f50c5bab5d2f9551)), closes [#35](https://github.com/dec0dOS/zero-ui/issues/35)
 99 | - some improvements in ui for caps and tags ([2f27c11](https://github.com/dec0dOS/zero-ui/commit/2f27c112ea2e05f8ca6de8219179d51261ab721f))
100 | 
101 | ### Bug Fixes
102 | 
103 | - fix compatibility for existing networks after supporting tags and capabilities ([a813d05](https://github.com/dec0dOS/zero-ui/commit/a813d05b3c2c2c286ae1df860eca209215347ce0))
104 | - fix original managed IP delete ([9386aa7](https://github.com/dec0dOS/zero-ui/commit/9386aa724b67019e0783691f611fc08877cbfe85)), closes [#12](https://github.com/dec0dOS/zero-ui/issues/12)
105 | - parse tags and capabilities in flow rules correctly ([369d96e](https://github.com/dec0dOS/zero-ui/commit/369d96e50ab523c85123e6d783c44d012e7756ed))
106 | - rename "tag enum id" to "tag value" ([27aa2c5](https://github.com/dec0dOS/zero-ui/commit/27aa2c5d47d99c329d1e80b60a08307e30239db1))
107 | 
108 | ### [1.0.21](https://github.com/dec0dOS/zero-ui/compare/v1.0.20...v1.0.21) (2021-11-04)
109 | 
110 | ### [1.0.20](https://github.com/dec0dOS/zero-ui/compare/v1.0.19...v1.0.20) (2021-08-24)
111 | 
112 | ### [1.0.19](https://github.com/dec0dOS/zero-ui/compare/v1.0.18...v1.0.19) (2021-08-24)
113 | 
114 | ### [1.0.18](https://github.com/dec0dOS/zero-ui/compare/v1.0.17...v1.0.18) (2021-08-24)
115 | 
116 | ### [1.0.17](https://github.com/dec0dOS/zero-ui/compare/v1.0.16...v1.0.17) (2021-08-24)
117 | 
118 | ### [1.0.16](https://github.com/dec0dOS/zero-ui/compare/v1.0.15...v1.0.16) (2021-08-24)
119 | 
120 | ### [1.0.15](https://github.com/dec0dOS/zero-ui/compare/v1.0.14...v1.0.15) (2021-08-24)
121 | 
122 | ### [1.0.14](https://github.com/dec0dOS/zero-ui/compare/v1.0.13...v1.0.14) (2021-08-24)
123 | 
124 | ### [1.0.13](https://github.com/dec0dOS/zero-ui/compare/v1.0.12...v1.0.13) (2021-08-24)
125 | 
126 | ### [1.0.12](https://github.com/dec0dOS/zero-ui/compare/v1.0.11...v1.0.12) (2021-08-24)
127 | 
128 | ### [1.0.11](https://github.com/dec0dOS/zero-ui/compare/v1.0.10...v1.0.11) (2021-07-08)
129 | 
130 | ### Bug Fixes
131 | 
132 | - database member data init fix. Thanks [@thelittlerocket](https://github.com/thelittlerocket)" ([92091d9](https://github.com/dec0dOS/zero-ui/commit/92091d9ea52ad3d64a898d8549cd4f185dbe78eb))
133 | - listen on ipv4 by default. May prevent issues on VPS with ipv6 enabled ([eb17cab](https://github.com/dec0dOS/zero-ui/commit/eb17cab75443edc5082146eb513615dc58d3f759))
134 | 
135 | ### [1.0.10](https://github.com/dec0dOS/zero-ui/compare/v1.0.9...v1.0.10) (2021-05-15)
136 | 
137 | ### [1.0.9](https://github.com/dec0dOS/zero-ui/compare/v1.0.8...v1.0.9) (2021-05-15)
138 | 
139 | ### [1.0.8](https://github.com/dec0dOS/zero-ui/compare/v1.0.7...v1.0.8) (2021-05-15)
140 | 
141 | ### [1.0.6](https://github.com/dec0dOS/zero-ui/compare/v1.0.5...v1.0.6) (2021-05-15)
142 | 
143 | ### [1.0.5](https://github.com/dec0dOS/zero-ui/compare/v1.0.4...v1.0.5) (2021-04-16)
144 | 
145 | ### Bug Fixes
146 | 
147 | - **backend/routes/network.js:** fixes bug after refactor ([112476e](https://github.com/dec0dOS/zero-ui/commit/112476e7fc2850ea7caef9c996d1b2610031395c))
148 | 
149 | ### [1.0.4](https://github.com/dec0dOS/zero-ui/compare/v1.0.3...v1.0.4) (2021-04-16)
150 | 
151 | ### Bug Fixes
152 | 
153 | - **backend/app.js:** fixes database initialization ([0aa2eed](https://github.com/dec0dOS/zero-ui/commit/0aa2eed17a96f97c42fa1fe953d27d1419ea91e2))
154 | 
155 | ### [1.0.3](https://github.com/dec0dOS/zero-ui/compare/v1.0.2...v1.0.3) (2021-03-22)
156 | 
157 | ### [1.0.2](https://github.com/dec0dOS/zero-ui/compare/v1.0.1...v1.0.2) (2021-03-22)
158 | 
159 | ### Bug Fixes
160 | 
161 | - merge commands in package.json ([6aa6f1d](https://github.com/dec0dOS/zero-ui/commit/6aa6f1d69bd399e985f6a20cd2c79e51a3fd1238))
162 | 
163 | ### [1.0.1](https://github.com/dec0dOS/zero-ui/compare/v1.0.0...v1.0.1) (2021-03-22)
164 | 
165 | ### Bug Fixes
166 | 
167 | - preserve data directory in git ([e3f613d](https://github.com/dec0dOS/zero-ui/commit/e3f613ddeb66b6f6b55cbbfd29d88c07df00a598))
168 | - tooling config ([548bf76](https://github.com/dec0dOS/zero-ui/commit/548bf764584cca6ba28ea6574d404d77d6ce84fb))
169 | 
170 | ## 1.0.0 (2021-03-21)
171 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  3 |   
  4 |      5 |   
  6 | 
  7 |
  5 |   
  6 | 
  7 |   
  8 |     ZeroUI - ZeroTier Controller Web UI - is a web user interface for a self-hosted ZeroTier network controller.
  9 |     
 10 |     Explore the screenshots »
 11 |     
 12 |     
 13 |     Bug Report
 14 |     ·
 15 |     Feature Request
 16 |     ·
 17 |     Ask a Question
 18 |   
 19 | 
 20 | 
 21 | 
 22 | Table of Contents
 23 | 
 24 | - [About](#about)
 25 |   - [Built With](#built-with)
 26 | - [Getting Started](#getting-started)
 27 |   - [Prerequisites](#prerequisites)
 28 |   - [Installation](#installation)
 29 | - [Usage](#usage)
 30 |   - [Update](#update)
 31 |   - [Backup](#backup)
 32 | - [Roadmap](#roadmap)
 33 | - [Contributing](#contributing)
 34 |   - [Development environment](#development-environment)
 35 | - [Support](#support)
 36 | - [Security](#security)
 37 | - [Copyright notice](#copyright-notice)
 38 | - [License](#license)
 39 | 
 40 |  
 41 | 
 42 | ---
 43 | 
 44 | ## About
 45 | 
 46 | 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:
 47 | 
 48 | - 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.
 49 | - 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.
 50 | - 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).
 51 | - 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.
 52 | - Deploying ZeroUI is straightforward; refer to the [installation](#installation) section for more information.
 53 | 
 54 | 
 55 | Curious about ZeroTier?
 56 | 
 57 | 
 58 | [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.
 59 | 
 60 | In essence, ZeroTier combines the capabilities of VPN and SD-WAN, simplifying network management.
 61 | 
 62 |  
 63 | 
 64 | ## Built With
 65 | 
 66 | Frontend:
 67 | 
 68 | - [React](https://reactjs.org)
 69 | - [Material UI](https://material-ui.com)
 70 | 
 71 | Backend:
 72 | 
 73 | - [NodeJS](https://nodejs.org)
 74 | - [Express](https://expressjs.com)
 75 | - [Lowdb](https://github.com/typicode/lowdb)
 76 | 
 77 | Ready-to-use deployment solution:
 78 | 
 79 | - [Docker](https://www.docker.com)
 80 | - [Docker Compose](https://docs.docker.com/compose/)
 81 | - [Caddy](https://caddyserver.com)
 82 | 
 83 | ## Getting Started
 84 | 
 85 | ### Prerequisites
 86 | 
 87 | 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).
 88 | 
 89 | For HTTPS setup, you will need a domain name. You can obtain one for free at https://www.duckdns.org.
 90 | 
 91 | ### Installation
 92 | 
 93 | Here's a straightforward one-minute installation guide, perfect for a fresh VPS setup:
 94 | 
 95 | 1. Create a project directory
 96 | 
 97 | ```sh
 98 | mkdir -p /srv/zero-ui/
 99 | cd /srv/zero-ui/
100 | ```
101 | 
102 | 2. Download the `docker-compose.yml` file
103 | 
104 | ```sh
105 | wget https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml
106 | ```
107 | 
108 | or
109 | 
110 | ```sh
111 | curl -L -O https://raw.githubusercontent.com/dec0dOS/zero-ui/main/docker-compose.yml
112 | ```
113 | 
114 | 3. Replace `YOURDOMAIN.com` with your domain name and set admin credentials (`ZU_DEFAULT_PASSWORD`) in `docker-compose.yml`
115 | 4. Pull the image
116 | 
117 | ```sh
118 | docker pull dec0dos/zero-ui
119 | ```
120 | 
121 | 5. Run the containers
122 | 
123 | ```sh
124 | docker compose up -d --no-build
125 | ```
126 | 
127 | 6. Check if everything is okay (`CTRL-C` to stop log preview)
128 | 
129 | ```sh
130 | docker compose logs -f
131 | ```
132 | 
133 | 7. Disable your firewall for the following ports: `80/tcp`, `443/tcp`, and `9993/udp`
134 | 
135 | - On Ubuntu/Debian with ufw installed:
136 | 
137 | ```sh
138 | ufw allow 80/tcp
139 | ufw allow 443/tcp
140 | ufw allow 9993/udp
141 | ```
142 | 
143 | - Or you can use iptables:
144 | 
145 | ```sh
146 | iptables -A INPUT -p tcp --dport 80 -j ACCEPT
147 | iptables -A INPUT -p tcp --dport 443 -j ACCEPT
148 | iptables -A INPUT -p udp --dport 9993 -j ACCEPT
149 | ```
150 | 
151 | 8. Navigate to `https://YOURDOMAIN.com/app/`.
152 |    Now you can use your ZeroUI instance with HTTPS support and automated certificate renewal.
153 | 
154 | > 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`.
155 | 
156 | Advanced manual setups are also supported. Check the following environment variables as a reference:
157 | | Name | Default value | Description |
158 | | ---------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
159 | | NODE_ENV | unset | You could learn more [here](https://nodejs.dev/learn/nodejs-the-difference-between-development-and-production) |
160 | | LISTEN_ADDRESS | `0.0.0.0` | Express server listen address |
161 | | ZU_SERVE_FRONTEND | `true` | You could disable frontend serving and use ZeroUI instance as REST API for your ZeroTier controller |
162 | | ZU_SECURE_HEADERS | `true` | Enables [helmet](https://helmetjs.github.io) |
163 | | ZU_CONTROLLER_ENDPOINT | `http://localhost:9993/` | ZeroTier controller API endpoint |
164 | | ZU_CONTROLLER_TOKEN | from `/var/lib/zerotier-one/authtoken.secret` | ZeroTier controller API token |
165 | | ZU_DEFAULT_USERNAME | unset (`docker-compose.yml`: admin) | Default username that will be set on the first run |
166 | | ZU_DEFAULT_PASSWORD | unset (`docker-compose.yml`: zero-ui) | Default password that will be set on the first run |
167 | | ZU_DATAPATH | `data/db.json` | ZeroUI data storage path |
168 | | 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 |
169 | | ZU_LAST_SEEN_FETCH | `true`| Enables [Last Seen feature](https://github.com/dec0dOS/zero-ui/issues/40) |
170 | | ZU_LAST_SEEN_SCHEDULE | `*/5 * * * *` | Last Seen cron-like schedule |
171 | | ZU_LOGIN_LIMIT | `false` | Enable rate limiter for /login endpoint |
172 | | ZU_LOGIN_LIMIT_WINDOW | 30 | The duration of the IP ban in minutes |
173 | | ZU_LOGIN_LIMIT_ATTEMPTS | 50 | Login attemps before ban |
174 | 
175 | 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).
176 | 
177 | For Ansible Role, please refer to the [zero-ui-ansible repository](https://github.com/dec0dOS/zero-ui-ansible).
178 | 
179 | 
180 | Controller Setup Tips (Outside Docker)
181 | 
182 | 
183 | 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:
184 | 
185 | 1. Allow controller management from any IP address:
186 | 
187 | ```sh
188 | echo "{\"settings\": {\"portMappingEnabled\": true,\"softwareUpdate\": \"disable\",\"allowManagementFrom\": [\"0.0.0.0/0\"]}}" > /var/lib/zerotier-one/local.conf
189 | ```
190 | 
191 | > 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.
192 | 
193 | 2. Add `network_mode: "host"` to zero-ui in `docker-compose.yml`.
194 | 
195 | For more information, please refer to this [discussion](https://github.com/dec0dOS/zero-ui/discussions/8).
196 | 
197 |  
198 | 
199 | ## Usage
200 | 
201 | After installation, log in with the credentials declared with `ZU_DEFAULT_USERNAME` and `ZU_DEFAULT_PASSWORD`.
202 | 
203 | Currently, some main ZeroTier Central features are missing. Refer to the [roadmap](#roadmap) for more information.
204 | 
205 | _For screenshots, please refer to the [screenshots](docs/SCREENSHOTS.md) section._
206 | 
207 | ### Update
208 | 
209 | To get the latest version, simply run
210 | 
211 | ```sh
212 | docker compose pull && docker compose up -d --no-build
213 | ```
214 | 
215 | 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.
216 | 
217 | ### Backup
218 | 
219 | 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:
220 | 
221 | ```sh
222 | tar cvf backup-ui.tar data/
223 | tar cvf backup-zt.tar zerotier-one/
224 | ```
225 | 
226 | ## Roadmap
227 | 
228 | For a list of proposed features (and known issues), see the [open issues](https://github.com/dec0dOS/zero-ui/issues).
229 | 
230 | - [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)
231 | - [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)
232 | - [Newest Bugs](https://github.com/dec0dOS/zero-ui/issues?q=is%3Aopen+is%3Aissue+label%3Abug)
233 | 
234 | [](https://github.com/dec0dOS/zero-ui/issues)
235 | 
236 | When creating bug reports, please ensure they are:
237 | 
238 | - _Reproducible._ Include steps to reproduce the problem.
239 | - _Specific._ Provide as much detail as possible, including version, environment, etc.
240 | - _Unique._ Avoid duplicating existing open issues.
241 | - _Scoped to a Single Bug._ Report one bug per issue.
242 | 
243 | ## Contributing
244 | 
245 | 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.
246 | 
247 | To contribute:
248 | 
249 | 1. Fork the project.
250 | 2. Create your feature branch (`git checkout -b feat/amazing_feature`).
251 | 3. Commit your changes (`git commit -m 'feat: add amazing_feature'`).
252 | 4. Push to the branch (`git push origin feat/amazing_feature`).
253 | 5. [Open a Pull Request](https://github.com/dec0dOS/zero-ui/compare?expand=1)
254 | 
255 | 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.
256 | 
257 | ### Development Environment
258 | 
259 | To set up a development environment, follow these steps:
260 | 
261 | 1. Clone the repo
262 | 
263 | ```sh
264 | git clone https://github.com/dec0dOS/zero-ui.git
265 | cd zero-ui
266 | ```
267 | 
268 | 2. Install packages
269 | 
270 | ```sh
271 | yarn install
272 | ```
273 | 
274 | 3. Start the development server
275 | 
276 | ```sh
277 | yarn dev
278 | ```
279 | 
280 | 4. Navigate to http://localhost:3000
281 | 
282 | 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:
283 | 
284 | ```sh
285 | sudo cat "/Library/Application Support/ZeroTier/One/authtoken.secret"
286 | ```
287 | 
288 | Afterward, you can start the ZeroUI development environment:
289 | 
290 | ```sh
291 | ZU_CONTROLLER_TOKEN=TOKEN_FROM_authtoken.secret yarn dev
292 | ```
293 | 
294 | For other platforms, please refer to the [ZeroTier manual](https://docs.zerotier.com/service/v1/).
295 | 
296 | ## Support
297 | 
298 | If you need assistance or have questions, reach out through [GitHub Discussions](https://github.com/dec0dOS/zero-ui/discussions).
299 | 
300 | ## Security
301 | 
302 | 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.
303 | 
304 | For enterprise support and a more reliable and scalable solution, please consider using ZeroTier Central.
305 | 
306 | For more information and to report security issues, please refer to our [security documentation](docs/SECURITY.md).
307 | 
308 | ## Copyright Notice
309 | 
310 | ZeroUI is not affiliated with, associated with, or endorsed by ZeroTier Central or ZeroTier, Inc.
311 | 
312 | ## License
313 | 
314 | []()
315 | 
316 | See [LICENSE](LICENSE) for more information.
317 | 
--------------------------------------------------------------------------------
/backend/.eslintrc.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "plugins": [
 3 |     "@typescript-eslint",
 4 |     "unicorn",
 5 |     "jsdoc",
 6 |     "import",
 7 |     "promise",
 8 |     "sonarjs"
 9 |   ],
10 |   "extends": [
11 |     "eslint:recommended",
12 |     "plugin:n/recommended",
13 |     "plugin:@typescript-eslint/recommended",
14 |     "plugin:unicorn/recommended",
15 |     "plugin:jsdoc/recommended",
16 |     "plugin:import/recommended",
17 |     "plugin:promise/recommended",
18 |     "plugin:sonarjs/recommended",
19 |     "plugin:security/recommended"
20 |   ],
21 |   "root": true,
22 |   "parser": "@typescript-eslint/parser",
23 |   "parserOptions": {
24 |     "project": "./tsconfig.json",
25 |     "ecmaVersion": "latest",
26 |     "sourceType": "module"
27 |   },
28 |   "rules": {
29 |     "@typescript-eslint/no-unused-vars": [
30 |       "error",
31 |       {
32 |         "argsIgnorePattern": "^_",
33 |         "varsIgnorePattern": "^_",
34 |         "ignoreRestSiblings": true
35 |       }
36 |     ],
37 |     "@typescript-eslint/no-misused-promises": [
38 |       "error",
39 |       {
40 |         "checksVoidReturn": false
41 |       }
42 |     ],
43 |     "@typescript-eslint/no-var-requires": "off",
44 |     "@typescript-eslint/ban-ts-comment": "off",
45 |     "jsdoc/require-jsdoc": ["warn", { "publicOnly": true }],
46 |     "jsdoc/require-description": "off",
47 |     "import/no-unresolved": "off",
48 |     "unicorn/no-empty-file": "off",
49 |     "unicorn/consistent-function-scoping": [
50 |       "error",
51 |       {
52 |         "checkArrowFunctions": false
53 |       }
54 |     ],
55 |     "unicorn/prefer-module": "off",
56 |     "unicorn/prevent-abbreviations": "off",
57 |     "unicorn/catch-error-name": "off",
58 |     "unicorn/prefer-ternary": "off",
59 |     "unicorn/prefer-event-target": "off",
60 |     "security/detect-object-injection": "off",
61 |     "security/detect-non-literal-fs-filename": "off"
62 |   }
63 | }
64 | 
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
 1 | # Data
 2 | data/db.json
 3 | 
 4 | # Logs
 5 | logs
 6 | *.log
 7 | npm-debug.log*
 8 | yarn-debug.log*
 9 | yarn-error.log*
10 | 
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 | 
17 | # Dependency directories
18 | node_modules/
19 | jspm_packages/
20 | 
21 | # Optional npm cache directory
22 | .npm
23 | 
24 | # Optional eslint cache
25 | .eslintcache
26 | 
27 | # Optional REPL history
28 | .node_repl_history
29 | 
30 | # Output of 'npm pack'
31 | *.tgz
32 | 
33 | # Yarn Integrity file
34 | .yarn-integrity
35 | 
36 | # Misc
37 | .DS_Store
38 | .env.local
39 | .env.development.local
40 | .env.test.local
41 | .env.production.local
42 | 
--------------------------------------------------------------------------------
/backend/app.js:
--------------------------------------------------------------------------------
  1 | import path from "path";
  2 | import * as url from "url";
  3 | import express from "express";
  4 | import logger from "morgan";
  5 | import compression from "compression";
  6 | import bearerToken from "express-bearer-token";
  7 | import helmet from "helmet";
  8 | import { Cron } from "croner";
  9 | 
 10 | import { db } from "./utils/db.js";
 11 | import { initAdmin } from "./utils/init-admin.js";
 12 | import { pingAll } from "./utils/ping.js";
 13 | 
 14 | import authRoutes from "./routes/auth.js";
 15 | import networkRoutes from "./routes/network.js";
 16 | import memberRoutes from "./routes/member.js";
 17 | import controllerRoutes from "./routes/controller.js";
 18 | 
 19 | const app = express();
 20 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
 21 | 
 22 | app.use(logger("dev"));
 23 | app.use(express.json());
 24 | app.use(express.urlencoded({ extended: false }));
 25 | if (process.env.ZU_DISABLE_AUTH !== "true") {
 26 |   app.use(
 27 |     bearerToken({
 28 |       headerKey: "token",
 29 |     })
 30 |   );
 31 | }
 32 | 
 33 | if (process.env.NODE_ENV === "production") {
 34 |   console.debug = function () {};
 35 | }
 36 | 
 37 | if (
 38 |   process.env.NODE_ENV === "production" &&
 39 |   process.env.ZU_SECURE_HEADERS !== "false"
 40 | ) {
 41 |   // @ts-ignore
 42 |   app.use(helmet());
 43 | }
 44 | 
 45 | if (
 46 |   process.env.NODE_ENV === "production" &&
 47 |   process.env.ZU_SERVE_FRONTEND !== "false"
 48 | ) {
 49 |   app.use(compression());
 50 |   app.use(
 51 |     ["/app", "/app/*"],
 52 |     express.static(path.join(__dirname, "..", "frontend", "build"))
 53 |   );
 54 |   app.use(
 55 |     ["/locales", "/locales/*"],
 56 |     express.static(path.join(__dirname, "..", "frontend", "build", "locales"))
 57 |   );
 58 |   app.get(["/app/network/*"], function (req, res) {
 59 |     res.sendFile(path.join(__dirname, "..", "frontend", "build", "index.html"));
 60 |   });
 61 |   app.get("/", function (req, res) {
 62 |     res.redirect("/app");
 63 |   });
 64 | }
 65 | 
 66 | initAdmin().then(function (admin) {
 67 |   db.defaults({ users: [admin], networks: [] }).write();
 68 | });
 69 | 
 70 | if (process.env.ZU_LAST_SEEN_FETCH !== "false") {
 71 |   let schedule = process.env.ZU_LAST_SEEN_SCHEDULE || "*/5 * * * *";
 72 |   Cron(schedule, () => {
 73 |     console.debug("Running scheduled job");
 74 |     const networks = db.get("networks").value();
 75 |     networks.forEach(async (network) => {
 76 |       console.debug("Processing network " + network.id);
 77 |       await pingAll(network);
 78 |     });
 79 |   });
 80 | }
 81 | 
 82 | const routerAPI = express.Router();
 83 | const routerController = express.Router();
 84 | 
 85 | routerAPI.use("/network", networkRoutes);
 86 | routerAPI.use("/network/:nwid/member", memberRoutes);
 87 | routerController.use("", controllerRoutes);
 88 | 
 89 | app.use("/auth", authRoutes);
 90 | app.use("/api", routerAPI); // offical SaaS API compatible
 91 | app.use("/controller", routerController); // other controller-specific routes
 92 | 
 93 | // error handlers
 94 | app.get("*", async function (req, res) {
 95 |   res.status(404).json({ error: "404 Not found" });
 96 | });
 97 | app.use(function (err, req, res, next) {
 98 |   console.error(err.stack);
 99 |   res.status(500).json({ error: "500 Internal server error" });
100 | });
101 | 
102 | export default app;
103 | 
--------------------------------------------------------------------------------
/backend/bin/www.js:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env node
 2 | import dotenv from "dotenv";
 3 | dotenv.config();
 4 | 
 5 | /**
 6 |  * Module dependencies.
 7 |  */
 8 | 
 9 | import app from "../app.js";
10 | 
11 | console.log("zero-ui:server");
12 | import http from "node:http";
13 | 
14 | /**
15 |  * Get port from environment and store in Express.
16 |  */
17 | 
18 | var port = normalizePort(process.env.PORT || "4000");
19 | app.set("port", port);
20 | 
21 | /**
22 |  * Create HTTP server.
23 |  */
24 | 
25 | var server = http.createServer(app);
26 | 
27 | /**
28 |  * Listen on provided port, on all network interfaces.
29 |  */
30 | 
31 | server.listen(port, process.env.LISTEN_ADDRESS || "0.0.0.0");
32 | server.on("error", onError);
33 | server.on("listening", onListening);
34 | 
35 | /**
36 |  * Normalize a port into a number, string, or false.
37 |  */
38 | 
39 | function normalizePort(val) {
40 |   var port = parseInt(val, 10);
41 | 
42 |   if (isNaN(port)) {
43 |     // named pipe
44 |     return val;
45 |   }
46 | 
47 |   if (port >= 0) {
48 |     // port number
49 |     return port;
50 |   }
51 | 
52 |   return false;
53 | }
54 | 
55 | /**
56 |  * Event listener for HTTP server "error" event.
57 |  */
58 | 
59 | function onError(error) {
60 |   if (error.syscall !== "listen") {
61 |     throw error;
62 |   }
63 | 
64 |   var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
65 | 
66 |   // handle specific listen errors with friendly messages
67 |   switch (error.code) {
68 |     case "EACCES":
69 |       console.error(bind + " requires elevated privileges");
70 |       process.exit(1);
71 |       break;
72 |     case "EADDRINUSE":
73 |       console.error(bind + " is already in use");
74 |       process.exit(1);
75 |       break;
76 |     default:
77 |       throw error;
78 |   }
79 | }
80 | 
81 | /**
82 |  * Event listener for HTTP server "listening" event.
83 |  */
84 | 
85 | function onListening() {
86 |   var addr = server.address();
87 |   var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr?.port;
88 |   console.log("Listening on " + bind);
89 | }
90 | 
--------------------------------------------------------------------------------
/backend/data/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dec0dOS/zero-ui/d104b0388e4ca691b421c56ccf06946c12913877/backend/data/.keep
--------------------------------------------------------------------------------
/backend/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module "axios";
2 | 
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "backend",
 3 |   "private": true,
 4 |   "type": "module",
 5 |   "scripts": {
 6 |     "start": "node ./bin/www",
 7 |     "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
 8 |     "typecheck": "tsc --pretty --noEmit -p tsconfig.json"
 9 |   },
10 |   "dependencies": {
11 |     "axios": "^0.27.2",
12 |     "compression": "^1.7.4",
13 |     "croner": "^7.0.2",
14 |     "debug": "^4.3.4",
15 |     "dotenv": "^16.3.1",
16 |     "express": "^4.18.2",
17 |     "express-bearer-token": "^2.4.0",
18 |     "express-rate-limit": "^7.1.1",
19 |     "helmet": "^5.1.1",
20 |     "lodash": "^4.17.21",
21 |     "lowdb": "^1.0.0",
22 |     "morgan": "^1.10.0",
23 |     "pbkdf2-wrapper": "^1.3.4"
24 |   },
25 |   "devDependencies": {
26 |     "@types/compression": "^1.7.3",
27 |     "@types/debug": "^4.1.9",
28 |     "@types/express": "^4.17.18",
29 |     "@types/lodash": "^4.14.199",
30 |     "@types/morgan": "^1.9.6",
31 |     "@typescript-eslint/eslint-plugin": "^6.7.4",
32 |     "@typescript-eslint/parser": "^6.7.4",
33 |     "eslint": "^8.51.0",
34 |     "eslint-plugin-import": "^2.28.1",
35 |     "eslint-plugin-jsdoc": "^46.8.2",
36 |     "eslint-plugin-n": "^16.1.0",
37 |     "eslint-plugin-promise": "^6.1.1",
38 |     "eslint-plugin-security": "^1.7.1",
39 |     "eslint-plugin-sonarjs": "^0.21.0",
40 |     "eslint-plugin-unicorn": "^48.0.1",
41 |     "typescript": "^5.2.2"
42 |   }
43 | }
44 | 
--------------------------------------------------------------------------------
/backend/routes/auth.js:
--------------------------------------------------------------------------------
 1 | import express from "express";
 2 | import rateLimit from "express-rate-limit";
 3 | 
 4 | const router = express.Router();
 5 | 
 6 | import * as auth from "../services/auth.js";
 7 | 
 8 | const loginLimiter = rateLimit({
 9 |   windowMs: (Number(process.env.ZU_LOGIN_LIMIT_WINDOW) || 30) * 60 * 1000, // 30 minutes
10 |   max: Number(process.env.ZU_LOGIN_LIMIT_ATTEMPTS) || 50, // limit each IP to 50 requests per windowMs
11 |   message: {
12 |     status: 429,
13 |     error: "tooManyAttempts",
14 |   },
15 | });
16 | 
17 | const loginLimiterWrapper = (req, res, next) => {
18 |   if (
19 |     process.env.NODE_ENV === "production" &&
20 |     process.env.ZU_LOGIN_LIMIT === "true"
21 |   ) {
22 |     return loginLimiter(req, res, next);
23 |   } else {
24 |     return next();
25 |   }
26 | };
27 | 
28 | router.get("/login", async function (req, res) {
29 |   if (process.env.ZU_DISABLE_AUTH === "true") {
30 |     res.send({ enabled: false });
31 |   } else {
32 |     res.send({ enabled: true });
33 |   }
34 | });
35 | 
36 | router.post("/login", loginLimiterWrapper, async function (req, res) {
37 |   if (req.body.username && req.body.password) {
38 |     auth.authorize(req.body.username, req.body.password, function (err, user) {
39 |       if (user) {
40 |         res.send({ token: user["token"] });
41 |       } else {
42 |         res.status(401).send({
43 |           error: err.message,
44 |         });
45 |       }
46 |     });
47 |   } else {
48 |     res.status(400).send({ error: "Specify username and password" });
49 |   }
50 | });
51 | 
52 | export default router;
53 | 
--------------------------------------------------------------------------------
/backend/routes/controller.js:
--------------------------------------------------------------------------------
 1 | import express from "express";
 2 | const router = express.Router();
 3 | 
 4 | import * as auth from "../services/auth.js";
 5 | import { api } from "../utils/controller-api.js";
 6 | 
 7 | router.get("/status", auth.isAuthorized, async function (req, res) {
 8 |   api.get("status").then(function (controllerRes) {
 9 |     res.send(controllerRes.data);
10 |   });
11 | });
12 | 
13 | export default router;
14 | 
--------------------------------------------------------------------------------
/backend/routes/member.js:
--------------------------------------------------------------------------------
 1 | import express from "express";
 2 | const router = express.Router({ mergeParams: true });
 3 | 
 4 | import * as auth from "../services/auth.js";
 5 | import * as member from "../services/member.js";
 6 | 
 7 | import { api } from "../utils/controller-api.js";
 8 | 
 9 | // get all members
10 | router.get("/", auth.isAuthorized, async function (req, res) {
11 |   // @ts-ignore
12 |   const nwid = req.params.nwid;
13 |   api
14 |     .get("controller/network/" + nwid + "/member")
15 |     .then(async function (controllerRes) {
16 |       const mids = Object.keys(controllerRes.data);
17 |       const data = await member.getMembersData(nwid, mids);
18 |       res.send(data);
19 |     })
20 |     .catch(function (err) {
21 |       res.status(404).send({ error: `Network not found ${err}` });
22 |     });
23 | });
24 | 
25 | // get member
26 | router.get("/:mid", auth.isAuthorized, async function (req, res) {
27 |   // @ts-ignore
28 |   const nwid = req.params.nwid;
29 |   const mid = req.params.mid;
30 |   const data = await member.getMembersData(nwid, [mid]);
31 |   if (data[0]) {
32 |     res.send(data[0]);
33 |   } else {
34 |     res.status(404).send({ error: "Member not found" });
35 |   }
36 | });
37 | 
38 | // update member
39 | router.post("/:mid", auth.isAuthorized, async function (req, res) {
40 |   // @ts-ignore
41 |   const nwid = req.params.nwid;
42 |   const mid = req.params.mid;
43 |   member.updateMemberAdditionalData(nwid, mid, req.body);
44 |   if (req.body.config) {
45 |     api
46 |       .post("controller/network/" + nwid + "/member/" + mid, req.body.config)
47 |       .then(async function () {
48 |         const data = await member.getMembersData(nwid, [mid]);
49 |         res.send(data[0]);
50 |       })
51 |       .catch(function (err) {
52 |         res.status(500).send({ error: err.message });
53 |       });
54 |   } else {
55 |     const data = await member.getMembersData(nwid, [mid]);
56 |     res.send(data[0]);
57 |   }
58 | });
59 | 
60 | // delete member
61 | router.delete("/:mid", auth.isAuthorized, async function (req, res) {
62 |   // @ts-ignore
63 |   const nwid = req.params.nwid;
64 |   const mid = req.params.mid;
65 |   member.deleteMemberAdditionalData(nwid, mid);
66 |   api
67 |     .delete("controller/network/" + nwid + "/member/" + mid)
68 |     .then(function () {})
69 |     .catch(function (err) {
70 |       res.status(500).send({ error: err.message });
71 |     });
72 |   // Need this to fix ZT controller bug https://github.com/zerotier/ZeroTierOne/issues/859
73 |   const defaultConfig = {
74 |     authorized: false,
75 |     ipAssignments: [],
76 |     capabilities: [],
77 |     tags: [],
78 |   };
79 |   api
80 |     .post("controller/network/" + nwid + "/member/" + mid, defaultConfig)
81 |     .then(function (controllerRes) {
82 |       res.status(controllerRes.status).send("");
83 |     })
84 |     .catch(function (err) {
85 |       res.status(500).send({ error: err.message });
86 |     });
87 | });
88 | 
89 | export default router;
90 | 
--------------------------------------------------------------------------------
/backend/routes/network.js:
--------------------------------------------------------------------------------
 1 | import express from "express";
 2 | const router = express.Router();
 3 | 
 4 | import * as auth from "../services/auth.js";
 5 | import * as network from "../services/network.js";
 6 | 
 7 | import { api } from "../utils/controller-api.js";
 8 | import { defaultRules } from "../utils/constants.js";
 9 | import { getZTAddress } from "../utils/zt-address.js";
10 | 
11 | let ZT_ADDRESS = null;
12 | getZTAddress().then(function (address) {
13 |   ZT_ADDRESS = address;
14 | });
15 | 
16 | // get all networks
17 | router.get("/", auth.isAuthorized, async function (req, res) {
18 |   api.get("controller/network").then(async function (controllerRes) {
19 |     const nwids = controllerRes.data;
20 |     const data = await network.getNetworksData(nwids);
21 |     res.send(data);
22 |   });
23 | });
24 | 
25 | // get network
26 | router.get("/:nwid", auth.isAuthorized, async function (req, res) {
27 |   const nwid = req.params.nwid;
28 |   const data = await network.getNetworksData([nwid]);
29 |   if (data[0]) {
30 |     res.send(data[0]);
31 |   } else {
32 |     res.status(404).send({ error: "Network not found" });
33 |   }
34 | });
35 | 
36 | // create new network
37 | router.post("/", auth.isAuthorized, async function (req, res) {
38 |   let reqData = req.body;
39 |   if (reqData.config) {
40 |     const config = reqData.config;
41 |     delete reqData.config;
42 |     reqData = config;
43 |     reqData.rules = JSON.parse(defaultRules);
44 |   } else {
45 |     res.status(400).send({ error: "Bad request" });
46 |   }
47 |   api
48 |     .post("controller/network/" + ZT_ADDRESS + "______", reqData)
49 |     .then(async function (controllerRes) {
50 |       await network.createNetworkAdditionalData(controllerRes.data.id);
51 |       const data = await network.getNetworksData([controllerRes.data.id]);
52 |       res.send(data[0]);
53 |     });
54 | });
55 | 
56 | // update network
57 | router.post("/:nwid", auth.isAuthorized, async function (req, res) {
58 |   const nwid = req.params.nwid;
59 |   network.updateNetworkAdditionalData(nwid, req.body);
60 |   if (req.body.config) {
61 |     api
62 |       .post("controller/network/" + nwid, req.body.config)
63 |       .then(async function () {
64 |         const data = await network.getNetworksData([nwid]);
65 |         res.send(data[0]);
66 |       })
67 |       .catch(function (err) {
68 |         res.status(500).send({ error: err.message });
69 |       });
70 |   } else {
71 |     const data = await network.getNetworksData([nwid]);
72 |     res.send(data[0]);
73 |   }
74 | });
75 | 
76 | // delete network
77 | router.delete("/:nwid", auth.isAuthorized, async function (req, res) {
78 |   const nwid = req.params.nwid;
79 |   network.deleteNetworkAdditionalData(nwid);
80 |   api
81 |     .delete("controller/network/" + nwid)
82 |     .then(function (controllerRes) {
83 |       res.status(controllerRes.status).send("");
84 |     })
85 |     .catch(function (err) {
86 |       res.status(500).send({ error: err.message });
87 |     });
88 | });
89 | 
90 | export default router;
91 | 
--------------------------------------------------------------------------------
/backend/services/auth.js:
--------------------------------------------------------------------------------
 1 | import { db } from "../utils/db.js";
 2 | import verifyHash from "pbkdf2-wrapper/verifyHash.js";
 3 | 
 4 | export async function authorize(username, password, callback) {
 5 |   try {
 6 |     var users = await db.get("users");
 7 |   } catch (err) {
 8 |     throw err;
 9 |   }
10 |   const user = users.find({ username: username });
11 |   if (!user.value()) return callback(new Error("logInFailed")); // If return "user not found" someone can do a user listing
12 |   const verified = await verifyHash(password, user.value()["password_hash"]);
13 |   if (verified) {
14 |     return callback(null, user.value());
15 |   } else {
16 |     return callback(new Error("logInFailed"));
17 |   }
18 | }
19 | 
20 | export async function isAuthorized(req, res, next) {
21 |   if (process.env.ZU_DISABLE_AUTH === "true") {
22 |     next();
23 |   } else {
24 |     if (req.token) {
25 |       const user = await db.get("users").find({ token: req.token }).value();
26 |       if (user) {
27 |         next();
28 |       } else {
29 |         res.status(403).send({ error: "Invalid token" });
30 |       }
31 |     } else {
32 |       res.status(401).send({ error: "Specify token" });
33 |     }
34 |   }
35 | }
36 | 
--------------------------------------------------------------------------------
/backend/services/member.js:
--------------------------------------------------------------------------------
  1 | import _ from "lodash";
  2 | import axios from "axios";
  3 | 
  4 | import { api } from "../utils/controller-api.js";
  5 | import { db } from "../utils/db.js";
  6 | import { getZTAddress } from "../utils/zt-address.js";
  7 | 
  8 | let ZT_ADDRESS = null;
  9 | getZTAddress().then(function (address) {
 10 |   ZT_ADDRESS = address;
 11 | });
 12 | 
 13 | async function getPeer(mid) {
 14 |   try {
 15 |     const peer = await api.get("peer/" + mid);
 16 |     return peer.data;
 17 |   } catch (err) {
 18 |     return;
 19 |   }
 20 | }
 21 | 
 22 | async function getMemberAdditionalData(data) {
 23 |   // DB BUG INITIALIZATION MIGRATION
 24 |   const network = db.get("networks").find({ id: data.nwid });
 25 |   network.defaults({ members: [] }).get("members").write();
 26 |   // END MIGRATION SECTION
 27 | 
 28 |   const member = db
 29 |     .get("networks")
 30 |     .find({ id: data.nwid })
 31 |     .get("members")
 32 |     .find({ id: data.id });
 33 | 
 34 |   const additionalData = member.get("additionalConfig").value() || {};
 35 |   const lastOnline = member.get("lastOnline").value() || 0;
 36 | 
 37 |   const peer = await getPeer(data.id);
 38 |   let peerData = {};
 39 |   if (peer && !_.isEmpty(peer)) {
 40 |     peerData.latency = peer.latency;
 41 |     if (peer.latency !== -1) peerData.online = 1;
 42 |     if (peer.latency == -1) peerData.online = 2;
 43 |     peerData.clientVersion = peer.version;
 44 |     if (peer.paths.length > 0) {
 45 |       let path = peer.paths.filter((p) => {
 46 |         let ret = p.active && !p.expired;
 47 |         if (typeof p.preferred !== "undefined") {
 48 |           ret = ret && p.preferred;
 49 |         }
 50 |         return ret;
 51 |       });
 52 |       if (path.length > 0) {
 53 |         peerData.lastOnline = path[0].lastReceive;
 54 |         peerData.physicalAddress = path[0].address.split("/")[0];
 55 |         peerData.physicalPort = path[0].address.split("/")[1];
 56 |       }
 57 |     }
 58 |   } else {
 59 |     peerData.online = 0;
 60 |   }
 61 | 
 62 |   delete data.lastAuthorizedCredential;
 63 |   delete data.lastAuthorizedCredentialType;
 64 |   delete data.objtype;
 65 |   delete data.remoteTraceLevel;
 66 |   delete data.remoteTraceTarget;
 67 | 
 68 |   return {
 69 |     id: data.nwid + "-" + data.id,
 70 |     clock: new Date().getTime(),
 71 |     networkId: data.nwid,
 72 |     nodeId: data.id,
 73 |     controllerId: ZT_ADDRESS,
 74 |     // @ts-ignore
 75 |     lastOnline: lastOnline,
 76 |     ...additionalData,
 77 |     ...peerData,
 78 |     config: data,
 79 |   };
 80 | }
 81 | 
 82 | async function filterDeleted(nwid, mid) {
 83 |   const member = db
 84 |     .get("networks")
 85 |     .find({ id: nwid })
 86 |     .get("members")
 87 |     .find({ id: mid });
 88 | 
 89 |   let deleted = member.get("deleted").value() || false;
 90 |   if (!deleted) return mid;
 91 |   else return;
 92 | }
 93 | 
 94 | export async function getMembersData(nwid, mids) {
 95 |   const prefix = "/controller/network/" + nwid + "/member/";
 96 |   const filtered = (
 97 |     await Promise.all(mids.map(async (mid) => await filterDeleted(nwid, mid)))
 98 |   ).filter((item) => item !== undefined);
 99 |   const links = filtered.map((mid) => prefix + mid);
100 | 
101 |   const multipleRes = await axios
102 |     .all(links.map((l) => api.get(l)))
103 |     .then(
104 |       axios.spread(function (...res) {
105 |         return res;
106 |       })
107 |     )
108 |     .catch(function (err) {
109 |       return [];
110 |     });
111 | 
112 |   let data = Promise.all(
113 |     multipleRes.map((el) => {
114 |       return getMemberAdditionalData(el.data);
115 |     })
116 |   );
117 | 
118 |   return data;
119 | }
120 | 
121 | export async function updateMemberAdditionalData(nwid, mid, data) {
122 |   if (data.config && data.config.authorized) {
123 |     db.get("networks")
124 |       .filter({ id: nwid })
125 |       .map("members")
126 |       .first()
127 |       .filter({ id: mid })
128 |       .first()
129 |       .set("deleted", false)
130 |       .write();
131 |   }
132 | 
133 |   let additionalData = {};
134 | 
135 |   if (data.hasOwnProperty("name")) {
136 |     additionalData.name = data.name;
137 |   }
138 |   if (data.hasOwnProperty("description")) {
139 |     additionalData.description = data.description;
140 |   }
141 | 
142 |   if (additionalData) {
143 |     const member = db
144 |       .get("networks")
145 |       .filter({ id: nwid })
146 |       .map("members")
147 |       .first()
148 |       .filter({ id: mid });
149 |     if (member.value().length) {
150 |       member
151 |         .map("additionalConfig")
152 |         .map((additionalConfig) => _.assign(additionalConfig, additionalData))
153 |         .write();
154 |     } else {
155 |       additionalData = { name: "", description: "" };
156 | 
157 |       if (data.hasOwnProperty("name")) {
158 |         additionalData.name = data.name;
159 |       }
160 |       if (data.hasOwnProperty("description")) {
161 |         additionalData.description = data.description;
162 |       }
163 |       db.get("networks")
164 |         .filter({ id: nwid })
165 |         .map("members")
166 |         .first()
167 |         .push({ id: mid, additionalConfig: additionalData })
168 |         .write();
169 |     }
170 |   }
171 | }
172 | 
173 | export async function deleteMemberAdditionalData(nwid, mid) {
174 |   // ZT controller bug
175 |   /* db.get("networks")
176 |        .find({ id: nwid })
177 |        .get("members")
178 |        .remove({ id: mid })
179 |        .write();
180 |   */
181 | 
182 |   await updateMemberAdditionalData(nwid, mid, {});
183 | 
184 |   db.get("networks")
185 |     .filter({ id: nwid })
186 |     .map("members")
187 |     .first()
188 |     .filter({ id: mid })
189 |     .first()
190 |     .set("deleted", true)
191 |     .write();
192 | }
193 | 
--------------------------------------------------------------------------------
/backend/services/network.js:
--------------------------------------------------------------------------------
 1 | import _ from "lodash";
 2 | import axios from "axios";
 3 | 
 4 | import { api } from "../utils/controller-api.js";
 5 | import { db } from "../utils/db.js";
 6 | import { defaultRulesSource } from "../utils/constants.js";
 7 | 
 8 | export async function getNetworkAdditionalData(data) {
 9 |   let additionalData = db
10 |     .get("networks")
11 |     .find({ id: data.id })
12 |     .get("additionalConfig");
13 | 
14 |   if (!additionalData.value()) {
15 |     createNetworkAdditionalData(data.id);
16 |   }
17 | 
18 |   delete data.rulesSource;
19 |   delete data.objtype;
20 |   delete data.revision;
21 |   delete data.remoteTraceLevel;
22 |   delete data.remoteTraceTarget;
23 | 
24 |   return {
25 |     id: data.id,
26 |     clock: new Date().getTime(),
27 |     ...additionalData.value(),
28 |     config: data,
29 |   };
30 | }
31 | 
32 | export async function getNetworksData(nwids) {
33 |   const prefix = "/controller/network/";
34 |   const links = nwids.map((nwid) => prefix + nwid);
35 | 
36 |   const multipleRes = await axios
37 |     .all(links.map((l) => api.get(l)))
38 |     .then(
39 |       axios.spread(function (...res) {
40 |         return res;
41 |       })
42 |     )
43 |     .catch(function () {
44 |       return [];
45 |     });
46 | 
47 |   let data = Promise.all(
48 |     multipleRes.map((el) => {
49 |       return getNetworkAdditionalData(el.data);
50 |     })
51 |   );
52 | 
53 |   return data;
54 | }
55 | 
56 | export async function createNetworkAdditionalData(nwid) {
57 |   const saveData = {
58 |     id: nwid,
59 |     additionalConfig: {
60 |       description: "",
61 |       rulesSource: defaultRulesSource,
62 |       tagsByName: {},
63 |       capabilitiesByName: {},
64 |     },
65 |     members: [],
66 |   };
67 | 
68 |   db.get("networks").push(saveData).write();
69 | }
70 | 
71 | export async function updateNetworkAdditionalData(nwid, data) {
72 |   let additionalData = {};
73 | 
74 |   if (data.hasOwnProperty("description")) {
75 |     additionalData.description = data.description;
76 |   }
77 |   if (data.hasOwnProperty("rulesSource")) {
78 |     additionalData.rulesSource = data.rulesSource;
79 |   }
80 |   if (data.hasOwnProperty("tagsByName")) {
81 |     additionalData.tagsByName = data.tagsByName;
82 |   }
83 |   if (data.hasOwnProperty("capabilitiesByName")) {
84 |     additionalData.capabilitiesByName = data.capabilitiesByName;
85 |   }
86 | 
87 |   if (additionalData) {
88 |     db.get("networks")
89 |       .filter({ id: nwid })
90 |       .map("additionalConfig")
91 |       .map((additionalConfig) => _.assign(additionalConfig, additionalData))
92 |       .write();
93 |   }
94 | }
95 | 
96 | export async function deleteNetworkAdditionalData(nwid) {
97 |   db.get("networks").remove({ id: nwid }).write();
98 | }
99 | 
--------------------------------------------------------------------------------
/backend/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "extends": "../tsconfig.json",
 3 |   "compilerOptions": {
 4 |     "rootDir": ".",
 5 |     "baseUrl": ".",
 6 |     "module": "NodeNext",
 7 |     "moduleResolution": "NodeNext"
 8 |   },
 9 |   "include": ["."]
10 | }
11 | 
--------------------------------------------------------------------------------
/backend/utils/constants.js:
--------------------------------------------------------------------------------
 1 | export const defaultRulesSource = `
 2 | # This is a default rule set that allows IPv4 and IPv6 traffic but otherwise
 3 | # behaves like a standard Ethernet switch.
 4 | 
 5 | #
 6 | # Allow only IPv4, IPv4 ARP, and IPv6 Ethernet frames.
 7 | #
 8 | drop
 9 |   not ethertype ipv4
10 |   and not ethertype arp
11 |   and not ethertype ipv6
12 | ;
13 | 
14 | #
15 | # Uncomment to drop non-ZeroTier issued and managed IP addresses.
16 | #
17 | # This prevents IP spoofing but also blocks manual IP management at the OS level and
18 | # bridging unless special rules to exempt certain hosts or traffic are added before
19 | # this rule.
20 | #
21 | #drop
22 | #  not chr ipauth
23 | #;
24 | 
25 | # Accept anything else. This is required since default is 'drop'.
26 | accept;
27 | `;
28 | 
29 | export const defaultRules = `
30 | [
31 |   {
32 |    "type": "MATCH_ETHERTYPE",
33 |    "not": true,
34 |    "or": false,
35 |    "etherType": 2048
36 |   },
37 |   {
38 |    "type": "MATCH_ETHERTYPE",
39 |    "not": true,
40 |    "or": false,
41 |    "etherType": 2054
42 |   },
43 |   {
44 |    "type": "MATCH_ETHERTYPE",
45 |    "not": true,
46 |    "or": false,
47 |    "etherType": 34525
48 |   },
49 |   {
50 |    "type": "ACTION_DROP"
51 |   },
52 |   {
53 |    "type": "ACTION_ACCEPT"
54 |   }
55 |  ]
56 | `;
57 | 
--------------------------------------------------------------------------------
/backend/utils/controller-api.js:
--------------------------------------------------------------------------------
 1 | import axios from "axios";
 2 | import fs from "node:fs";
 3 | import os from "node:os";
 4 | 
 5 | const baseURL = process.env.ZU_CONTROLLER_ENDPOINT || "http://localhost:9993/";
 6 | 
 7 | var token;
 8 | if (process.env.ZU_CONTROLLER_TOKEN) {
 9 |   token = process.env.ZU_CONTROLLER_TOKEN;
10 | } else if (os.platform() === "linux") {
11 |   token = fs.readFileSync("/var/lib/zerotier-one/authtoken.secret", "utf8");
12 | } else {
13 |   throw new Error("Please provide ZU_CONTROLLER_TOKEN in environment");
14 | }
15 | 
16 | export const api = axios.create({
17 |   baseURL: baseURL,
18 |   responseType: "json",
19 |   headers: {
20 |     "X-ZT1-Auth": token,
21 |   },
22 | });
23 | 
--------------------------------------------------------------------------------
/backend/utils/db.js:
--------------------------------------------------------------------------------
1 | import low from "lowdb";
2 | import FileSync from "lowdb/adapters/FileSync.js";
3 | 
4 | const adapter = new FileSync(process.env.ZU_DATAPATH || "data/db.json");
5 | 
6 | export const db = low(adapter);
7 | 
--------------------------------------------------------------------------------
/backend/utils/init-admin.js:
--------------------------------------------------------------------------------
 1 | import crypto from "crypto";
 2 | import hashPassword from "pbkdf2-wrapper/hashText.js";
 3 | 
 4 | export async function initAdmin() {
 5 |   if (!process.env.ZU_DEFAULT_PASSWORD || !process.env.ZU_DEFAULT_USERNAME) {
 6 |     console.error("ZU_DEFAULT_PASSWORD or ZU_DEFAULT_USERNAME not found!");
 7 |     process.exit(1);
 8 |   }
 9 |   const username = process.env.ZU_DEFAULT_USERNAME;
10 |   const hash = await hashPassword(process.env.ZU_DEFAULT_PASSWORD);
11 |   return {
12 |     username: username,
13 |     password_hash: hash,
14 |     token: crypto.randomBytes(16).toString("hex"),
15 |   };
16 | }
17 | 
--------------------------------------------------------------------------------
/backend/utils/ping.js:
--------------------------------------------------------------------------------
 1 | import _ from "lodash";
 2 | 
 3 | import { api } from "./controller-api.js";
 4 | import { db } from "./db.js";
 5 | 
 6 | export async function pingAll(network) {
 7 |   await Promise.all(
 8 |     network.members.map(async (member) => {
 9 |       console.debug("Processing member " + member.id);
10 |       api
11 |         .get("peer/" + member.id)
12 |         .then(function (controllerResp) {
13 |           if (!_.isEmpty(controllerResp.data)) {
14 |             // write lastOnline field in db
15 |             db.get("networks")
16 |               .filter({ id: network.id })
17 |               .map("members")
18 |               .first()
19 |               .filter({ id: member.id })
20 |               .first()
21 |               .set("lastOnline", new Date().getTime())
22 |               .write();
23 |           }
24 |         })
25 |         .catch(function (err) {
26 |           console.debug("Couldn't fetch", member.id);
27 |           return;
28 |         });
29 |     })
30 |   );
31 | }
32 | 
--------------------------------------------------------------------------------
/backend/utils/zt-address.js:
--------------------------------------------------------------------------------
 1 | import { api } from "../utils/controller-api.js";
 2 | 
 3 | export async function getZTAddress() {
 4 |   try {
 5 |     const res = await api.get("status");
 6 |     return res.data.address;
 7 |   } catch (err) {
 8 |     console.error(
 9 |       // @ts-ignore
10 |       "Couldn't connect to the controller on " + err.config.baseURL
11 |     );
12 |   }
13 | }
14 | 
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
 1 | version: "3"
 2 | 
 3 | services:
 4 |   zerotier:
 5 |     image: zyclonite/zerotier:1.10.6
 6 |     container_name: zu-controller
 7 |     restart: unless-stopped
 8 |     volumes:
 9 |       - ./zerotier-one:/var/lib/zerotier-one
10 |     environment:
11 |       - ZT_OVERRIDE_LOCAL_CONF=true
12 |       - ZT_ALLOW_MANAGEMENT_FROM=0.0.0.0/0
13 |     expose:
14 |       - "9993/tcp"
15 |     ports:
16 |       - "9993:9993/udp"
17 |   zero-ui:
18 |     image: dec0dos/zero-ui:latest
19 |     container_name: zu-main
20 |     build:
21 |       context: .
22 |       dockerfile: ./docker/zero-ui/Dockerfile
23 |     restart: unless-stopped
24 |     depends_on:
25 |       - zerotier
26 |     volumes:
27 |       - ./zerotier-one:/var/lib/zerotier-one
28 |       - ./data:/app/backend/data
29 |     environment:
30 |       - ZU_CONTROLLER_ENDPOINT=http://zerotier:9993/
31 |       - ZU_SECURE_HEADERS=true
32 |       - ZU_DEFAULT_USERNAME=admin
33 |       - ZU_DEFAULT_PASSWORD=zero-ui
34 |     expose:
35 |       - "4000"
36 |   https-proxy:
37 |     image: caddy:latest
38 |     container_name: zu-https-proxy
39 |     restart: unless-stopped
40 |     depends_on:
41 |       - zero-ui
42 |     command: caddy reverse-proxy --from YOURDOMAIN.com --to zero-ui:4000
43 |     volumes:
44 |       - ./caddy:/data/caddy
45 |     ports:
46 |       - "80:80"
47 |       - "443:443"
48 | 
--------------------------------------------------------------------------------
/docker/zero-ui/Dockerfile:
--------------------------------------------------------------------------------
 1 | # Stage 1: Build frontend
 2 | FROM --platform=$BUILDPLATFORM node:lts-alpine AS frontend-build
 3 | 
 4 | ENV GENERATE_SOURCEMAP=false
 5 | 
 6 | # Enable corepack and create necessary directories in one layer
 7 | RUN corepack enable && mkdir -p /app/frontend
 8 | 
 9 | WORKDIR /app
10 | 
11 | # Copy package-related files and install dependencies
12 | COPY tsconfig.json package.json yarn.lock* .yarnrc.yml ./
13 | COPY .yarn/ ./.yarn
14 | 
15 | # Set working directory to frontend and copy package files
16 | WORKDIR /app/frontend
17 | COPY ./frontend/package*.json /app/frontend/
18 | 
19 | # Install frontend dependencies and build the frontend
20 | RUN yarn workspaces focus frontend
21 | COPY ./frontend /app/frontend/
22 | RUN yarn build
23 | 
24 | # Stage 2: Build backend
25 | FROM node:lts-alpine AS backend-build
26 | 
27 | # Enable corepack and create necessary directories in one layer
28 | RUN corepack enable && mkdir -p /app/backend
29 | 
30 | WORKDIR /app
31 | 
32 | # Copy package-related files and install dependencies
33 | COPY package.json yarn.lock* .yarnrc.yml ./
34 | COPY .yarn/ ./.yarn
35 | 
36 | # Set working directory to backend and copy package files
37 | WORKDIR /app/backend
38 | COPY ./backend/package*.json /app/backend/
39 | 
40 | # Install backend dependencies
41 | RUN yarn workspaces focus --production backend && yarn cache clean
42 | 
43 | # Copy the backend source files
44 | COPY ./backend /app/backend
45 | 
46 | # Final Stage: Production
47 | FROM node:lts-alpine
48 | 
49 | # Set the working directory to /app/backend
50 | WORKDIR /app/backend
51 | 
52 | # Copy the built frontend from the frontend-build stage
53 | COPY --from=frontend-build /app/frontend/build /app/frontend/build
54 | 
55 | # Copy the backend files from the backend-build stage
56 | COPY --from=backend-build /app/backend /app/backend
57 | COPY --from=backend-build /app/node_modules /app/backend/node_modules
58 | 
59 | # Environment variables
60 | ENV NODE_ENV=production
61 | ENV ZU_SECURE_HEADERS=true
62 | ENV ZU_SERVE_FRONTEND=true
63 | 
64 | # Expose the application port
65 | EXPOSE 4000
66 | 
67 | # Start the application
68 | CMD ["node", "bin/www.js"]
--------------------------------------------------------------------------------
/docs/SCREENSHOTS.md:
--------------------------------------------------------------------------------
1 | # Home page
2 | 
3 | 
4 | 
5 | # Network page
6 | 
7 | 
8 | 
--------------------------------------------------------------------------------
/docs/SECURITY.md:
--------------------------------------------------------------------------------
 1 | # Security Policy
 2 | 
 3 | ## Reporting a Vulnerability
 4 | 
 5 | If there are any vulnerabilities in ZeroUI, don't hesitate to _report them_.
 6 | 
 7 | 1. Use any of the [private contact addresses](https://github.com/dec0dOS/zero-ui#support).
 8 | 2. Describe the vulnerability.
 9 | 
10 | - If you have a fix, that is most welcome - please attach or summarize it in your message!
11 | 
12 | 3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report.
13 | 
14 | - Please **do not disclose the vulnerability publicly** until a fix is released!
15 | 
16 | 4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it.
17 | 
--------------------------------------------------------------------------------
/docs/images/homepage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dec0dOS/zero-ui/d104b0388e4ca691b421c56ccf06946c12913877/docs/images/homepage.png
--------------------------------------------------------------------------------
/docs/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dec0dOS/zero-ui/d104b0388e4ca691b421c56ccf06946c12913877/docs/images/logo.png
--------------------------------------------------------------------------------
/docs/images/network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dec0dOS/zero-ui/d104b0388e4ca691b421c56ccf06946c12913877/docs/images/network.png
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "root": true,
 3 |   "env": { "browser": true, "es2020": true },
 4 |   "extends": [
 5 |     "eslint:recommended",
 6 |     "plugin:react/recommended",
 7 |     "plugin:react/jsx-runtime",
 8 |     "plugin:react-hooks/recommended"
 9 |   ],
10 |   "ignorePatterns": ["dist", ".eslintrc.cjs"],
11 |   "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
12 |   "settings": { "react": { "version": "17.0.2" } },
13 |   "plugins": ["react-refresh"],
14 |   "rules": {
15 |     "react-refresh/only-export-components": [
16 |       "warn",
17 |       { "allowConstantExport": true }
18 |     ],
19 |     "react/prop-types": ["off"],
20 |     "no-unused-vars": ["off"]
21 |   }
22 | }
23 | 
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
 1 | # Dependencies
 2 | node_modules
 3 | .pnp
 4 | .pnp.js
 5 | 
 6 | # Testing
 7 | coverage
 8 | 
 9 | # Production
10 | build
11 | 
12 | # Misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 | 
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 | 
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     
10 |     
11 |     
12 |     ZeroUI
13 |   
14 |   
15 |     
16 |     
17 |     
18 |   
19 | 
20 | 
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "frontend",
 3 |   "private": true,
 4 |   "dependencies": {
 5 |     "@fontsource/roboto": "^4.5.8",
 6 |     "@material-ui/core": "^4.12.4",
 7 |     "@material-ui/icons": "^4.11.3",
 8 |     "@material-ui/styles": "^4.11.5",
 9 |     "@uiw/react-codemirror": "^3.1.0",
10 |     "axios": "^0.27.2",
11 |     "codemirror": "^5.62.3",
12 |     "date-fns": "^2.29.2",
13 |     "history": "^5.3.0",
14 |     "i18next": "^23.5.1",
15 |     "i18next-browser-languagedetector": "^7.1.0",
16 |     "i18next-http-backend": "^2.2.2",
17 |     "ipaddr.js": "^2.0.1",
18 |     "lodash": "^4.17.21",
19 |     "react": "^17.0.2",
20 |     "react-data-table-component": "^6.11.8",
21 |     "react-dom": "^17.0.2",
22 |     "react-i18next": "^13.3.0",
23 |     "react-is": "^17.0.2",
24 |     "react-router-dom": "^5.2.0",
25 |     "react-use": "^17.4.0",
26 |     "styled-components": "^5.3.11"
27 |   },
28 |   "devDependencies": {
29 |     "@types/codemirror": "^5.60.10",
30 |     "@types/lodash": "^4.14.199",
31 |     "@types/react": "^17.0.67",
32 |     "@types/react-dom": "^17.0.21",
33 |     "@types/react-is": "^17.0.5",
34 |     "@types/react-router-dom": "^5.3.3",
35 |     "@types/styled-components": "^5.1.28",
36 |     "@vitejs/plugin-react": "^4.1.0",
37 |     "eslint": "^8.51.0",
38 |     "eslint-plugin-react": "^7.33.2",
39 |     "eslint-plugin-react-hooks": "^4.6.0",
40 |     "eslint-plugin-react-refresh": "^0.4.3",
41 |     "rimraf": "^5.0.5",
42 |     "source-map-explorer": "^2.5.3",
43 |     "typescript": "^5.2.2",
44 |     "vite": "^4.4.11",
45 |     "vite-plugin-static-copy": "^1.0.6"
46 |   },
47 |   "scripts": {
48 |     "start": "vite",
49 |     "build": "vite build",
50 |     "serve": "vite preview",
51 |     "clean": "rimraf build",
52 |     "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
53 |     "typecheck": "tsc --pretty --noEmit -p tsconfig.json",
54 |     "analyze": "vite build --sourcemap true && source-map-explorer 'build/assets/*.js'  --no-border-checks"
55 |   }
56 | }
57 | 
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dec0dOS/zero-ui/d104b0388e4ca691b421c56ccf06946c12913877/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/locales/en/common.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "flowRules": "Flow Rules",
 3 |   "createNetwork": "Create A Network",
 4 |   "createOneNetwork": "Please create at least one network",
 5 |   "controllerNetworks": "Controller networks",
 6 |   "network_one": "Network",
 7 |   "network_other": "Networks",
 8 |   "controllerAddress": "Network controller address",
 9 |   "loginToContinue": "Please, Log In to continue",
10 |   "zerouiDesc": "ZeroUI - ZeroTier Controller Web UI - is a web user interface for a self-hosted ZeroTier network controller.",
11 |   "logIn": "Log In",
12 |   "logInToken": "Token Log In",
13 |   "cancel": "Cancel",
14 |   "management": "Management",
15 |   "deleteNetwork": "Delete Network",
16 |   "deleteAlert": "This action cannot be undone.",
17 |   "deleteNetworkConfirm": "Are you sure you want to delete this network?",
18 |   "deleteMemberConfirm": "Are you sure you want to delete this member?",
19 |   "delete": "Delete",
20 |   "logOut": "Log out",
21 |   "advancedFeature": "ADVANCED FEATURE",
22 |   "noDevices": "No devices have joined this network. Use the app on your devices to join",
23 |   "member_one": "Member",
24 |   "member_other": "Members",
25 |   "addMemberManually": "Manually Add Member",
26 |   "name": "Name",
27 |   "description": "Description",
28 |   "allowBridging": "Allow Ethernet Bridging",
29 |   "noAutoIP": "Do Not Auto-Assign IPs",
30 |   "capabilities": "Capabilities",
31 |   "noCapDef": "No capabilities defined",
32 |   "tags": "Tags",
33 |   "noTagDef": "No tags defined",
34 |   "authorized": "Authorized",
35 |   "address": "Address",
36 |   "managedIPs": "Managed IPs",
37 |   "lastSeen": "Last seen",
38 |   "version": "Version",
39 |   "physIp": "Physical IP",
40 |   "latency": "Latency",
41 |   "settings": "Settings",
42 |   "generalSettings": "General settings",
43 |   "networkId": "Network ID",
44 |   "accessControl": "Access control",
45 |   "public": "Public",
46 |   "private": "Private",
47 |   "managedRoutes": "Managed routes",
48 |   "addRoute": "Add route",
49 |   "target": "Target",
50 |   "via": "Via",
51 |   "start": "Start",
52 |   "end": "End",
53 |   "ipv4AutoAssign": "IPv4 Auto-Assign",
54 |   "autoAssignPool": "IPv4 Auto-Assign",
55 |   "addIPv4Pool": "Add IPv4 Pool",
56 |   "multicastLimit": "Multicast Recipient Limit",
57 |   "enableBroadcast": "Enable Broadcast",
58 |   "logInFailed": "Invalid username or password",
59 |   "tooManyAttempts": "Too many login attempts, please try again in 15 minutes.",
60 |   "language": "Language",
61 |   "notAuthorized": "You are not authorized. Please Log In.",
62 |   "saveChanges": "Save changes",
63 |   "optional": "Optional",
64 |   "destination": "Destination",
65 |   "username": "Username",
66 |   "password": "Password",
67 |   "languageName": "English"
68 | }
69 | 
--------------------------------------------------------------------------------
/frontend/public/locales/es-ES/common.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "flowRules": "Reglas de flujo",
 3 |   "createNetwork": "Crear una red",
 4 |   "createOneNetwork": "Por favor, crea al menos una red",
 5 |   "controllerNetworks": "Controlador de redes",
 6 |   "network_one": "Red",
 7 |   "network_other": "Redes",
 8 |   "controllerAddress": "Dirección del controlador",
 9 |   "loginToContinue": "Por favor, inicia sesión para continuar",
10 |   "zerouiDesc": "ZeroUI - ZeroTier Controller Web UI - es una interfaz de usuario web para un controlador de red ZeroTier self-hosted.",
11 |   "logIn": "Iniciar sesión",
12 |   "logInToken": "Iniciar sesión con token",
13 |   "cancel": "Cancelar",
14 |   "management": "Gestión",
15 |   "deleteNetwork": "Borrar red",
16 |   "deleteAlert": "Esta acción no puede ser revertida.",
17 |   "deleteNetworkConfirm": "¿Seguro que deseas borrar esta red?",
18 |   "deleteMemberConfirm": "¿Seguro que deseas borrar este usuario?",
19 |   "delete": "Borrar",
20 |   "logOut": "Cerrar sesión",
21 |   "advancedFeature": "CARACTERÍSTICA AVANZADA",
22 |   "noDevices": "Ningún dispositivo se ha unido a esta red. Utilice la aplicación en sus dispositivos para unirse",
23 |   "member_one": "Miembro",
24 |   "member_other": "Miembros",
25 |   "addMemberManually": "Añadir miembro manualmente",
26 |   "name": "Nombre",
27 |   "description": "Descripción",
28 |   "allowBridging": "Permitir puente Ethernet",
29 |   "noAutoIP": "No autoasignar IPs",
30 |   "capabilities": "Permisos",
31 |   "noCapDef": "No hay permisos definidos",
32 |   "tags": "Etiquetas",
33 |   "noTagDef": "No hay etiquetas definidas",
34 |   "authorized": "Autorizado",
35 |   "address": "Dirección",
36 |   "managedIPs": "IPs asignadas",
37 |   "lastSeen": "Visto por última vez",
38 |   "version": "Versión",
39 |   "physIp": "IP pública",
40 |   "latency": "Latencia",
41 |   "settings": "Ajustes",
42 |   "generalSettings": "Ajustes generales",
43 |   "networkId": "ID de red",
44 |   "accessControl": "Control de acceso",
45 |   "public": "Público",
46 |   "private": "Privado",
47 |   "managedRoutes": "Rutas gestionadas",
48 |   "addRoute": "Añadir ruta",
49 |   "target": "Objetivo",
50 |   "via": "Vía",
51 |   "start": "Inicio",
52 |   "end": "Final",
53 |   "autoAssignPool": "Rango de IPv4 autoasignables",
54 |   "ipv4AutoAssign": "Rangos de IPv4 automáticos",
55 |   "addIPv4Pool": "Añadir rango IPv4",
56 |   "multicastLimit": "Límite de destinatarios multicast",
57 |   "enableBroadcast": "Habilitar broadcast",
58 |   "logInFailed": "Nombre de usuario o contraseña incorrecto",
59 |   "tooManyAttempts": "Demasiados intentos de inicio de sesión. Vuelvee a intentarlo en 15 minutos",
60 |   "language": "Idioma",
61 |   "notAuthorized": "No estás autorizado. Por favor, inicia sesión.",
62 |   "saveChanges": "Guardar cambios",
63 |   "optional": "Opcional",
64 |   "destination": "Destino",
65 |   "username": "Nombre de usuario",
66 |   "password": "Contraseña",
67 |   "languageName": "Español"
68 | }
69 | 
--------------------------------------------------------------------------------
/frontend/public/locales/ru-RU/common.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "flowRules": "Правила потока",
 3 |   "createNetwork": "Создать сеть",
 4 |   "createOneNetwork": "Пожалуйста, создайте хотя бы одну сеть",
 5 |   "controllerNetworks": "Сети контроллера",
 6 |   "network_one": "Сеть",
 7 |   "network_few": "Сети",
 8 |   "network_many": "Сетей",
 9 |   "network_other": "Сетей",
10 |   "controllerAddress": "Адрес контроллера сети",
11 |   "loginToContinue": "Пожалуйста, войдите, чтобы продолжить",
12 |   "zerouiDesc": "ZeroUI - Веб-интерфейс контроллера ZeroTier - это веб-интерфейс для самостоятельного хостинга контроллера сети ZeroTier.",
13 |   "logIn": "Войти",
14 |   "logInToken": "Войти по токену",
15 |   "cancel": "Отмена",
16 |   "management": "Управление",
17 |   "deleteNetwork": "Удалить сеть",
18 |   "deleteAlert": "Это действие не может быть отменено.",
19 |   "deleteNetworkConfirm": "Вы уверены, что хотите удалить эту сеть?",
20 |   "deleteMemberConfirm": "Вы уверены, что хотите удалить это устройство?",
21 |   "delete": "Удалить",
22 |   "logOut": "Выйти",
23 |   "advancedFeature": "РАСШИРЕННАЯ ФУНКЦИЯ",
24 |   "noDevices": "Ни одно устройство не присоединилось к этой сети. Используйте приложение на ваших устройствах для подключения",
25 |   "member_one": "Устройство",
26 |   "member_few": "Устройства",
27 |   "member_many": "Устройства",
28 |   "member_other": "Устройств",
29 |   "addMemberManually": "Добавить устройство вручную",
30 |   "name": "Имя",
31 |   "description": "Описание",
32 |   "allowBridging": "Разрешить Ethernet мост",
33 |   "noAutoIP": "Не назначать IP автоматически",
34 |   "capabilities": "Возможности",
35 |   "noCapDef": "Нет определенных возможностей",
36 |   "tags": "Теги",
37 |   "noTagDef": "Нет определенных тегов",
38 |   "authorized": "Авторизован",
39 |   "address": "Адрес",
40 |   "managedIPs": "Управляемые IP",
41 |   "lastSeen": "Последний раз был онлайн",
42 |   "version": "Версия",
43 |   "physIp": "Физический IP",
44 |   "latency": "Задержка",
45 |   "settings": "Настройки",
46 |   "generalSettings": "Общие настройки",
47 |   "networkId": "ID сети",
48 |   "accessControl": "Контроль доступа",
49 |   "public": "Публичный",
50 |   "private": "Частный",
51 |   "managedRoutes": "Управляемые маршруты",
52 |   "addRoute": "Добавить маршрут",
53 |   "target": "Цель",
54 |   "via": "Через",
55 |   "start": "Начало",
56 |   "end": "Конец",
57 |   "ipv4AutoAssign": "Автоматическое назначение IPv4",
58 |   "autoAssignPool": "Автоматическое назначение IPv4",
59 |   "addIPv4Pool": "Добавить пул IPv4",
60 |   "multicastLimit": "Ограничение получателей мультикаста",
61 |   "enableBroadcast": "Включить широковещание",
62 |   "logInFailed": "Неверное имя пользователя или пароль",
63 |   "tooManyAttempts": "Слишком много попыток входа, попробуйте снова через 15 минут.",
64 |   "language": "Язык",
65 |   "notAuthorized": "Вы не авторизованы. Пожалуйста, войдите.",
66 |   "saveChanges": "Сохранить изменения",
67 |   "optional": "Необязательно",
68 |   "destination": "Назначение",
69 |   "username": "Имя пользователя",
70 |   "password": "Пароль",
71 |   "languageName": "Русский"
72 | }
73 | 
--------------------------------------------------------------------------------
/frontend/public/locales/zh_CN/common.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "flowRules": "流量规则",
 3 |   "createNetwork": "创建网络",
 4 |   "createOneNetwork": "请至少创建一个网络",
 5 |   "controllerNetworks": "控制器网络",
 6 |   "network_one": "网络",
 7 |   "network_other": "网络们",
 8 |   "controllerAddress": "网络控制器地址",
 9 |   "loginToContinue": "请登录以继续",
10 |   "zerouiDesc": "ZeroUI - ZeroTier 控制器Web界面 - 是一个用于自托管ZeroTier网络控制器的Web用户界面。",
11 |   "logIn": "登录",
12 |   "logInToken": "令牌登录",
13 |   "cancel": "取消",
14 |   "management": "管理",
15 |   "deleteNetwork": "删除网络",
16 |   "deleteAlert": "此操作无法撤销。",
17 |   "deleteNetworkConfirm": "您确定要删除此网络吗?",
18 |   "deleteMemberConfirm": "您确定要删除此成员吗?",
19 |   "delete": "删除",
20 |   "logOut": "登出",
21 |   "advancedFeature": "高级功能",
22 |   "noDevices": "没有设备加入此网络。请使用您的设备上的应用程序加入。",
23 |   "member_one": "成员",
24 |   "member_other": "成员们",
25 |   "addMemberManually": "手动添加成员",
26 |   "name": "名称",
27 |   "description": "描述",
28 |   "allowBridging": "允许以太网桥接",
29 |   "noAutoIP": "不自动分配IP",
30 |   "capabilities": "能力",
31 |   "noCapDef": "未定义能力",
32 |   "tags": "标签",
33 |   "noTagDef": "未定义标签",
34 |   "authorized": "已授权",
35 |   "address": "地址",
36 |   "managedIPs": "管理的IP地址",
37 |   "lastSeen": "最后看到的时间",
38 |   "version": "版本",
39 |   "physIp": "物理IP",
40 |   "latency": "延迟",
41 |   "settings": "设置",
42 |   "generalSettings": "常规设置",
43 |   "networkId": "网络ID",
44 |   "accessControl": "访问控制",
45 |   "public": "公共",
46 |   "private": "私有",
47 |   "managedRoutes": "管理的路由",
48 |   "addRoute": "添加路由",
49 |   "target": "目标",
50 |   "via": "通过",
51 |   "start": "开始",
52 |   "end": "结束",
53 |   "ipv4AutoAssign": "IPv4 自动分配",
54 |   "autoAssignPool": "IPv4 自动分配池",
55 |   "addIPv4Pool": "添加IPv4池",
56 |   "multicastLimit": "多播接收者限制",
57 |   "enableBroadcast": "启用广播",
58 |   "logInFailed": "无效的用户名或密码",
59 |   "tooManyAttempts": "尝试登录次数过多,请15分钟后重试。",
60 |   "language": "语言",
61 |   "notAuthorized": "您没有权限。请登录。",
62 |   "saveChanges": "保存更改",
63 |   "optional": "可选",
64 |   "destination": "目的地",
65 |   "username": "用户名",
66 |   "password": "密码",
67 |   "languageName": "中文"
68 | }
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "icons": [
 3 |     {
 4 |       "src": "favicon.ico",
 5 |       "sizes": "48x48",
 6 |       "type": "image/x-icon"
 7 |     }
 8 |   ],
 9 |   "start_url": "./app",
10 |   "display": "standalone",
11 |   "theme_color": "#000000",
12 |   "background_color": "#ffffff"
13 | }
14 | 
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow: /
4 | 
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
 1 | import "@fontsource/roboto";
 2 | 
 3 | import { Suspense } from "react";
 4 | import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
 5 | 
 6 | import Theme from "./components/Theme";
 7 | import Bar from "./components/Bar";
 8 | 
 9 | import Home from "./routes/Home";
10 | import NotFound from "./routes/NotFound";
11 | import Network from "./routes/Network/Network";
12 | import Settings from "./routes/Settings";
13 | 
14 | import Loading from "./components/Loading";
15 | 
16 | import "./i18n";
17 | 
18 | function App() {
19 |   return (
20 |     
21 |       }>
22 |         
23 |           
24 |           
25 |             
26 |             
27 |             
28 |             
29 |             
30 |           
31 |         
32 |       
33 |     
34 |   );
35 | }
36 | 
37 | export default App;
38 | 
--------------------------------------------------------------------------------
/frontend/src/components/Bar/Bar.jsx:
--------------------------------------------------------------------------------
  1 | import logo from "./assets/logo.png";
  2 | 
  3 | import { useState } from "react";
  4 | import { Link as RouterLink, useHistory } from "react-router-dom";
  5 | import { useLocalStorage } from "react-use";
  6 | 
  7 | import {
  8 |   AppBar,
  9 |   Toolbar,
 10 |   Typography,
 11 |   Box,
 12 |   Button,
 13 |   Divider,
 14 |   Menu,
 15 |   MenuItem,
 16 |   Link,
 17 | } from "@material-ui/core";
 18 | import MenuIcon from "@material-ui/icons/Menu";
 19 | 
 20 | import LogIn from "components/LogIn";
 21 | 
 22 | import { useTranslation } from "react-i18next";
 23 | 
 24 | function Bar() {
 25 |   const [loggedIn, setLoggedIn] = useLocalStorage("loggedIn", false);
 26 |   const [disabledAuth] = useLocalStorage("disableAuth", false);
 27 |   const [anchorEl, setAnchorEl] = useState(null);
 28 | 
 29 |   const history = useHistory();
 30 | 
 31 |   const openMenu = (event) => {
 32 |     setAnchorEl(event.currentTarget);
 33 |   };
 34 | 
 35 |   const closeMenu = () => {
 36 |     setAnchorEl(null);
 37 |   };
 38 | 
 39 |   const onLogOutClick = () => {
 40 |     setLoggedIn(false);
 41 |     localStorage.clear();
 42 |     history.push("/");
 43 |     history.go(0);
 44 |   };
 45 | 
 46 |   const { t, i18n } = useTranslation();
 47 | 
 48 |   const menuItems = [
 49 |     // TODO: add settings page
 50 |     {
 51 |       name: t("settings"),
 52 |       to: "/settings",
 53 |     },
 54 |     ...(!disabledAuth
 55 |       ? [
 56 |           {
 57 |             name: t("logOut"),
 58 |             divide: true,
 59 |             onClick: onLogOutClick,
 60 |           },
 61 |         ]
 62 |       : []),
 63 |   ];
 64 | 
 65 |   return (
 66 |     
 71 |       
 72 |         
 73 |           
 74 |             
 80 |                81 |             
 82 |           
 83 |         
 84 |         {loggedIn && menuItems.length > 0 && (
 85 |           <>
 86 |             
 89 | 
 90 |             
143 |           >
144 |         )}
145 |         {!loggedIn && LogIn()}
146 |       
147 |     
148 |   );
149 | }
150 | 
151 | export default Bar;
152 | 
--------------------------------------------------------------------------------
/frontend/src/components/Bar/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dec0dOS/zero-ui/d104b0388e4ca691b421c56ccf06946c12913877/frontend/src/components/Bar/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/components/Bar/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Bar";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/HomeLoggedIn/HomeLoggedIn.jsx:
--------------------------------------------------------------------------------
 1 | import { useState, useEffect } from "react";
 2 | import { useHistory } from "react-router-dom";
 3 | 
 4 | import { Divider, Button, Grid, Typography, Box } from "@material-ui/core";
 5 | import useStyles from "./HomeLoggedIn.styles";
 6 | 
 7 | import NetworkButton from "./components/NetworkButton";
 8 | 
 9 | import API from "utils/API";
10 | import { generateNetworkConfig } from "utils/NetworkConfig";
11 | 
12 | import { useTranslation } from "react-i18next";
13 | 
14 | function HomeLoggedIn() {
15 |   const [networks, setNetworks] = useState([]);
16 | 
17 |   const classes = useStyles();
18 |   const history = useHistory();
19 | 
20 |   const createNetwork = async () => {
21 |     const network = await API.post("network", generateNetworkConfig());
22 |     console.log(network);
23 |     history.push("/network/" + network.data["config"]["id"]);
24 |   };
25 | 
26 |   useEffect(() => {
27 |     async function fetchData() {
28 |       const networks = await API.get("network");
29 |       setNetworks(networks.data);
30 |       console.log("Networks:", networks.data);
31 |     }
32 |     fetchData();
33 |   }, []);
34 | 
35 |   const { t, i18n } = useTranslation();
36 | 
37 |   return (
38 |
 81 |             
 82 |           
 83 |         
 84 |         {loggedIn && menuItems.length > 0 && (
 85 |           <>
 86 |             
 89 | 
 90 |             
143 |           >
144 |         )}
145 |         {!loggedIn && LogIn()}
146 |       
147 |     
148 |   );
149 | }
150 | 
151 | export default Bar;
152 | 
--------------------------------------------------------------------------------
/frontend/src/components/Bar/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dec0dOS/zero-ui/d104b0388e4ca691b421c56ccf06946c12913877/frontend/src/components/Bar/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/components/Bar/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Bar";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/HomeLoggedIn/HomeLoggedIn.jsx:
--------------------------------------------------------------------------------
 1 | import { useState, useEffect } from "react";
 2 | import { useHistory } from "react-router-dom";
 3 | 
 4 | import { Divider, Button, Grid, Typography, Box } from "@material-ui/core";
 5 | import useStyles from "./HomeLoggedIn.styles";
 6 | 
 7 | import NetworkButton from "./components/NetworkButton";
 8 | 
 9 | import API from "utils/API";
10 | import { generateNetworkConfig } from "utils/NetworkConfig";
11 | 
12 | import { useTranslation } from "react-i18next";
13 | 
14 | function HomeLoggedIn() {
15 |   const [networks, setNetworks] = useState([]);
16 | 
17 |   const classes = useStyles();
18 |   const history = useHistory();
19 | 
20 |   const createNetwork = async () => {
21 |     const network = await API.post("network", generateNetworkConfig());
22 |     console.log(network);
23 |     history.push("/network/" + network.data["config"]["id"]);
24 |   };
25 | 
26 |   useEffect(() => {
27 |     async function fetchData() {
28 |       const networks = await API.get("network");
29 |       setNetworks(networks.data);
30 |       console.log("Networks:", networks.data);
31 |     }
32 |     fetchData();
33 |   }, []);
34 | 
35 |   const { t, i18n } = useTranslation();
36 | 
37 |   return (
38 |     
39 |       
47 |       
48 |       
49 |         
50 |           {t("controllerNetworks")}
51 |           {networks[0] && t("controllerAddress")}
52 |           
53 |             {networks[0] && networks[0]["id"].slice(0, 10)}
54 |           
55 |         
56 |         
57 |           {t("network", { count: networks.length })}
58 |           
59 |             {networks[0] ? (
60 |               networks.map((network) => (
61 |                 
62 |                   
63 |                 
64 |               ))
65 |             ) : (
66 |               {t("createOneNetwork")}
67 |             )}
68 |           
69 |         
70 |       
71 |     
14 |       
15 |         
16 |           {network["id"]}
17 |           
18 |             {network["config"]["name"]}
19 |           
20 |           
21 |             
22 |               {network["config"]["ipAssignmentPools"] &&
23 |                 network["config"]["ipAssignmentPools"][0] &&
24 |                 getCIDRAddress(
25 |                   network["config"]["ipAssignmentPools"][0]["ipRangeStart"],
26 |                   network["config"]["ipAssignmentPools"][0]["ipRangeEnd"]
27 |                 )}
28 |             
29 |           
30 |         
31 |       
32 |
33 |   );
34 | }
35 | 
36 | export default NetworkButton;
37 | 
--------------------------------------------------------------------------------
/frontend/src/components/HomeLoggedIn/components/NetworkButton/NetworkButton.styles.jsx:
--------------------------------------------------------------------------------
 1 | import { makeStyles } from "@material-ui/core/styles";
 2 | 
 3 | const useStyles = makeStyles((theme) => ({
 4 |   link: {
 5 |     textDecoration: "none",
 6 |     color: "black",
 7 |   },
 8 |   flexContainer: {
 9 |     display: "flex",
10 |     flexDirection: "row",
11 |     paddingTop: "8px",
12 |   },
13 |   name: {
14 |     whiteSpace: "nowrap",
15 |     overflow: "hidden",
16 |     textOverflow: "ellipsis",
17 |   },
18 |   nwid: {
19 |     color: "#007fff",
20 |     fontWeight: "bolder",
21 |   },
22 |   cidr: {
23 |     color: "#b5b5b5",
24 |   },
25 | }));
26 | 
27 | export default useStyles;
28 | 
--------------------------------------------------------------------------------
/frontend/src/components/HomeLoggedIn/components/NetworkButton/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./NetworkButton";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/HomeLoggedIn/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./HomeLoggedIn";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/HomeLoggedOut/HomeLoggedOut.jsx:
--------------------------------------------------------------------------------
 1 | import { useEffect } from "react";
 2 | import { Grid, Typography } from "@material-ui/core";
 3 | import { useLocalStorage } from "react-use";
 4 | import { useHistory } from "react-router-dom";
 5 | 
 6 | import { useTranslation } from "react-i18next";
 7 | 
 8 | import axios from "axios";
 9 | 
10 | function HomeLoggedOut() {
11 |   const [, setLoggedIn] = useLocalStorage("loggedIn", false);
12 |   const [, setToken] = useLocalStorage("token", null);
13 |   const [, setDisableAuth] = useLocalStorage("disableAuth", false);
14 |   const history = useHistory();
15 | 
16 |   useEffect(() => {
17 |     async function fetchData() {
18 |       axios
19 |         .get("/auth/login", { withCredentials: true })
20 |         .then(function (response) {
21 |           if (!response.data.enabled) {
22 |             setLoggedIn(true);
23 |             setDisableAuth(true);
24 |             setToken("");
25 |             history.go(0);
26 |           } else {
27 |             setDisableAuth(false);
28 |           }
29 |         });
30 |     }
31 |     fetchData();
32 |   }, [history, setDisableAuth, setLoggedIn, setToken]);
33 | 
34 |   const { t, i18n } = useTranslation();
35 | 
36 |   return (
37 |     
47 |       
48 |         
49 |           {t("zerouiDesc")}
50 |         
51 | 
52 |         
53 |           {t("loginToContinue")}
54 |         
55 |       
56 |     
57 |   );
58 | }
59 | 
60 | export default HomeLoggedOut;
61 | 
--------------------------------------------------------------------------------
/frontend/src/components/HomeLoggedOut/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./HomeLoggedOut";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/Loading/Loading.jsx:
--------------------------------------------------------------------------------
 1 | import { Typography, Box, CircularProgress } from "@material-ui/core";
 2 | 
 3 | import useStyles from "./Loading.styles";
 4 | 
 5 | function Loading() {
 6 |   const classes = useStyles();
 7 | 
 8 |   return (
 9 |     
10 |       
11 |       
12 |         Loading
13 |         
14 |       
15 |     
16 |   );
17 | }
18 | 
19 | export default Loading;
20 | 
--------------------------------------------------------------------------------
/frontend/src/components/Loading/Loading.styles.jsx:
--------------------------------------------------------------------------------
 1 | // Loading.styles.jsx
 2 | import { makeStyles } from "@material-ui/core/styles";
 3 | 
 4 | const useStyles = makeStyles({
 5 |   root: {
 6 |     display: "flex",
 7 |     flexDirection: "column",
 8 |     justifyContent: "center",
 9 |     alignItems: "center",
10 |     height: "100vh",
11 |   },
12 |   loadingText: {
13 |     marginTop: "16px",
14 |     position: "relative",
15 |     "& .loadingDots::after": {
16 |       content: '"."',
17 |       position: "absolute",
18 |       left: "100%",
19 |       marginLeft: "4px",
20 |       animation: "$loadingDots 1s infinite",
21 |     },
22 |   },
23 |   "@keyframes loadingDots": {
24 |     "0%": { content: '"."' },
25 |     "25%": { content: '".."' },
26 |     "50%": { content: '"..."' },
27 |     "75%": { content: '"...."' },
28 |     "100%": { content: '"."' },
29 |   },
30 | });
31 | 
32 | export default useStyles;
33 | 
--------------------------------------------------------------------------------
/frontend/src/components/Loading/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Loading.jsx";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/LogIn/LogIn.jsx:
--------------------------------------------------------------------------------
 1 | import { Divider } from "@material-ui/core";
 2 | 
 3 | import LogInUser from "./components/LogInUser";
 4 | import LogInToken from "./components/LogInToken";
 5 | 
 6 | function LogIn() {
 7 |   return (
 8 |     <>
 9 |       {import.meta.env.DEV && (
10 |         <>
11 |           
12 |           
13 |         >
14 |       )}
15 |       
16 |     >
17 |   );
18 | }
19 | 
20 | export default LogIn;
21 | 
--------------------------------------------------------------------------------
/frontend/src/components/LogIn/components/LogInToken/LogInToken.jsx:
--------------------------------------------------------------------------------
 1 | import { useState } from "react";
 2 | import { useHistory } from "react-router-dom";
 3 | import { useLocalStorage } from "react-use";
 4 | 
 5 | import {
 6 |   TextField,
 7 |   Button,
 8 |   Dialog,
 9 |   DialogActions,
10 |   DialogContent,
11 |   DialogContentText,
12 |   DialogTitle,
13 | } from "@material-ui/core";
14 | 
15 | import { useTranslation } from "react-i18next";
16 | 
17 | function LogInToken() {
18 |   const [open, setOpen] = useState(false);
19 |   const [errorText, setErrorText] = useState("");
20 | 
21 |   const [, setLoggedIn] = useLocalStorage("loggedIn", false);
22 |   const [token, setToken] = useLocalStorage("token", null);
23 | 
24 |   const history = useHistory();
25 | 
26 |   const handleClickOpen = () => {
27 |     setOpen(true);
28 |   };
29 | 
30 |   const handleClose = () => {
31 |     setOpen(false);
32 |   };
33 | 
34 |   const handleKeyPress = (event) => {
35 |     const key = event.key;
36 | 
37 |     if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
38 |       return;
39 |     }
40 | 
41 |     if (key === "Enter") {
42 |       LogIn();
43 |     }
44 |   };
45 | 
46 |   const { t, i18n } = useTranslation();
47 | 
48 |   const LogIn = () => {
49 |     if (token.length !== 32) {
50 |       setErrorText("Token length error");
51 |       return;
52 |     }
53 |     setLoggedIn(true);
54 |     setToken(token);
55 |     handleClose();
56 |     history.go(0);
57 |   };
58 | 
59 |   return (
60 |     
61 |       
64 |       
90 |     
91 |   );
92 | }
93 | 
94 | export default LogInToken;
95 | 
--------------------------------------------------------------------------------
/frontend/src/components/LogIn/components/LogInToken/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./LogInToken";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/LogIn/components/LogInUser/LogInUser.jsx:
--------------------------------------------------------------------------------
  1 | import { useState } from "react";
  2 | import { useHistory } from "react-router-dom";
  3 | import { useLocalStorage } from "react-use";
  4 | import {
  5 |   TextField,
  6 |   Button,
  7 |   Dialog,
  8 |   DialogActions,
  9 |   DialogContent,
 10 |   DialogTitle,
 11 |   Snackbar,
 12 | } from "@material-ui/core";
 13 | 
 14 | import axios from "axios";
 15 | 
 16 | import { useTranslation } from "react-i18next";
 17 | 
 18 | function LogInUser() {
 19 |   const [open, setOpen] = useState(false);
 20 |   const [snackbarOpen, setSnackbarOpen] = useState(false);
 21 | 
 22 |   const [error, setError] = useState("");
 23 | 
 24 |   const [username, setUsername] = useState("");
 25 |   const [password, setPassword] = useState("");
 26 | 
 27 |   const [, setLoggedIn] = useLocalStorage("loggedIn", false);
 28 |   const [, setToken] = useLocalStorage("token", null);
 29 | 
 30 |   const history = useHistory();
 31 | 
 32 |   const handleClickOpen = () => {
 33 |     setOpen(true);
 34 |   };
 35 | 
 36 |   const handleClose = () => {
 37 |     setOpen(false);
 38 |     setSnackbarOpen(false);
 39 |   };
 40 | 
 41 |   const handleKeyPress = (event) => {
 42 |     const key = event.key;
 43 | 
 44 |     if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
 45 |       return;
 46 |     }
 47 | 
 48 |     if (key === "Enter") {
 49 |       LogIn();
 50 |     }
 51 |   };
 52 | 
 53 |   const LogIn = () => {
 54 |     if (!username || !password) {
 55 |       return;
 56 |     }
 57 | 
 58 |     axios
 59 |       .post("/auth/login", {
 60 |         username: username,
 61 |         password: password,
 62 |       })
 63 |       .then(function (response) {
 64 |         setLoggedIn(true);
 65 |         setToken(response.data.token);
 66 |         handleClose();
 67 |         history.go(0);
 68 |       })
 69 |       .catch(function (error) {
 70 |         setPassword("");
 71 |         setSnackbarOpen(true);
 72 |         setError(error.response.data.error);
 73 |         // console.error(error.response.data.error);
 74 |       });
 75 |   };
 76 | 
 77 |   const { t, i18n } = useTranslation();
 78 | 
 79 |   return (
 80 |     <>
 81 |       
 84 |       
118 |       
126 |     >
127 |   );
128 | }
129 | 
130 | export default LogInUser;
131 | 
--------------------------------------------------------------------------------
/frontend/src/components/LogIn/components/LogInUser/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./LogInUser";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/LogIn/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./LogIn";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkHeader/NetworkHeader.jsx:
--------------------------------------------------------------------------------
 1 | import { Grid, Typography } from "@material-ui/core";
 2 | 
 3 | function NetworkHeader({ network }) {
 4 |   return (
 5 |     
 6 |       
 7 |         {network["config"]["id"]}
 8 |       
 9 |       
10 |         {network["config"] && network["config"]["name"]}
11 |       
12 |       {network["config"] && network["description"]}
13 |     
14 |   );
15 | }
16 | 
17 | export default NetworkHeader;
18 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkHeader/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./NetworkHeader";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkManagement/NetworkManagement.jsx:
--------------------------------------------------------------------------------
 1 | import { useState } from "react";
 2 | import { useParams, useHistory } from "react-router-dom";
 3 | 
 4 | import {
 5 |   Accordion,
 6 |   AccordionSummary,
 7 |   AccordionDetails,
 8 |   Button,
 9 |   Dialog,
10 |   DialogContent,
11 |   DialogContentText,
12 |   DialogTitle,
13 |   DialogActions,
14 |   Typography,
15 | } from "@material-ui/core";
16 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
17 | import DeleteIcon from "@material-ui/icons/Delete";
18 | 
19 | import API from "utils/API";
20 | 
21 | import { useTranslation } from "react-i18next";
22 | 
23 | function NetworkManagement() {
24 |   const { nwid } = useParams();
25 |   const history = useHistory();
26 |   const [open, setOpen] = useState(false);
27 | 
28 |   const handleClickOpen = () => {
29 |     setOpen(true);
30 |   };
31 | 
32 |   const handleClose = () => {
33 |     setOpen(false);
34 |   };
35 | 
36 |   const sendDelReq = async () => {
37 |     const req = await API.delete("/network/" + nwid);
38 |     console.log("Action:", req);
39 |   };
40 | 
41 |   const deleteNetwork = async () => {
42 |     await sendDelReq();
43 |     history.push("/");
44 |     history.go(0);
45 |   };
46 | 
47 |   const { t, i18n } = useTranslation();
48 | 
49 |   return (
50 |     
51 |       }>
52 |         {t("management")}
53 |       
54 |       
55 |         }
59 |           onClick={handleClickOpen}
60 |         >
61 |           {t("deleteNetwork")}
62 |         
63 |         
77 |       
78 |     
79 |   );
80 | }
81 | 
82 | export default NetworkManagement;
83 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkManagement/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./NetworkManagement";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/NetworkMembers.jsx:
--------------------------------------------------------------------------------
  1 | import {
  2 |   Accordion,
  3 |   AccordionDetails,
  4 |   AccordionSummary,
  5 |   Checkbox,
  6 |   Grid,
  7 |   IconButton,
  8 |   Typography,
  9 | } from "@material-ui/core";
 10 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
 11 | import RefreshIcon from "@material-ui/icons/Refresh";
 12 | import { useCallback, useEffect, useState } from "react";
 13 | import DataTable from "react-data-table-component";
 14 | import { useParams } from "react-router-dom";
 15 | import API from "utils/API";
 16 | import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
 17 | import { formatDistance } from "date-fns";
 18 | import AddMember from "./components/AddMember";
 19 | import DeleteMember from "./components/DeleteMember";
 20 | import ManagedIP from "./components/ManagedIP";
 21 | import MemberName from "./components/MemberName";
 22 | import MemberSettings from "./components/MemberSettings";
 23 | 
 24 | import { useTranslation } from "react-i18next";
 25 | 
 26 | function NetworkMembers({ network }) {
 27 |   const { nwid } = useParams();
 28 |   const [members, setMembers] = useState([]);
 29 | 
 30 |   const fetchData = useCallback(async () => {
 31 |     try {
 32 |       const members = await API.get("network/" + nwid + "/member");
 33 |       setMembers(members.data);
 34 |       console.log("Members:", members.data);
 35 |     } catch (err) {
 36 |       console.error(err);
 37 |     }
 38 |   }, [nwid]);
 39 | 
 40 |   useEffect(() => {
 41 |     fetchData();
 42 |     const timer = setInterval(() => fetchData(), 30000);
 43 |     return () => clearInterval(timer);
 44 |   }, [nwid, fetchData]);
 45 | 
 46 |   const sendReq = async (mid, data) => {
 47 |     const req = await API.post("/network/" + nwid + "/member/" + mid, data);
 48 |     console.log("Action:", req);
 49 |   };
 50 | 
 51 |   const { t, i18n } = useTranslation();
 52 | 
 53 |   const handleChange =
 54 |     (member, key1, key2 = null, mode = "text", id = null) =>
 55 |     (event) => {
 56 |       const value = parseValue(event, mode, member, key1, key2, id);
 57 | 
 58 |       const updatedMember = replaceValue({ ...member }, key1, key2, value);
 59 | 
 60 |       const index = members.findIndex((item) => {
 61 |         return item["config"]["id"] === member["config"]["id"];
 62 |       });
 63 |       let mutableMembers = [...members];
 64 |       mutableMembers[index] = updatedMember;
 65 |       setMembers(mutableMembers);
 66 | 
 67 |       const data = setValue({}, key1, key2, value);
 68 |       sendReq(member["config"]["id"], data);
 69 |     };
 70 | 
 71 |   const columns = [
 72 |     {
 73 |       id: "auth",
 74 |       name: t("authorized"),
 75 |       minWidth: "80px",
 76 |       cell: (row) => (
 77 |         
 82 |       ),
 83 |     },
 84 |     {
 85 |       id: "address",
 86 |       name: t("address"),
 87 |       minWidth: "150px",
 88 |       cell: (row) => (
 89 |         {row.config.address}
 90 |       ),
 91 |     },
 92 |     {
 93 |       id: "name",
 94 |       name: t("name") + "/" + t("description"),
 95 |       minWidth: "250px",
 96 |       cell: (row) => ,
 97 |     },
 98 |     {
 99 |       id: "ips",
100 |       name: t("managedIPs"),
101 |       minWidth: "220px",
102 |       cell: (row) => ,
103 |     },
104 |     {
105 |       id: "lastSeen",
106 |       name: t("lastSeen"),
107 |       minWidth: "100px",
108 |       cell: (row) =>
109 |         row.online === 1 ? (
110 |           {"ONLINE"}
111 |         ) : row.controllerId === row.config.address ? (
112 |           {"CONTROLLER"}
113 |         ) : row.online === 0 ? (
114 |           
115 |             {row.lastOnline !== 0
116 |               ? formatDistance(row.lastOnline, row.clock, {
117 |                   includeSeconds: false,
118 |                   addSuffix: true,
119 |                 })
120 |               : "OFFLINE"}
121 |           
122 |         ) : (
123 |           {"RELAYED"}
124 |         ),
125 |     },
126 |     {
127 |       id: "physicalip",
128 |       name: t("version") + " / " + t("physIp") + " / " + t("latency"),
129 |       minWidth: "220px",
130 |       cell: (row) =>
131 |         row.online === 1 ? (
132 |           
133 |             {"v" +
134 |               row.config.vMajor +
135 |               "." +
136 |               row.config.vMinor +
137 |               "." +
138 |               row.config.vRev}
139 |             
140 |             {row.physicalAddress + "/" + row.physicalPort}
141 |             
142 |             {"(" + row.latency + " ms)"}
143 |           
144 |         ) : (
145 |           ""
146 |         ),
147 |     },
148 |     {
149 |       id: "delete",
150 |       name: t("settings"),
151 |       minWidth: "50px",
152 |       right: true,
153 |       cell: (row) => (
154 |         <>
155 |           
160 |           
161 |         >
162 |       ),
163 |     },
164 |   ];
165 | 
166 |   return (
167 |     
168 |       }>
169 |         {t("member", { count: members.length })}
170 |       
171 |       
172 |         
173 |           
174 |             
175 |           
176 |           
177 |             {members.length ? (
178 |               
183 |             ) : (
184 |               
194 |                 
195 |                   {t("noDevices")} {nwid}.
196 |                 
197 |               
198 |             )}
199 |           
200 |           
201 |             
202 |           
203 |         
204 |       
205 |     
206 |   );
207 | }
208 | 
209 | export default NetworkMembers;
210 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/AddMember/AddMember.jsx:
--------------------------------------------------------------------------------
 1 | import { useState } from "react";
 2 | 
 3 | import { List, Typography, IconButton, TextField } from "@material-ui/core";
 4 | import AddIcon from "@material-ui/icons/Add";
 5 | 
 6 | import API from "utils/API";
 7 | 
 8 | import { useTranslation } from "react-i18next";
 9 | 
10 | function AddMember({ nwid, callback }) {
11 |   const [member, setMember] = useState("");
12 | 
13 |   const handleInput = (event) => {
14 |     setMember(event.target.value);
15 |   };
16 | 
17 |   const addMemberReq = async () => {
18 |     if (member.length === 10) {
19 |       const req = await API.post("/network/" + nwid + "/member/" + member, {
20 |         config: { authorized: true },
21 |         hidden: false,
22 |       });
23 |       console.log("Action:", req);
24 |       callback();
25 |     }
26 |     setMember("");
27 |   };
28 | 
29 |   const { t, i18n } = useTranslation();
30 | 
31 |   return (
32 |     <>
33 |       {t("addMemberManually")}
34 |       
41 |         
46 | 
47 |         
48 |           
53 |         
54 |       
55 |     >
56 |   );
57 | }
58 | 
59 | export default AddMember;
60 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/AddMember/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./AddMember";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/DeleteMember/DeleteMember.jsx:
--------------------------------------------------------------------------------
 1 | import { useState } from "react";
 2 | 
 3 | import {
 4 |   Button,
 5 |   Dialog,
 6 |   DialogTitle,
 7 |   DialogContent,
 8 |   DialogContentText,
 9 |   DialogActions,
10 |   IconButton,
11 | } from "@material-ui/core";
12 | import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
13 | 
14 | import API from "utils/API";
15 | import { useTranslation } from "react-i18next";
16 | 
17 | function DeleteMember({ nwid, mid, callback }) {
18 |   const { t, i18n } = useTranslation();
19 |   const [open, setOpen] = useState(false);
20 | 
21 |   const handleClickOpen = () => {
22 |     setOpen(true);
23 |   };
24 | 
25 |   const handleClose = () => {
26 |     setOpen(false);
27 |   };
28 | 
29 |   const deleteMemberReq = async () => {
30 |     const req = await API.delete("/network/" + nwid + "/member/" + mid);
31 |     console.log("Action:", req);
32 |     setOpen(false);
33 |     callback();
34 |   };
35 | 
36 |   return (
37 |     <>
38 |       
39 |         
40 |       
41 |       
55 |     >
56 |   );
57 | }
58 | 
59 | export default DeleteMember;
60 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/DeleteMember/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./DeleteMember";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/ManagedIP/ManagedIP.jsx:
--------------------------------------------------------------------------------
 1 | import { useState } from "react";
 2 | 
 3 | import { Grid, List, TextField, IconButton } from "@material-ui/core";
 4 | import AddIcon from "@material-ui/icons/Add";
 5 | import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
 6 | 
 7 | import { validateIP, normilizeIP } from "utils/IP";
 8 | 
 9 | function ManagedIP({ member, handleChange }) {
10 |   const [ipInput, setIpInput] = useState();
11 |   const [normolizedInput, setNormolizedInput] = useState();
12 | 
13 |   const handleInput = (event) => {
14 |     const ip = event.target.value;
15 |     setIpInput(ip);
16 |     if (validateIP(ip)) {
17 |       setNormolizedInput(normilizeIP(ip));
18 |     }
19 |   };
20 | 
21 |   return (
22 |     
23 |       {member.config.ipAssignments.map((value, i) => (
24 |
29 |           
40 |             
41 |           
42 |           {value}
43 |         
44 |       ))}
45 | 
46 |
53 |         
64 |           
69 |         
70 |         
71 |       
72 |     
73 |   );
74 | }
75 | 
76 | export default ManagedIP;
77 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/ManagedIP/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./ManagedIP";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/MemberName/MemberName.jsx:
--------------------------------------------------------------------------------
 1 | import { Grid, TextField } from "@material-ui/core";
 2 | import { useTranslation } from "react-i18next";
 3 | 
 4 | function MemberName({ member, handleChange }) {
 5 |   const { t, i18n } = useTranslation();
 6 |   return (
 7 |     
 8 |       
17 |       
26 |     
27 |   );
28 | }
29 | 
30 | export default MemberName;
31 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/MemberName/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./MemberName";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/MemberSettings/MemberSettings.jsx:
--------------------------------------------------------------------------------
  1 | import {
  2 |   Checkbox,
  3 |   Dialog,
  4 |   DialogContent,
  5 |   DialogTitle,
  6 |   FormControlLabel,
  7 |   Grid,
  8 |   IconButton,
  9 |   Paper,
 10 |   Typography,
 11 | } from "@material-ui/core";
 12 | import BuildIcon from "@material-ui/icons/Build";
 13 | import { useState } from "react";
 14 | import Tag from "./components/Tag";
 15 | 
 16 | import { useTranslation } from "react-i18next";
 17 | 
 18 | function MemberSettings({ member, network, handleChange }) {
 19 |   const { t, i18n } = useTranslation();
 20 |   const [open, setOpen] = useState(false);
 21 | 
 22 |   const handleClickOpen = () => {
 23 |     setOpen(true);
 24 |   };
 25 | 
 26 |   const handleClose = () => {
 27 |     setOpen(false);
 28 |   };
 29 | 
 30 |   return (
 31 |     <>
 32 |       
 33 |         
 34 |       
 35 |       
128 |     >
129 |   );
130 | }
131 | 
132 | export default MemberSettings;
133 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/MemberSettings/components/Tag/Tag.jsx:
--------------------------------------------------------------------------------
  1 | import {
  2 |   Checkbox,
  3 |   FormControlLabel,
  4 |   Grid,
  5 |   IconButton,
  6 |   Input,
  7 |   Paper,
  8 |   Select,
  9 |   Typography,
 10 | } from "@material-ui/core";
 11 | import DeleteIcon from "@material-ui/icons/Delete";
 12 | import { useEffect, useState } from "react";
 13 | import { useDebounce } from "react-use";
 14 | 
 15 | function Tag({ member, tagName, tagDetail, handleChange }) {
 16 |   const [tagValue, setTagValue] = useState("");
 17 |   const [tagChangedByUser, setTagChangedByUser] = useState(false);
 18 | 
 19 |   useEffect(() => {
 20 |     let tagIndex = member["config"]["tags"].findIndex((item) => {
 21 |       return item[0] === tagDetail["id"];
 22 |     });
 23 |     let value = "";
 24 |     if (tagIndex !== -1) {
 25 |       value = member["config"]["tags"][tagIndex][1];
 26 |     }
 27 |     value = value !== false ? value : "";
 28 |     setTagValue(value);
 29 |   }, [member, tagDetail]);
 30 | 
 31 |   useDebounce(
 32 |     async () => {
 33 |       if (tagChangedByUser) {
 34 |         let value = tagValue === "" ? "" : parseInt(tagValue);
 35 |         let event = { target: { value: value } };
 36 |         handleChange(
 37 |           member,
 38 |           "config",
 39 |           "tags",
 40 |           "tagChange",
 41 |           tagDetail["id"]
 42 |         )(event);
 43 |       }
 44 |       setTagChangedByUser(false);
 45 |     },
 46 |     500,
 47 |     [tagValue]
 48 |   );
 49 | 
 50 |   const handleSelectChange = (event) => {
 51 |     let newValue = event.target.value;
 52 |     setTagChangedByUser(true);
 53 |     setTagValue(newValue);
 54 |   };
 55 | 
 56 |   const handleFlagChange = (value) => (event) => {
 57 |     let newValue;
 58 |     let oldValue;
 59 | 
 60 |     if (tagValue === "") {
 61 |       oldValue = 0;
 62 |     } else {
 63 |       oldValue = tagValue;
 64 |     }
 65 | 
 66 |     if (event.target.checked) {
 67 |       newValue = oldValue + value;
 68 |     } else {
 69 |       newValue = oldValue - value;
 70 |     }
 71 |     setTagChangedByUser(true);
 72 |     setTagValue(newValue);
 73 |   };
 74 | 
 75 |   const handleInputChange = (event) => {
 76 |     let value = event.target.value;
 77 |     if (/^(|0|[1-9]\d*)$/.test(value)) {
 78 |       value = value === "" ? value : parseInt(value);
 79 |     } else {
 80 |       value = 0;
 81 |     }
 82 |     setTagChangedByUser(true);
 83 |     setTagValue(value);
 84 |   };
 85 | 
 86 |   const clearTag = (event) => {
 87 |     setTagChangedByUser(true);
 88 |     setTagValue("");
 89 |   };
 90 | 
 91 |   return (
 92 |     
 93 |       
 94 |         
 95 |           
 99 |             {tagName}
100 |             {tagValue === "" ? (
101 |               ""
102 |             ) : (
103 |               
104 |                 
105 |               
106 |             )}
107 |           
108 |         
109 |         
110 |           
111 |             
140 |           
141 |           
142 |             
147 |           
148 |         
149 |         
150 |           {Object.entries(tagDetail["flags"]).map(([flagKey, flagValue]) => (
151 |             
158 |               }
159 |               key={"flag-" + flagKey}
160 |               label={flagKey}
161 |             />
162 |           ))}
163 |         
164 |       
165 |     
166 |   );
167 | }
168 | 
169 | export default Tag;
170 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/MemberSettings/components/Tag/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Tag";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/components/MemberSettings/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./MemberSettings";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkMembers/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./NetworkMembers";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkRules/NetworkRules.jsx:
--------------------------------------------------------------------------------
  1 | import {
  2 |   Accordion,
  3 |   AccordionDetails,
  4 |   AccordionSummary,
  5 |   Button,
  6 |   Divider,
  7 |   Grid,
  8 |   Hidden,
  9 |   Snackbar,
 10 |   Typography,
 11 | } from "@material-ui/core";
 12 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
 13 | import CodeMirror from "@uiw/react-codemirror";
 14 | import "codemirror/theme/3024-day.css";
 15 | import { compile } from "external/RuleCompiler";
 16 | import debounce from "lodash/debounce";
 17 | import { useState } from "react";
 18 | import API from "utils/API";
 19 | 
 20 | import { useTranslation } from "react-i18next";
 21 | 
 22 | function NetworkRules({ network, callback }) {
 23 |   const { t, i18n } = useTranslation();
 24 | 
 25 |   const [editor, setEditor] = useState(null);
 26 |   const [flowData, setFlowData] = useState({
 27 |     rules: [...network.config.rules],
 28 |     capabilities: [...network.config.capabilities],
 29 |     tags: [...network.config.tags],
 30 |   });
 31 |   const [tagCapByNameData, setTagCapByNameData] = useState({
 32 |     tagsByName: network.tagsByName || {},
 33 |     capabilitiesByName: network.capabilitiesByName || {},
 34 |   });
 35 |   const [errors, setErrors] = useState([]);
 36 |   const [snackbarOpen, setSnackbarOpen] = useState(false);
 37 | 
 38 |   const saveChanges = async () => {
 39 |     if (editor) {
 40 |       const req = await API.post("/network/" + network["config"]["id"], {
 41 |         config: { ...flowData },
 42 |         rulesSource: editor.getValue(),
 43 |         ...tagCapByNameData,
 44 |       });
 45 |       console.log("Action", req);
 46 |       setSnackbarOpen(true);
 47 |       const timer = setTimeout(() => {
 48 |         setSnackbarOpen(false);
 49 |       }, 1500);
 50 | 
 51 |       callback();
 52 | 
 53 |       return () => clearTimeout(timer);
 54 |     }
 55 |   };
 56 | 
 57 |   const onChange = debounce((event) => {
 58 |     const src = event.getValue();
 59 |     setEditor(event);
 60 |     let rules = [],
 61 |       caps = {},
 62 |       tags = {};
 63 |     const res = compile(src, rules, caps, tags);
 64 |     if (!res) {
 65 |       let capabilitiesByName = {};
 66 |       for (var key in caps) {
 67 |         capabilitiesByName[key] = caps[key].id;
 68 |       }
 69 | 
 70 |       let tagsByName = { ...tags };
 71 |       for (let key in tags) {
 72 |         tags[key] = { id: tags[key].id, default: tags[key].default };
 73 |       }
 74 | 
 75 |       setFlowData({
 76 |         rules: [...rules],
 77 |         capabilities: [...Object.values(caps)],
 78 |         tags: [...Object.values(tags)],
 79 |       });
 80 | 
 81 |       setTagCapByNameData({
 82 |         tagsByName: tagsByName,
 83 |         capabilitiesByName: capabilitiesByName,
 84 |       });
 85 |       setErrors([]);
 86 |     } else {
 87 |       setErrors(res);
 88 |     }
 89 |   }, 100);
 90 | 
 91 |   return (
 92 |     
 93 |       }>
 94 |         {t("flowRules")}
 95 |       
 96 |       
 97 |         {/*   Important note: value in CodeMirror instance means INITAIL VALUE
 98 |               or it could be used to replace editor state with the new value.
 99 |               No need to update on every user character input Flow Rules
100 |         */}
101 |         
106 |         
107 |
108 |             
119 |           
120 |         
121 |         
122 |         
131 |           {errors.length ? (
132 |             
133 |               {"[" + errors[0] + ":" + errors[1] + "] " + errors[2]}
134 |             
135 |           ) : (
136 |             
139 |           )}
140 |         
141 |         
149 |       
150 |     
151 |   );
152 | }
153 | 
154 | export default NetworkRules;
155 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkRules/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./NetworkRules";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkSettings/NetworkSettings.jsx:
--------------------------------------------------------------------------------
  1 | import {
  2 |   Accordion,
  3 |   AccordionSummary,
  4 |   AccordionDetails,
  5 |   Checkbox,
  6 |   Divider,
  7 |   Grid,
  8 |   Typography,
  9 |   TextField,
 10 |   Select,
 11 | } from "@material-ui/core";
 12 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
 13 | 
 14 | import ManagedRoutes from "./components/ManagedRoutes";
 15 | import IPv4AutoAssign from "./components/IPv4AutoAssign";
 16 | 
 17 | import API from "utils/API";
 18 | import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
 19 | 
 20 | import { useTranslation } from "react-i18next";
 21 | 
 22 | function NetworkSettings({ network, setNetwork }) {
 23 |   const { t, i18n } = useTranslation();
 24 |   const sendReq = async (data) => {
 25 |     try {
 26 |       const req = await API.post("/network/" + network["config"]["id"], data);
 27 |       console.log("Action", req);
 28 |     } catch (err) {
 29 |       console.error(err);
 30 |     }
 31 |   };
 32 | 
 33 |   const handleChange =
 34 |     (key1, key2, mode = "text", additionalData = null) =>
 35 |     (event) => {
 36 |       const value = parseValue(event, mode, additionalData);
 37 | 
 38 |       let updatedNetwork = replaceValue({ ...network }, key1, key2, value);
 39 |       setNetwork(updatedNetwork);
 40 | 
 41 |       let data = setValue({}, key1, key2, value);
 42 | 
 43 |       sendReq(data);
 44 |     };
 45 | 
 46 |   return (
 47 |     
 48 |       }>
 49 |         {t("generalSettings")}
 50 |       
 51 |       
 52 |         
 53 |           
 54 |             {t("networkId")}
 55 |             
 56 |               {network["config"]["id"]}
 57 |             
 58 |           
 59 |           
 60 |             
 69 |           
 70 |           
 71 |             
 83 |           
 84 |           
 85 |           
 86 |             {t("accessControl")}
 87 |             
 95 |           
 96 |           
 97 |           
 98 |             
102 |           
103 |           
104 |           
105 |             
109 |           
110 |           {/* TODO: */}
111 |           {/* 
112 |             IPv6 Auto-Assign
113 |            */}
114 |           
115 |           
116 |             
125 |           
126 |           
127 |             
132 |             {t("enableBroadcast")}
133 |           
134 |           {/* TODO: */}
135 |           {/* 
136 |             DNS
137 |            */}
138 |         
139 |       
140 |     
141 |   );
142 | }
143 | 
144 | export default NetworkSettings;
145 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkSettings/components/IPv4AutoAssign/IPv4AutoAssign.jsx:
--------------------------------------------------------------------------------
  1 | import { useState } from "react";
  2 | 
  3 | import {
  4 |   Button,
  5 |   Box,
  6 |   Divider,
  7 |   Grid,
  8 |   List,
  9 |   Typography,
 10 |   TextField,
 11 |   IconButton,
 12 | } from "@material-ui/core";
 13 | import AddIcon from "@material-ui/icons/Add";
 14 | import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
 15 | 
 16 | import DataTable from "react-data-table-component";
 17 | 
 18 | import { addressPool } from "utils/NetworkConfig";
 19 | import { getCIDRAddress, validateIP, normilizeIP } from "utils/IP";
 20 | 
 21 | import { useTranslation } from "react-i18next";
 22 | 
 23 | function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
 24 |   const { t, i18n } = useTranslation();
 25 |   const [start, setStart] = useState("");
 26 |   const [end, setEnd] = useState("");
 27 | 
 28 |   const handleStartInput = (event) => {
 29 |     setStart(event.target.value);
 30 |   };
 31 | 
 32 |   const handleEndInput = (event) => {
 33 |     setEnd(event.target.value);
 34 |   };
 35 | 
 36 |   const setDefaultPool = (index) => {
 37 |     addPoolReq(addressPool[index]["start"], addressPool[index]["end"], true);
 38 | 
 39 |     handleChange("config", "routes", "custom", [
 40 |       {
 41 |         target: getCIDRAddress(
 42 |           addressPool[index]["start"],
 43 |           addressPool[index]["end"]
 44 |         ),
 45 |       },
 46 |     ])(null);
 47 |   };
 48 | 
 49 |   const addPoolReq = (localStart, localEnd, reset = false) => {
 50 |     let data = {};
 51 |     console.log(localStart, localEnd);
 52 |     if (validateIP(localStart) && validateIP(localEnd)) {
 53 |       data["ipRangeStart"] = normilizeIP(localStart);
 54 |       data["ipRangeEnd"] = normilizeIP(localEnd);
 55 |     } else {
 56 |       return;
 57 |     }
 58 | 
 59 |     let newPool = [];
 60 |     if (ipAssignmentPools && !reset) {
 61 |       newPool = [...ipAssignmentPools];
 62 |     }
 63 |     newPool.push(data);
 64 |     console.log(newPool);
 65 | 
 66 |     handleChange("config", "ipAssignmentPools", "custom", newPool)(null);
 67 | 
 68 |     setStart("");
 69 |     setEnd("");
 70 |   };
 71 | 
 72 |   const removePoolReq = (index) => {
 73 |     let newPool = [...ipAssignmentPools];
 74 |     newPool.splice(index, 1);
 75 | 
 76 |     handleChange("config", "ipAssignmentPools", "custom", newPool)(null);
 77 |   };
 78 | 
 79 |   const columns = [
 80 |     {
 81 |       id: "remove",
 82 |       width: "10px",
 83 |       cell: (_, index) => (
 84 |          removePoolReq(index)}
 88 |         >
 89 |           
 90 |         
 91 |       ),
 92 |     },
 93 |     {
 94 |       id: "Start",
 95 |       name: t("start"),
 96 |       cell: (row) => row["ipRangeStart"],
 97 |     },
 98 |     {
 99 |       id: "End",
100 |       name: t("end"),
101 |       cell: (row) => row["ipRangeEnd"],
102 |     },
103 |   ];
104 | 
105 |   return (
106 |     <>
107 |       {t("ipv4AutoAssign")}
108 |       
113 |         
114 |           {addressPool.map((item, index) => (
115 |             
116 |               
123 |             
124 |           ))}
125 |         
126 |       
127 |       
128 |         {t("autoAssignPool")}
129 |       
130 |       
131 |         
132 |           
137 |           
138 |           {t("addIPv4Pool")}
139 |           
145 |             
150 |             
157 |             
162 |              addPoolReq(start, end)}
166 |             >
167 |               
172 |             
173 |           
174 |         
175 |       
176 |     >
177 |   );
178 | }
179 | 
180 | export default IPv4AutoAssign;
181 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkSettings/components/IPv4AutoAssign/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./IPv4AutoAssign";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkSettings/components/ManagedRoutes/ManagedRoutes.jsx:
--------------------------------------------------------------------------------
  1 | import { useState } from "react";
  2 | 
  3 | import {
  4 |   Box,
  5 |   Divider,
  6 |   Grid,
  7 |   List,
  8 |   Typography,
  9 |   TextField,
 10 |   IconButton,
 11 | } from "@material-ui/core";
 12 | import AddIcon from "@material-ui/icons/Add";
 13 | import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
 14 | 
 15 | import DataTable from "react-data-table-component";
 16 | 
 17 | import { validateIP, normilizeIP, validateCIDR } from "utils/IP";
 18 | 
 19 | import { useTranslation } from "react-i18next";
 20 | 
 21 | function ManagedRoutes({ routes, handleChange }) {
 22 |   const { t, i18n } = useTranslation();
 23 |   const [destination, setDestination] = useState("");
 24 |   const [via, setVia] = useState("");
 25 | 
 26 |   const handleDestinationInput = (event) => {
 27 |     setDestination(event.target.value);
 28 |   };
 29 | 
 30 |   const handleViaInput = (event) => {
 31 |     setVia(event.target.value);
 32 |   };
 33 | 
 34 |   const addRouteReq = () => {
 35 |     let data = {};
 36 |     if (validateCIDR(destination)) {
 37 |       data["target"] = destination;
 38 |     } else {
 39 |       return;
 40 |     }
 41 |     if (via && validateIP(via)) {
 42 |       data["via"] = normilizeIP(via);
 43 |     }
 44 | 
 45 |     let newRoutes = [...routes];
 46 |     newRoutes.push(data);
 47 | 
 48 |     handleChange("config", "routes", "custom", newRoutes)(null);
 49 | 
 50 |     setDestination("");
 51 |     setVia("");
 52 |   };
 53 | 
 54 |   const removeRouteReq = (index) => {
 55 |     let newRoutes = [...routes];
 56 |     newRoutes.splice(index, 1);
 57 | 
 58 |     handleChange("config", "routes", "custom", newRoutes)(null);
 59 |   };
 60 | 
 61 |   const columns = [
 62 |     {
 63 |       id: "remove",
 64 |       width: "10px",
 65 |       cell: (_, index) => (
 66 |          removeRouteReq(index)}
 70 |         >
 71 |           
 72 |         
 73 |       ),
 74 |     },
 75 |     {
 76 |       id: "target",
 77 |       name: t("target"),
 78 |       cell: (row) => row["target"],
 79 |     },
 80 |     {
 81 |       id: "via",
 82 |       name: t("via"),
 83 |       cell: (row) => (row["via"] ? row["via"] : "(LAN)"),
 84 |     },
 85 |   ];
 86 | 
 87 |   return (
 88 |     <>
 89 |       
 90 |         {t("managedRoutes")} ({routes.length + "/128"})
 91 |       
 92 |       
 93 |         
 94 |           
 95 |           
 96 |           {t("addRoute")}
 97 |
103 |             
108 |             
115 |             
120 |             
121 |               
126 |             
127 |           
128 |         
129 |       
130 |     >
131 |   );
132 | }
133 | 
134 | export default ManagedRoutes;
135 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkSettings/components/ManagedRoutes/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./ManagedRoutes";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/NetworkSettings/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./NetworkSettings";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/Settings/Settings.jsx:
--------------------------------------------------------------------------------
 1 | import {
 2 |   Accordion,
 3 |   AccordionSummary,
 4 |   AccordionDetails,
 5 |   Grid,
 6 |   Typography,
 7 |   Select,
 8 | } from "@material-ui/core";
 9 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
10 | 
11 | import { useTranslation } from "react-i18next";
12 | import localesList from "generated/localesList.json";
13 | 
14 | function Settings() {
15 |   const { t, i18n } = useTranslation();
16 | 
17 |   const handleChange = () => (event) => {
18 |     i18n.changeLanguage(event.target.value);
19 |   };
20 | 
21 |   return (
22 |     
23 |       }>
24 |         {t("language")}
25 |       
26 |       
27 |         
28 |           
35 |         
36 |       
37 |     
38 |   );
39 | }
40 | 
41 | export default Settings;
42 | 
--------------------------------------------------------------------------------
/frontend/src/components/Settings/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Settings";
2 | 
--------------------------------------------------------------------------------
/frontend/src/components/Theme/Theme.jsx:
--------------------------------------------------------------------------------
 1 | import { ThemeProvider } from "@material-ui/styles";
 2 | import { createTheme } from "@material-ui/core/styles";
 3 | import { red, amber } from "@material-ui/core/colors";
 4 | 
 5 | const theme = createTheme({
 6 |   palette: {
 7 |     primary: {
 8 |       main: amber[500],
 9 |     },
10 |     secondary: {
11 |       main: red[500],
12 |     },
13 |     type: "light",
14 |   },
15 | });
16 | 
17 | function Theme({ children }) {
18 |   return {children};
19 | }
20 | 
21 | export default Theme;
22 | 
--------------------------------------------------------------------------------
/frontend/src/components/Theme/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Theme";
2 | 
--------------------------------------------------------------------------------
/frontend/src/generated/localesList.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |     "code": "en",
 4 |     "name": "English"
 5 |   },
 6 |   {
 7 |     "code": "es-ES",
 8 |     "name": "Español"
 9 |   },
10 |   {
11 |     "code": "ru-RU",
12 |     "name": "Русский"
13 |   },
14 |   {
15 |     "code": "zh_CN",
16 |     "name": "中文"
17 |   }
18 | ]
19 | 
--------------------------------------------------------------------------------
/frontend/src/i18n.js:
--------------------------------------------------------------------------------
 1 | import i18n from "i18next";
 2 | import languageDetector from "i18next-browser-languagedetector";
 3 | import { initReactI18next } from "react-i18next";
 4 | import Backend from "i18next-http-backend";
 5 | 
 6 | import localesList from "./utils/localesList.json";
 7 | const supportedLngs = localesList.map((locale) => locale.code);
 8 | 
 9 | i18n
10 |   .use(languageDetector)
11 |   .use(initReactI18next)
12 |   .use(Backend)
13 |   .init({
14 |     compatibilityJSON: "v4",
15 |     fallbackLng: "en",
16 |     detection: {
17 |       order: ["path", "cookie", "localStorage", "htmlTag"],
18 |       caches: ["localStorage", "cookie"],
19 |     },
20 |     debug: true,
21 |     interpolation: {
22 |       escapeValue: true,
23 |     },
24 |     react: {
25 |       useSuspense: true,
26 |     },
27 |     supportedLngs,
28 |     backend: {
29 |       loadPath: "/locales/{{lng}}/{{ns}}.json",
30 |     },
31 |     ns: ["common"],
32 |     defaultNS: "common",
33 |   });
34 | 
35 | export default i18n;
36 | 
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
 1 | body {
 2 |   margin: 0;
 3 |   overflow-x: hidden;
 4 |   font-family:
 5 |     "Roboto",
 6 |     -apple-system,
 7 |     BlinkMacSystemFont,
 8 |     "Segoe UI",
 9 |     "Oxygen",
10 |     "Ubuntu",
11 |     "Cantarell",
12 |     "Fira Sans",
13 |     "Droid Sans",
14 |     "Helvetica Neue",
15 |     sans-serif;
16 |   -webkit-font-smoothing: antialiased;
17 |   -moz-osx-font-smoothing: grayscale;
18 | }
19 | 
--------------------------------------------------------------------------------
/frontend/src/index.jsx:
--------------------------------------------------------------------------------
 1 | import "./index.css";
 2 | 
 3 | import React from "react";
 4 | import ReactDOM from "react-dom";
 5 | 
 6 | import App from "./App";
 7 | 
 8 | ReactDOM.render(
 9 |   
10 |     
11 |   ,
12 |   document.getElementById("root")
13 | );
14 | 
--------------------------------------------------------------------------------
/frontend/src/routes/Home/Home.jsx:
--------------------------------------------------------------------------------
 1 | import { useLocalStorage } from "react-use";
 2 | 
 3 | import HomeLoggedIn from "components/HomeLoggedIn";
 4 | import HomeLoggedOut from "components/HomeLoggedOut";
 5 | 
 6 | function Home() {
 7 |   const [loggedIn] = useLocalStorage("loggedIn", false);
 8 | 
 9 |   if (loggedIn) {
10 |     return ;
11 |   } else {
12 |     return ;
13 |   }
14 | }
15 | 
16 | export default Home;
17 | 
--------------------------------------------------------------------------------
/frontend/src/routes/Home/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Home";
2 | 
--------------------------------------------------------------------------------
/frontend/src/routes/Network/Network.jsx:
--------------------------------------------------------------------------------
 1 | import { Grid, Link, Typography } from "@material-ui/core";
 2 | import ArrowBackIcon from "@material-ui/icons/ArrowBack";
 3 | import NetworkHeader from "components/NetworkHeader";
 4 | import NetworkManagement from "components/NetworkManagement";
 5 | import NetworkMembers from "components/NetworkMembers";
 6 | import NetworkRules from "components/NetworkRules";
 7 | import NetworkSettings from "components/NetworkSettings";
 8 | import { useCallback, useEffect, useState } from "react";
 9 | import { Link as RouterLink, useHistory, useParams } from "react-router-dom";
10 | import { useLocalStorage } from "react-use";
11 | import API from "utils/API";
12 | import useStyles from "./Network.styles";
13 | 
14 | import { useTranslation } from "react-i18next";
15 | 
16 | function Network() {
17 |   const { t, i18n } = useTranslation();
18 |   const { nwid } = useParams();
19 |   const [loggedIn] = useLocalStorage("loggedIn", false);
20 |   const [network, setNetwork] = useState({});
21 | 
22 |   const classes = useStyles();
23 |   const history = useHistory();
24 | 
25 |   const fetchData = useCallback(async () => {
26 |     try {
27 |       const network = await API.get("network/" + nwid);
28 |       setNetwork(network.data);
29 |       console.log("Current network:", network.data);
30 |     } catch (err) {
31 |       if (err.response.status === 404) {
32 |         history.push("/404");
33 |       }
34 |       console.error(err);
35 |     }
36 |   }, [nwid, history]);
37 | 
38 |   useEffect(() => {
39 |     fetchData();
40 |   }, [nwid, fetchData]);
41 | 
42 |   if (loggedIn) {
43 |     return (
44 |       <>
45 |
46 |           
47 |             
48 |             {t("network", { count: 2 })}
49 |           
50 |         
52 |           {network["config"] && (
53 |             <>
54 |               
55 |               
56 |             >
57 |           )}
58 |           
59 |           {network["config"] && (
60 |             
61 |           )}
62 |           
63 |         
64 |       >
65 |     );
66 |   } else {
67 |     return (
68 |       
78 |         
79 |           {t("notAuthorized")}
80 |         
81 |       
82 |     );
83 |   }
84 | }
85 | 
86 | export default Network;
87 | 
--------------------------------------------------------------------------------
/frontend/src/routes/Network/Network.styles.jsx:
--------------------------------------------------------------------------------
 1 | import { makeStyles } from "@material-ui/core/styles";
 2 | 
 3 | const useStyles = makeStyles((theme) => ({
 4 |   backIcon: {
 5 |     fontSize: 12,
 6 |   },
 7 |   container: {
 8 |     margin: "3%",
 9 |   },
10 |   breadcrumbs: {
11 |     paddingTop: "2%",
12 |     paddingLeft: "2%",
13 |   },
14 | }));
15 | 
16 | export default useStyles;
17 | 
--------------------------------------------------------------------------------
/frontend/src/routes/Network/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Network";
2 | 
--------------------------------------------------------------------------------
/frontend/src/routes/NotFound/NotFound.jsx:
--------------------------------------------------------------------------------
 1 | import { Grid, Typography } from "@material-ui/core";
 2 | 
 3 | function NotFound() {
 4 |   return (
 5 |     
15 |       
16 |         
17 |           404
18 |         
19 | 
20 |         
21 |           Not found
22 |         
23 |       
24 |     
25 |   );
26 | }
27 | 
28 | export default NotFound;
29 | 
--------------------------------------------------------------------------------
/frontend/src/routes/NotFound/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./NotFound";
2 | 
--------------------------------------------------------------------------------
/frontend/src/routes/Settings/Settings.jsx:
--------------------------------------------------------------------------------
 1 | import { Grid, Link, Typography } from "@material-ui/core";
 2 | import ArrowBackIcon from "@material-ui/icons/ArrowBack";
 3 | import SettingsComponent from "components/Settings";
 4 | 
 5 | import { Link as RouterLink } from "react-router-dom";
 6 | import { useLocalStorage } from "react-use";
 7 | 
 8 | import useStyles from "./Settings.styles";
 9 | 
10 | import { useTranslation } from "react-i18next";
11 | 
12 | function Settings() {
13 |   const { t, i18n } = useTranslation();
14 |   const [loggedIn] = useLocalStorage("loggedIn", false);
15 | 
16 |   const classes = useStyles();
17 | 
18 |   if (loggedIn) {
19 |     return (
20 |       <>
21 |         
22 |           
23 |             
24 |             {t("settings")}
25 |           
26 |         
28 |           
29 |         
30 |       >
31 |     );
32 |   } else {
33 |     return (
34 |       
44 |         
45 |           {t("notAuthorized")}
46 |         
47 |       
48 |     );
49 |   }
50 | }
51 | 
52 | export default Settings;
53 | 
--------------------------------------------------------------------------------
/frontend/src/routes/Settings/Settings.styles.jsx:
--------------------------------------------------------------------------------
 1 | import { makeStyles } from "@material-ui/core/styles";
 2 | 
 3 | const useStyles = makeStyles((theme) => ({
 4 |   backIcon: {
 5 |     fontSize: 12,
 6 |   },
 7 |   container: {
 8 |     margin: "3%",
 9 |   },
10 |   breadcrumbs: {
11 |     paddingTop: "2%",
12 |     paddingLeft: "2%",
13 |   },
14 | }));
15 | 
16 | export default useStyles;
17 | 
--------------------------------------------------------------------------------
/frontend/src/routes/Settings/index.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./Settings";
2 | 
--------------------------------------------------------------------------------
/frontend/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.png";
2 | 
--------------------------------------------------------------------------------
/frontend/src/utils/API.js:
--------------------------------------------------------------------------------
 1 | import axios from "axios";
 2 | 
 3 | const baseURL = "/api/";
 4 | 
 5 | export default axios.create({
 6 |   baseURL: baseURL,
 7 |   responseType: "json",
 8 |   withCredentials: "true",
 9 |   headers:
10 |     localStorage.getItem("disableAuth") === "true"
11 |       ? {}
12 |       : {
13 |           Authorization: `token ${JSON.parse(localStorage.getItem("token"))}`,
14 |         },
15 | });
16 | 
--------------------------------------------------------------------------------
/frontend/src/utils/ChangeHelper.js:
--------------------------------------------------------------------------------
 1 | import { pull } from "lodash";
 2 | 
 3 | export function parseValue(
 4 |   event,
 5 |   mode = "text",
 6 |   data = null,
 7 |   key1 = null,
 8 |   key2 = null,
 9 |   id = null
10 | ) {
11 |   let value;
12 |   if (mode === "json") {
13 |     value = JSON.parse(event.target.value);
14 |   } else if (mode === "checkbox") {
15 |     value = event.target.checked;
16 |   } else if (mode === "arrayDel") {
17 |     value = data[key1][key2];
18 |     if (id !== null) {
19 |       value.splice(id, 1);
20 |     }
21 |   } else if (mode === "arrayAdd") {
22 |     value = data[key1][key2];
23 |     if (id) {
24 |       value.push(id);
25 |     }
26 |   } else if (mode === "custom") {
27 |     value = data;
28 |   } else if (mode === "capChange") {
29 |     value = data[key1][key2];
30 |     if (event.target.checked) {
31 |       value.push(id);
32 |     } else {
33 |       pull(value, id);
34 |     }
35 |   } else if (mode === "tagChange") {
36 |     value = data[key1][key2];
37 |     let tagValue = event.target.value;
38 |     let tagIndex = value.findIndex((item) => {
39 |       return item[0] === id;
40 |     });
41 |     if (tagIndex !== -1) {
42 |       value.splice(tagIndex, 1);
43 |     }
44 |     if (tagValue !== "") {
45 |       value.push([id, tagValue]);
46 |     }
47 |   } else {
48 |     value = event.target.value;
49 |   }
50 |   return value;
51 | }
52 | 
53 | export function replaceValue(data, key1, key2, value) {
54 |   if (key2) {
55 |     data[key1][key2] = value;
56 |   } else {
57 |     data[key1] = value;
58 |   }
59 |   return data;
60 | }
61 | 
62 | export function setValue(data, key1, key2, value) {
63 |   if (key2) {
64 |     data = {
65 |       [key1]: { [key2]: value },
66 |     };
67 |   } else {
68 |     data = {
69 |       [key1]: value,
70 |     };
71 |   }
72 |   return data;
73 | }
74 | 
--------------------------------------------------------------------------------
/frontend/src/utils/IP.js:
--------------------------------------------------------------------------------
 1 | import ipaddr from "ipaddr.js";
 2 | 
 3 | export function getCIDRAddress(start, end) {
 4 |   const cidr = getCIDR(start, end);
 5 |   return start.replace(/.$/, 0) + "/" + cidr;
 6 | }
 7 | 
 8 | function getCIDR(start, end) {
 9 |   const startInt = toInt(start);
10 |   const endInt = toInt(end);
11 |   const binaryXOR = startInt ^ endInt;
12 |   if (binaryXOR === 0) {
13 |     return 32;
14 |   } else {
15 |     const binaryStr = binaryXOR.toString(2);
16 |     const zeroCount = binaryStr.split("0").length - 1;
17 |     const oneCount = binaryStr.split("1").length - 1;
18 |     return 32 - (zeroCount + oneCount);
19 |   }
20 | }
21 | 
22 | function toInt(addr) {
23 |   const ip = ipaddr.parse(addr);
24 |   const arr = ip.octets;
25 |   let ipInt = 0;
26 |   let counter = 3;
27 |   for (const i in arr) {
28 |     ipInt += arr[i] * Math.pow(256, counter);
29 |     counter--;
30 |   }
31 |   return ipInt;
32 | }
33 | 
34 | export function validateIP(string) {
35 |   return ipaddr.IPv4.isValid(string) || ipaddr.IPv6.isValid(string);
36 | }
37 | 
38 | export function normilizeIP(string) {
39 |   const addr = ipaddr.parse(string);
40 |   return addr.toNormalizedString();
41 | }
42 | 
43 | export function validateCIDR(string) {
44 |   try {
45 |     ipaddr.parseCIDR(string);
46 |     return true;
47 |   } catch (err) {
48 |     return false;
49 |   }
50 | }
51 | 
--------------------------------------------------------------------------------
/frontend/src/utils/NetworkConfig.js:
--------------------------------------------------------------------------------
  1 | export function generateNetworkConfig() {
  2 |   const randSubnetPart = getRandomInt(0, 254).toString();
  3 |   const randNamePart = new Date().getTime();
  4 |   return {
  5 |     config: {
  6 |       name: "new-net-" + randNamePart.toString().substring(8),
  7 |       private: true,
  8 |       v6AssignMode: { rfc4193: false, "6plane": false, zt: false },
  9 |       v4AssignMode: { zt: true },
 10 |       routes: [
 11 |         {
 12 |           target: "172.30." + randSubnetPart + ".0/24",
 13 |           via: null,
 14 |           flags: 0,
 15 |           metric: 0,
 16 |         },
 17 |       ],
 18 |       ipAssignmentPools: [
 19 |         {
 20 |           ipRangeStart: "172.30." + randSubnetPart + ".1",
 21 |           ipRangeEnd: "172.30." + randSubnetPart + ".254",
 22 |         },
 23 |       ],
 24 |       enableBroadcast: true,
 25 |     },
 26 |   };
 27 | }
 28 | 
 29 | function getRandomInt(min, max) {
 30 |   min = Math.ceil(min);
 31 |   max = Math.floor(max);
 32 |   return Math.floor(Math.random() * (max - min)) + min;
 33 | }
 34 | 
 35 | export const addressPool = [
 36 |   {
 37 |     name: "10.147.17.*",
 38 |     start: "10.147.17.1",
 39 |     end: "10.147.17.254",
 40 |   },
 41 |   {
 42 |     name: "10.147.18.*",
 43 |     start: "10.147.18.1",
 44 |     end: "10.147.18.254",
 45 |   },
 46 |   {
 47 |     name: "10.147.19.*",
 48 |     start: "10.147.19.1",
 49 |     end: "10.147.19.254",
 50 |   },
 51 |   {
 52 |     name: "10.147.20.*",
 53 |     start: "10.147.20.1",
 54 |     end: "10.147.20.254",
 55 |   },
 56 |   {
 57 |     name: "10.241.*.*",
 58 |     start: "10.241.0.1",
 59 |     end: "10.241.255.254",
 60 |   },
 61 |   {
 62 |     name: "10.242.*.*",
 63 |     start: "10.242.0.1",
 64 |     end: "10.242.255.254",
 65 |   },
 66 |   {
 67 |     name: "10.243.*.*",
 68 |     start: "10.243.0.1",
 69 |     end: "10.243.255.254",
 70 |   },
 71 |   {
 72 |     name: "10.244.*.*",
 73 |     start: "10.244.0.1",
 74 |     end: "10.244.255.254",
 75 |   },
 76 |   {
 77 |     name: "172.23.*.*",
 78 |     start: "172.23.0.1",
 79 |     end: "172.23.255.254",
 80 |   },
 81 |   {
 82 |     name: "172.24.*.*",
 83 |     start: "172.24.0.1",
 84 |     end: "172.24.255.254",
 85 |   },
 86 |   {
 87 |     name: "172.25.*.*",
 88 |     start: "172.25.0.1",
 89 |     end: "172.25.255.254",
 90 |   },
 91 |   {
 92 |     name: "172.26.*.*",
 93 |     start: "172.26.0.1",
 94 |     end: "172.26.255.254",
 95 |   },
 96 |   {
 97 |     name: "172.27.*.*",
 98 |     start: "172.27.0.1",
 99 |     end: "172.27.255.254",
100 |   },
101 |   {
102 |     name: "172.28.*.*",
103 |     start: "172.28.0.1",
104 |     end: "172.28.255.254",
105 |   },
106 |   {
107 |     name: "172.29.*.*",
108 |     start: "172.29.0.1",
109 |     end: "172.29.255.254",
110 |   },
111 |   {
112 |     name: "172.30.*.*",
113 |     start: "172.30.0.1",
114 |     end: "172.30.255.254",
115 |   },
116 |   {
117 |     name: "192.168.192.*",
118 |     start: "192.168.192.1",
119 |     end: "192.168.192.254",
120 |   },
121 |   {
122 |     name: "192.168.193.*",
123 |     start: "192.168.193.1",
124 |     end: "192.168.193.254",
125 |   },
126 |   {
127 |     name: "192.168.194.*",
128 |     start: "192.168.194.1",
129 |     end: "192.168.194.254",
130 |   },
131 |   {
132 |     name: "192.168.195.*",
133 |     start: "192.168.195.1",
134 |     end: "192.168.195.254",
135 |   },
136 | ];
137 | 
--------------------------------------------------------------------------------
/frontend/src/utils/localesList.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |     "code": "en",
 4 |     "name": "English"
 5 |   },
 6 |   {
 7 |     "code": "es-ES",
 8 |     "name": "Español"
 9 |   },
10 |   {
11 |     "code": "ru-RU",
12 |     "name": "Русский"
13 |   }
14 | ]
15 | 
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "extends": "../tsconfig.json",
 3 |   "compilerOptions": {
 4 |     "rootDir": ".",
 5 |     "baseUrl": "src",
 6 |     "module": "NodeNext",
 7 |     "moduleResolution": "NodeNext",
 8 |     "jsx": "preserve",
 9 |     "resolveJsonModule": true
10 |   },
11 |   "include": ["src"]
12 | }
13 | 
--------------------------------------------------------------------------------
/frontend/vite-plugin-generate-locales.js:
--------------------------------------------------------------------------------
 1 | import fs from "fs";
 2 | import path from "path";
 3 | import * as url from "url";
 4 | 
 5 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
 6 | 
 7 | export default function generateLocalesPlugin() {
 8 |   return {
 9 |     name: "generate-locales",
10 |     buildStart() {
11 |       const localesDir = path.resolve(__dirname, "public", "locales");
12 | 
13 |       if (fs.existsSync(localesDir)) {
14 |         const localesList = fs
15 |           .readdirSync(localesDir)
16 |           .filter((file) => {
17 |             return fs.statSync(path.join(localesDir, file)).isDirectory();
18 |           })
19 |           .map((locale) => {
20 |             const commonFilePath = path.join(localesDir, locale, "common.json");
21 |             if (fs.existsSync(commonFilePath)) {
22 |               const commonFile = JSON.parse(
23 |                 fs.readFileSync(commonFilePath, "utf-8")
24 |               );
25 |               return {
26 |                 code: locale,
27 |                 name: commonFile.languageName || locale,
28 |               };
29 |             }
30 |             return {
31 |               code: locale,
32 |               name: locale,
33 |             };
34 |           });
35 | 
36 |         // Save the array to a JSON file
37 |         const outputPath = path.resolve(
38 |           __dirname,
39 |           "src",
40 |           "generated",
41 |           "localesList.json"
42 |         );
43 |         fs.writeFileSync(outputPath, JSON.stringify(localesList, null, 2));
44 | 
45 |         console.log(`Locales list saved to ${outputPath}`);
46 |       } else {
47 |         console.error("Locales directory not found.");
48 |       }
49 |     },
50 |   };
51 | }
52 | 
--------------------------------------------------------------------------------
/frontend/vite.config.mjs:
--------------------------------------------------------------------------------
 1 | import process from "node:process";
 2 | import { defineConfig, searchForWorkspaceRoot } from "vite";
 3 | import react from "@vitejs/plugin-react";
 4 | import { viteStaticCopy } from "vite-plugin-static-copy";
 5 | import generateLocalesPlugin from "./vite-plugin-generate-locales.js";
 6 | 
 7 | export default defineConfig({
 8 |   base: "/app",
 9 |   server: {
10 |     port: 3000,
11 |     strictPort: true,
12 |     proxy: {
13 |       "/auth": "http://127.0.0.1:4000",
14 |       "/api": "http://127.0.0.1:4000",
15 |       "/controller": "http://127.0.0.1:4000",
16 |     },
17 |     fs: {
18 |       allow: [searchForWorkspaceRoot(process.cwd()), "../node_modules"],
19 |     },
20 |   },
21 |   resolve: {
22 |     alias: {
23 |       components: "/src/components",
24 |       utils: "/src/utils",
25 |       external: "/src/external",
26 |       generated: "/src/generated",
27 |     },
28 |   },
29 |   build: {
30 |     outDir: "build",
31 |     chunkSizeWarningLimit: 1000,
32 |   },
33 |   plugins: [
34 |     react(),
35 |     generateLocalesPlugin(),
36 |     viteStaticCopy({
37 |       targets: [
38 |         {
39 |           src: "public/locales",
40 |           dest: "",
41 |         },
42 |       ],
43 |     }),
44 |   ],
45 | });
46 | 
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "zero-ui",
 3 |   "version": "1.5.8",
 4 |   "workspaces": [
 5 |     "backend",
 6 |     "frontend"
 7 |   ],
 8 |   "scripts": {
 9 |     "postinstall": "husky install",
10 |     "upgrade:deps": "yarn upgrade-interactive",
11 |     "upgrade:yarn": "yarn set version latest",
12 |     "clean:deps": "cd frontend && rimraf node_modules && cd ../backend && rimraf node_modules && cd .. && rimraf node_modules",
13 |     "format": "yarn prettier --check .",
14 |     "format:fix": "yarn prettier --write .",
15 |     "lint": "yarn workspaces foreach --all --parallel run lint",
16 |     "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\"",
17 |     "build": "cd frontend && cross-env GENERATE_SOURCEMAP=false yarn build",
18 |     "prod": "cd backend && cross-env NODE_ENV=production ZU_SECURE_HEADERS=false yarn start",
19 |     "docker:build": "docker build . -t dec0dos/zero-ui -f docker/zero-ui/Dockerfile --progress=plain",
20 |     "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",
21 |     "release": "standard-version && git push --follow-tags origin main && git add CHANGELOG.md",
22 |     "commit": "yarn git-cz"
23 |   },
24 |   "devDependencies": {
25 |     "@commitlint/cli": "^17.7.2",
26 |     "@commitlint/config-conventional": "^17.7.0",
27 |     "commitizen": "^4.3.0",
28 |     "concurrently": "^8.2.1",
29 |     "cross-env": "^7.0.3",
30 |     "cz-conventional-changelog": "^3.3.0",
31 |     "husky": "^8.0.3",
32 |     "lint-staged": "^14.0.1",
33 |     "nodemon": "^3.0.1",
34 |     "prettier": "^3.0.3",
35 |     "rimraf": "^5.0.5",
36 |     "standard-version": "^9.5.0"
37 |   },
38 |   "prettier": {
39 |     "trailingComma": "es5",
40 |     "tabWidth": 2,
41 |     "semi": true,
42 |     "singleQuote": false
43 |   },
44 |   "commitlint": {
45 |     "extends": [
46 |       "@commitlint/config-conventional"
47 |     ]
48 |   },
49 |   "config": {
50 |     "commitizen": {
51 |       "path": "cz-conventional-changelog"
52 |     }
53 |   },
54 |   "lint-staged": {
55 |     "*.{tsx,ts,js,jsx,scss,css,js,json,md}": [
56 |       "yarn prettier --write"
57 |     ]
58 |   },
59 |   "packageManager": "yarn@4.3.1"
60 | }
61 | 
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "allowJs": true,
 4 |     "checkJs": true,
 5 |     "noEmit": true,
 6 |     "skipLibCheck": true,
 7 |     "target": "ESNext",
 8 |     "lib": ["ESNext", "dom"],
 9 |     "strict": true,
10 |     "noImplicitAny": false,
11 |     "allowSyntheticDefaultImports": true
12 |   }
13 | }
14 | 
--------------------------------------------------------------------------------