Compare commits

...

198 Commits

Author SHA1 Message Date
Donald Zou fbb82fa759 Update README.md 2024-08-17 01:00:40 -04:00
Donald Zou a9f977c3b4 Merge pull request #303 from donaldzou/v4
Merging the v4 branch to main.
2024-08-17 00:56:29 -04:00
Donald Zou 5669e22548 Removed all deprecated JS files 2024-08-17 00:52:38 -04:00
Donald Zou da4bb9b83d Merge branch 'main' into v4 2024-08-17 00:46:46 -04:00
Donald Zou c2cbaf0937 I think this is it :) 2024-08-17 00:31:46 -04:00
Donald Zou d81dce536c Update README.md 2024-08-16 23:32:16 -04:00
Donald Zou 8e1e2c5b85 Update README.md 2024-08-16 23:24:20 -04:00
Donald Zou 959d5e6822 Update README.md 2024-08-16 23:22:55 -04:00
Donald Zou 036a0b4cb3 Update README.md 2024-08-16 23:18:28 -04:00
Donald Zou c038f14374 Update README.md 2024-08-16 23:03:01 -04:00
Donald Zou 343cf91643 Update README.md 2024-08-16 23:02:36 -04:00
Donald Zou 4fe70a4c46 Update README.md 2024-08-16 23:01:58 -04:00
Donald Zou 59f7200512 Update README.md 2024-08-16 22:53:23 -04:00
Donald Zou 0335e709c2 Update README.md 2024-08-16 22:51:56 -04:00
Donald Zou a7bb5ac21b Update wgd.sh 2024-08-16 17:36:22 -04:00
Donald Zou 99655cab33 Update wgd.sh 2024-08-16 17:35:38 -04:00
Donald Zou 348e574154 Update wgd.sh 2024-08-16 17:34:54 -04:00
Donald Zou f35dee8643 Update wgd.sh 2024-08-16 17:34:03 -04:00
Donald Zou 168412c2e7 Update wgd.sh 2024-08-16 17:32:34 -04:00
Donald Zou b9832542fb Update wgd.sh 2024-08-16 17:24:50 -04:00
Donald Zou 316545b253 Update wgd.sh 2024-08-16 17:23:16 -04:00
Donald Zou 348a57a0d8 Update wgd.sh 2024-08-16 17:22:57 -04:00
Donald Zou a3f5654816 Update wgd.sh 2024-08-16 17:21:42 -04:00
Donald Zou 8207afe806 Update wgd.sh 2024-08-16 17:20:05 -04:00
Donald Zou e15b8f8092 Update wgd.sh 2024-08-16 17:19:07 -04:00
Donald Zou e6c99028d6 Update wgd.sh 2024-08-16 17:16:54 -04:00
Donald Zou 2e691d7c26 Update wgd.sh 2024-08-16 17:13:18 -04:00
Donald Zou 6c87812780 Update wgd.sh 2024-08-16 17:12:40 -04:00
Donald Zou 798a3632cf Update wgd.sh 2024-08-16 03:32:28 -04:00
Donald Zou aa1d0aabd2 Update wgd.sh 2024-08-16 03:28:26 -04:00
Donald Zou f348e691fa Update wgd.sh 2024-08-16 03:27:24 -04:00
Donald Zou 0dfda83e45 Update wgd.sh 2024-08-16 03:25:25 -04:00
Donald Zou 56044fa3f7 Update wgd.sh 2024-08-16 03:18:08 -04:00
Donald Zou 4c69fd4f60 Update wgd.sh 2024-08-16 03:14:03 -04:00
Donald Zou 92e3f7a6a3 Update wgd.sh 2024-08-16 03:12:29 -04:00
Donald Zou 76d0618d6e Update wgd.sh 2024-08-16 03:11:30 -04:00
Donald Zou 80c3a99eb1 Update wgd.sh 2024-08-16 03:10:26 -04:00
Donald Zou 91e3a3237b Update wgd.sh 2024-08-16 03:06:31 -04:00
Donald Zou 8c8c5a5826 Update wgd.sh 2024-08-16 03:02:45 -04:00
Donald Zou b9370955d6 Update wgd.sh 2024-08-16 02:53:45 -04:00
Donald Zou 2761e728f7 Update wgd.sh 2024-08-16 02:50:54 -04:00
Donald Zou f418bf4f63 Update wgd.sh 2024-08-16 02:48:31 -04:00
Donald Zou 3d046e4369 Update wgd.sh 2024-08-16 02:40:25 -04:00
Donald Zou b6eb6f2d70 Update wgd.sh 2024-08-16 02:19:30 -04:00
Donald Zou 45ce3e26c1 Update wgd.sh 2024-08-16 02:15:10 -04:00
Donald Zou ae91cedf76 Update wgd.sh 2024-08-16 02:07:40 -04:00
Donald Zou b2a6b484b5 Update wgd.sh 2024-08-16 02:03:41 -04:00
Donald Zou 95c4fa56cc Update wgd.sh 2024-08-16 02:01:02 -04:00
Donald Zou e8c56afa8e Update wgd.sh 2024-08-16 02:00:45 -04:00
Donald Zou ddc3b1f6c2 Update wgd.sh 2024-08-16 01:58:40 -04:00
Donald Zou d33d886574 Update wgd.sh 2024-08-16 01:55:53 -04:00
Donald Zou 0487980d2f Update wgd.sh
Selectively use a specific version of Python: 3.10, 3.11 or 3.12
2024-08-16 01:47:44 -04:00
Donald Zou e68257d6c9 Updated some documentations 2024-08-15 23:20:29 -04:00
Donald Zou ea0bbab680 Update api-documents.md 2024-08-15 22:50:01 -04:00
Donald Zou 5d4a8136c5 Updated some documentation 2024-08-15 22:47:44 -04:00
Donald Zou c98d851cd2 No longer reading configuration file every few seconds
Only when the file is changed
2024-08-15 18:26:20 -04:00
Donald Zou 42fa89db7a Added to parse name in .conf file 2024-08-15 17:45:54 -04:00
Donald Zou 1a70acc6f2 Update documentation 2024-08-15 16:55:34 -04:00
Donald Zou ee0a287112 Update README.md 2024-08-15 00:56:28 -04:00
Donald Zou cf16a66c63 Update README.md 2024-08-15 00:53:38 -04:00
Donald Zou 4544b17c94 Update README.md 2024-08-15 00:53:23 -04:00
Donald Zou 8119c9da51 Update README.md 2024-08-15 00:49:21 -04:00
Donald Zou baf4d75c01 Update documentations 2024-08-15 00:48:27 -04:00
Donald Zou ac9eff0fcc Removed images 2024-08-14 23:59:50 -04:00
Donald Zou dc57bd92ff Update README.md 2024-08-14 23:57:45 -04:00
Donald Zou 3c56a2c868 Update README.md 2024-08-14 23:55:52 -04:00
Donald Zou 2d2ab10a24 Update README.md 2024-08-14 23:54:54 -04:00
Donald Zou ac906c9994 Update README.md 2024-08-14 23:53:28 -04:00
Donald Zou 480406d579 Update README.md 2024-08-14 23:52:14 -04:00
Donald Zou 47efb644b7 Working on some updates 2024-08-14 22:45:36 -04:00
Donald Zou fd0e519e41 Fixed some API endpoint missed the prefix 2024-08-14 18:40:28 -04:00
Donald Zou 0c8bb990d0 Some UI adjustment 2024-08-14 14:35:49 -04:00
Donald Zou bd7139827b Update 2024-08-14 12:31:37 -04:00
Donald Zou 4f648aff52 Updated UI for smaller screen 2024-08-14 12:07:53 -04:00
Donald Zou 3feb45dc01 Fixed for URL protocol 2024-08-14 10:59:01 -04:00
Donald Zou 0489dc7c33 Update fetch.js
Fixed for running over HTTPS
2024-08-14 10:57:41 -04:00
Donald Zou a30843cff9 Added app prefix for URL 2024-08-14 01:17:47 -04:00
Donald Zou 3a34a0eb40 Adjusted some UI 2024-08-13 12:29:58 -04:00
Donald Zou e3f82e136a Adjusted some UI 2024-08-12 18:04:41 -04:00
Donald Zou 8a7df4ba9f Update wgd.sh 2024-08-12 01:19:55 -04:00
Donald Zou e86d1a4c7a Updated langugage 2024-08-12 00:34:54 -04:00
Donald Zou 5b9d0b60a1 Adjusted some UI 2024-08-11 19:20:52 -04:00
Donald Zou 7eff2f0c49 Fixed issue #250 2024-08-11 19:20:42 -04:00
Donald Zou 97236bb01d Fixed new configuration hang when error 2024-08-11 19:20:03 -04:00
Donald Zou 9c6aa12f48 Update README.md 2024-08-11 16:53:50 -04:00
Donald Zou 96ccb03eea Adjusted some code for electron version 2024-08-11 16:39:00 -04:00
Donald Zou 55f55820c5 Update wg-dashboard.service 2024-08-11 11:02:08 -04:00
Donald Zou 955839d513 I think cross server actually worked 2024-08-11 01:48:13 -04:00
Donald Zou a650e628e5 CORS SUCCESS!!! 2024-08-10 19:23:50 -04:00
Donald Zou 54142b73fb Ohhhhh kay testing CORS :) 2024-08-10 19:03:21 -04:00
Donald Zou 55e0d2695d Update .gitignore 2024-08-10 12:58:41 -04:00
Donald Zou 2f90ab15dc Let's try ElectronJS 2024-08-10 12:58:14 -04:00
Donald Zou fd3fc66bfc Thinking of adding Electron.js 2024-08-10 00:25:25 -04:00
Donald Zou a352a94d8a Update .gitignore 2024-08-10 00:24:07 -04:00
Donald Zou 410b81f46f Some UI adjustment 2024-08-09 22:16:38 -04:00
Donald Zou aa3711c5cc Adjusted search peer UI 2024-08-09 21:30:01 -04:00
Donald Zou d6b1f97a04 Update 2024-08-09 20:46:11 -04:00
Donald Zou b4e8e57a22 Updated UI for nav bar 2024-08-09 17:58:41 -04:00
Donald Zou 9644e6195c Finished adding data usage on configuration list 2024-08-09 17:30:44 -04:00
Donald Zou 764e0c7607 Replaced all cursor without recursive use 2024-08-09 17:29:57 -04:00
Donald Zou 97d640dd40 Update dashboard.py 2024-08-09 16:09:55 -04:00
Donald Zou d2915b5b05 Update dashboard.py 2024-08-09 16:09:23 -04:00
Donald Zou f274f6fd18 Fixed some bugs.. 2024-08-09 16:09:08 -04:00
Donald Zou f507ac2569 Rewrote the add peer function 2024-08-08 23:27:13 -04:00
Donald Zou 208cbd6d89 Peer data reset function is don e 2024-08-08 20:56:14 -04:00
Donald Zou cd2aa2902a Update README.md 2024-08-07 00:44:46 -04:00
Donald Zou fa2d7fa3da Peer Sharing is done :) 2024-08-07 00:37:05 -04:00
Donald Zou 7463767781 Sharing peer is done, fixed #294
Fixed issue #294 with a Vue.js plugin instead of using the `datatime-local` input tag.

Still need to work on the end-user UI for sharing.
2024-08-06 19:15:00 -04:00
Donald Zou 958bc864c9 The sharing function is almost done 2024-08-06 10:17:14 -04:00
Donald Zou 4484668750 Backend for peer sharing is done 2024-08-05 19:57:17 -04:00
Donald Zou d5dea4b87f Fixed the issue mentioned in #290 2024-08-05 17:55:58 -04:00
Donald Zou 0fdef6a0a2 fixing some Gunicorn bugs.. again.. 2024-08-05 15:39:11 -04:00
Donald Zou bd71b6bad8 Update gunicorn.conf.py 2024-08-04 20:24:31 -04:00
Donald Zou 9b7887b279 Update gunicorn.conf.py 2024-08-04 20:23:41 -04:00
Donald Zou 3960e43872 Update gunicorn.conf.py 2024-08-04 20:22:41 -04:00
Donald Zou 201c8f9ec9 Update dashboard.py 2024-08-04 20:17:29 -04:00
Donald Zou 8c8374a08c Update wgd.sh 2024-08-04 20:16:26 -04:00
Donald Zou 467595afc9 Update wgd.sh 2024-08-04 20:15:46 -04:00
Donald Zou acb54f098c Update gunicorn.conf.py 2024-08-04 20:13:17 -04:00
Donald Zou 5755d13460 Update gunicorn.conf.py 2024-08-04 19:45:28 -04:00
Donald Zou 2c3500315d Update gunicorn.conf.py 2024-08-04 19:36:49 -04:00
Donald Zou 47ea60c0cd Testing v4 2024-08-04 19:35:59 -04:00
Donald Zou 18b18c1396 Update wgd.sh 2024-08-04 19:33:05 -04:00
Donald Zou ff227de5fa Update gunicorn.conf.py 2024-08-04 19:32:16 -04:00
Donald Zou 6799692811 Testing.. 2024-08-04 19:30:48 -04:00
Donald Zou c6173f7f6f Update wgd.sh 2024-08-04 19:22:26 -04:00
Donald Zou d0e4dabc44 Update wgd.sh 2024-08-04 19:22:06 -04:00
Donald Zou f815dae300 Update wgd.sh 2024-08-04 19:21:38 -04:00
Donald Zou b3bd6bb39e Update wgd.sh 2024-08-04 19:20:31 -04:00
Donald Zou 71df6409c2 Update gunicorn.conf.py 2024-08-04 19:17:41 -04:00
Donald Zou e4f9a1e0cf Update gunicorn.conf.py 2024-08-04 19:03:30 -04:00
Donald Zou ca6a05e393 Update gunicorn.conf.py 2024-08-04 18:59:45 -04:00
Donald Zou c0d26164dc Update wgd.sh 2024-08-04 18:48:13 -04:00
Donald Zou 76fe2a1ba9 Update wgd.sh 2024-08-04 18:45:57 -04:00
Donald Zou 8cbdb54402 Still fixing Gunicron 2024-08-04 16:08:05 -04:00
Donald Zou 764ef80a62 Update wgd.sh 2024-08-04 15:44:55 -04:00
Donald Zou 0c37d93c01 Testing if this will fix bash exit before Gunicorn ran... 2024-08-04 15:29:07 -04:00
Donald Zou c57a5128e5 Update wgd.sh 2024-08-04 03:20:44 -04:00
Donald Zou 6cf4eba20a Redirect when received a 401 error 2024-08-04 01:31:31 -04:00
Donald Zou 6825d728c2 Log function is completed 2024-08-03 17:03:39 -04:00
Donald Zou 6d3091b2a2 Push dockerfiles 2024-08-03 15:29:56 -04:00
Donald Zou 1740ab0bbe Update README.md 2024-07-31 02:23:20 -04:00
Donald Zou 9d2b5593a2 Add files via upload 2024-07-31 02:21:42 -04:00
Donald Zou 1a9bdc5e6d Merge pull request #288 from donaldzou/donaldzou-patch-3
Update README.md
2024-07-29 18:58:49 -04:00
Donald Zou 1f565bca10 Update README.md 2024-07-29 18:58:39 -04:00
Donald Zou da089197a3 Changed the name 2024-07-29 18:56:56 -04:00
Donald Zou 2df8f41d6c Changed v4.0 preview GIF 2024-07-29 18:56:35 -04:00
Donald Zou 4e4f0d4c97 Create S1creen Recording 2024-07-29 at 5.59.53 PM.webm 2024-07-29 18:40:46 -04:00
Donald Zou eaad22c0a1 Create proxy.js 2024-07-29 18:40:19 -04:00
Donald Zou ceb8cdd337 Merge pull request #281 from donaldzou/donaldzou-patch-3
Update README.md
2024-06-16 15:42:28 +08:00
Donald Zou 570c754eec Update README.md 2024-06-16 15:42:17 +08:00
Donald Zou aedcfd1d24 Merge pull request #277 from DaanSelen/main
Added updated Docker functionality.
2024-06-11 22:40:38 +08:00
daanselen 44e738acf5 Merge branch 'main' of https://github.com/DaanSelen/WGDashboard-dockerized 2024-06-10 20:36:13 +02:00
daanselen 701b45c286 Improved (imo) the use of English. 2024-06-10 20:34:30 +02:00
dselen 72fe687d82 Update Docker-explain.md 2024-06-09 23:13:31 +02:00
dselen 632cd66b57 Update Docker-explain.md 2024-06-09 23:13:19 +02:00
daanselen 20530c000e Added container documentation, and added a few new comments. 2024-06-09 23:11:22 +02:00
Daan 8824786fb4 For some reason, cloning the GitHub (which has been simplified) works from the Dockerfile directly.
But the wg0.conf file needs a re-copy on run-time. Working now.

Made code modifications and optimizations.

Please report bugs to me.
2024-06-08 13:30:24 +02:00
Daan bdeb4a4efe Cleaned up lingering code line. 2024-06-07 14:55:16 +02:00
Daan cf455fc19b Modified code so that the docker logging makes sense. Also fixed a bug which can happen with iptables (-s illegal option). Pushing to repo. 2024-06-07 13:25:46 +02:00
Daan 1bcddadb7a Features tested working, feeling good about a release. 2024-06-06 16:11:49 +02:00
Daan 6dc28f11e0 Working on isolated peers feature. 2024-06-06 00:12:11 +02:00
Daan 8ad601fcc0 Added features, plus explanations in about them in the compose.yaml file. Looking great! 2024-06-05 20:27:24 +02:00
Daan f400844a3d Modified scripts 2024-06-05 09:16:29 +02:00
Daan 560096878f Persistency seems to work with this model, it has succeeded my tests. 2024-06-04 23:26:03 +02:00
Daan 0938f5ab71 Container deployed with working VPN server built-in. Missing is persistency among recreations, looking into that... 2024-06-04 22:49:17 +02:00
Daan 06193d27c0 Added dockerfile and entrypoint for basic compilation, moving to finalizing 2024-06-04 17:05:17 +02:00
Donald Zou 60e64a3646 Merge pull request #269 from donaldzou/donaldzou-patch-2
Update README.md
2024-05-14 22:44:20 +08:00
Donald Zou d60f89976e Update README.md 2024-05-14 22:44:09 +08:00
Donald Zou b6e62b08e4 Merge pull request #265 from donaldzou/v3.0.6.2
Update dashboard.py
2024-05-04 11:42:10 +08:00
Donald Zou effe5b32fd Update dashboard.py 2024-05-04 11:40:32 +08:00
Donald Zou 9e38137e76 Removed DNS as a required field from peer settings 2024-05-03 10:15:00 +08:00
Donald Zou 210f5eabc9 Merge pull request #261 from donaldzou/donaldzou-patch-1
Update FUNDING.yml
2024-03-31 11:20:04 -04:00
Donald Zou 5f5c91a8ff Update FUNDING.yml 2024-03-31 11:19:54 -04:00
Donald Zou 05c7121c8a Update README.md 2024-03-31 00:55:46 -04:00
Donald Zou 551a8e9588 Add files via upload 2024-03-31 00:53:00 -04:00
Donald Zou 8fe5507ff8 Update README.md 2024-02-27 18:38:10 -05:00
Donald Zou 4aed647865 Merge pull request #254 from Upellift99/main
Enabling use of underscores in configuration name
2024-02-01 18:39:05 -05:00
John 74ab7aaa3d Enabling use of underscores in configuration name. 2024-02-01 09:53:04 +01:00
John dcf2055851 Enabling use of underscores in configuration name. 2024-02-01 09:47:52 +01:00
Donald Zou 734a4b5e00 Merge pull request #252 from donaldzou/donaldzou-patch-1
Update README.md
2024-01-23 15:11:21 -05:00
Donald Zou da70fac0b6 Update README.md 2024-01-23 15:11:02 -05:00
Donald Zou 3d6ecba4f5 Update README.md 2024-01-09 14:10:08 -05:00
Donald Zou 86220694ce Update README.md
Added instruction to install `net-tools`. It is required for the Python package `ifcfg`. Seems like it is not pre-installed on a fresh debian based system.
2023-12-10 13:01:45 -05:00
Donald Zou 77f31177c8 Update .gitignore 2023-12-10 12:56:49 -05:00
Donald Zou 33ff91aea8 Update .gitignore 2023-12-10 12:56:02 -05:00
Donald Zou 2c9cade70a Create SECURITY.md 2023-03-23 13:07:36 -04:00
Donald Zou d7d7a84bd5 Update README.md 2022-04-10 09:35:50 -04:00
Donald Zou 990463fbea Update README.md 2022-04-10 09:29:07 -04:00
Donald Zou 0d9a8ba6f7 Update README.md 2022-04-06 21:05:24 -04:00
Donald Zou edbaf3ac82 Update README.md 2022-03-29 12:27:39 -04:00
Donald Zou cd84674ae0 Update README.md 2022-03-28 08:23:12 -04:00
Donald Zou f60ea43f29 Create codeql-analysis.yml 2022-03-27 21:44:49 -04:00
Donald Zou 76f547a726 Update README.md 2022-03-26 11:14:27 -04:00
Donald Zou dffe5e0819 Update README.md 2022-03-25 14:11:39 -04:00
Donald Zou fb9ef0c547 Update README.md 2022-03-22 17:34:34 -04:00
Donald Cheng Hong Zou d52cd2b17c Doing a update 2022-03-22 17:28:19 -04:00
Donald Cheng Hong Zou cefa80f317 Fixed redirect functionality 2022-03-22 16:17:12 -04:00
89 changed files with 5917 additions and 3813 deletions
+1
View File
@@ -1,3 +1,4 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [donaldzou] github: [donaldzou]
patreon: DonaldDonnyZou
+70
View File
@@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '30 5 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
-1
View File
@@ -48,5 +48,4 @@ coverage
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo
proxy.js
.vite/* .vite/*
-28
View File
@@ -1,28 +0,0 @@
FROM ubuntu:20.04
ARG WG_ADDRESS=$WG_ADDRESS
RUN apt-get update && \
apt-get install -y --no-install-recommends iproute2 wireguard-tools iptables nano net-tools python3 python3-pip python3-venv procps openresolv inotify-tools && \
apt-get clean
RUN mkdir -p /etc/wireguard/
RUN mkdir -p /opt/wgdashboard
RUN mkdir -p /opt/wgdashboard_tmp
# configure wireguard
RUN wg genkey | tee /opt/wgdashboard_tmp/privatekey | wg pubkey | tee /opt/wgdashboard_tmp/publickey
RUN cd / && echo "[Interface]" > wg0.conf && echo "SaveConfig = true" >> wg0.conf && echo -n "PrivateKey = " >> wg0.conf && cat /opt/wgdashboard_tmp/privatekey >> wg0.conf \
&& echo "ListenPort = 51820" >> wg0.conf && echo "Address = ${WG_ADDRESS}" >> wg0.conf && chmod 700 wg0.conf
COPY ./src /opt/wgdashboard_tmp
RUN pip3 install -r /opt/wgdashboard_tmp/requirements.txt --no-cache-dir
RUN rm -rf /opt/wgdashboard_tmp
COPY ./entrypoint.sh /entrypoint.sh
RUN chmod u+x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
WORKDIR /opt/wgdashboard
EXPOSE 10086
EXPOSE 51820/udp
+280 -278
View File
@@ -1,28 +1,36 @@
<p align="center"> <p align="center">
<img alt="WGDashboard" src="img/logo.png" width="128"> <img alt="WGDashboard" src="./src/static/img/logo.png" width="128">
</p> </p>
<h1 align="center">WGDashboard</h1> <h1 align="center">WGDashboard</h1>
<p align="center"> <p align="center">
<img src="http://ForTheBadge.com/images/badges/made-with-python.svg"> <img src="https://forthebadge.com/images/badges/made-with-python.svg">
<img src="https://forthebadge.com/images/badges/made-with-javascript.svg">
<img src="https://forthebadge.com/images/badges/license-mit.svg">
</p>
<p align="center">
<img src="https://forthebadge.com/images/badges/built-with-love.svg">
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/donaldzou/wireguard-dashboard/releases/latest"><img src="https://img.shields.io/github/v/release/donaldzou/wireguard-dashboard"></a> <a href="https://github.com/donaldzou/wireguard-dashboard/releases/latest"><img src="https://img.shields.io/github/v/release/donaldzou/wireguard-dashboard"></a>
<a href="https://wakatime.com/badge/user/45f53c7c-9da9-4cb0-85d6-17bd38cc748b/project/5334ae20-e9a6-4c55-9fea-52d4eb9dfba6"><img src="https://wakatime.com/badge/user/45f53c7c-9da9-4cb0-85d6-17bd38cc748b/project/5334ae20-e9a6-4c55-9fea-52d4eb9dfba6.svg" alt="wakatime"></a> <a href="https://wakatime.com/badge/github/donaldzou/WGDashboard"><img src="https://wakatime.com/badge/github/donaldzou/WGDashboard.svg" alt="wakatime"></a>
<a href="https://hits.seeyoufarm.com"><img src="https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fdonaldzou%2FWGDashboard&count_bg=%2379C83D&title_bg=%23555555&icon=github.svg&icon_color=%23E7E7E7&title=Visitor&edge_flat=false"/></a> <a href="https://hits.seeyoufarm.com"><img src="https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fdonaldzou%2FWGDashboard&count_bg=%2379C83D&title_bg=%23555555&icon=github.svg&icon_color=%23E7E7E7&title=Visitor&edge_flat=false"/></a>
</p> </p>
<p align="center">Monitoring WireGuard is not convenient, need to login into server and type <code>wg show</code>. That's why this project is being created, to view all configurations and manage them in a easy way.</p> <p align="center">Monitoring WireGuard is not convenient, need to remote access to server and type <code>wg show</code>. That's why this project is being created, to view all configurations and manage them in a easy way.</p>
<p align="center"><small><i>This project is not affiliate to the official WireGuard Project</i></small></p> <p align="center">With all these awesome features, while keeping it <b>simple</b>, <b>easy to install and use</b></p>
<p align="center"><b><i>This project is not affiliate to the official WireGuard Project</i></b></p>
## 📣 What's New: v4.0 ## 📣 What's New: v4.0
> I can't thank enough for all of you who wait for this release, and for those who are new to this project, welcome :) Also, huge thanks who sponsored me GitHub :heart: ### 🎉 New Features
- 🎉 **New Features**
- **Updated dashboard design**: Re-designed some of the section with more modern style and layout, the UI is faster and more responsive, it also uses less memory. But overall is still the same dashboard you're familiarized. - **Updated dashboard design**: Re-designed some of the section with more modern style and layout, the UI is faster and more responsive, it also uses less memory. But overall is still the same dashboard you're familiarized.
- **Docker Solution**: We now have 2 docker solutions! Thanks to @DaanSelen & @shuricksumy for providing them. For more information, please see the [Docker](#-docker-solutions) section below.
- **Peer Job Scheduler**: Now you can schedule jobs for each peer to either **restrict** or **delete** the peer if the peer's total / upload / download data usage exceeded a limit, or you can set a specific datetime to restrict or delete the peer. - **Peer Job Scheduler**: Now you can schedule jobs for each peer to either **restrict** or **delete** the peer if the peer's total / upload / download data usage exceeded a limit, or you can set a specific datetime to restrict or delete the peer.
- **API Key for WGDashboard's REST API**: You can now request all the api endpoint used in the dashboard. For more details please review the API Documentation below. - **Share Peer's QR Code with Public Link**: You can share a peer's QR code and `.conf` file without the need to loging in.
- **WGDashboard's REST API**: You can now request all the api endpoint used in the dashboard. For more details please review the [API Documentation](./docs/api-documents.md).
- **Logging**: Dashboard will now log all activity on the dashboard and API requests. - **Logging**: Dashboard will now log all activity on the dashboard and API requests.
- **Time-Based One-Time Password (TOTP)**: You can enable this function to add one more layer of security, and generate the TOTP with your choice of authenticator. - **Time-Based One-Time Password (TOTP)**: You can enable this function to add one more layer of security, and generate the TOTP with your choice of authenticator.
- **Designs** - **Designs**
@@ -30,36 +38,65 @@
- **Night mode**: You know what that means, it avoids bugs ;) - **Night mode**: You know what that means, it avoids bugs ;)
- **Enforce Python Virtual Environment**: I noticed newer Python version (3.12) does not allow to install packages globally, and plus I think is a good idea to use venv. - **Enforce Python Virtual Environment**: I noticed newer Python version (3.12) does not allow to install packages globally, and plus I think is a good idea to use venv.
- **🧐 Other Changes** ### 🧐 Other Changes
- **Deprecated jQuery from the project, and migrated and rewrote the whole front-end with Vue.js. This allow the dashboard is future proofed, and potential cross server access with a desktop app.** - **Deprecated jQuery from the project, and migrated and rewrote the whole front-end with Vue.js. This allows the dashboard is future proofed, and potential cross server access with a desktop app.**
- Rewrote the backend into a REST API structure - Rewrote the backend into a REST API structure
- Improved SQL query efficient - Improved SQL query efficient
- Removed all templates, except for `index.html` where it will load the Vue.js app. - Removed all templates, except for `index.html` where it will load the Vue.js app.
- Parsing names in `.conf`
- Minimized the need to read `.conf`, only when any `.conf` is modified
### 🥘 New Experimental Features
- **Cross-Server Access**: Now you can access other servers that installed `v4` of WGDashboard through API key.
- **Desktop App**: Thanks to **Cross-Server Access**, you can now download an ElectronJS based desktop app of WGDashboard, and use that to access WGDashboard on different servers.
- > For more information, please scroll down to [🥘 Experimental Functions](#-experimental-functions)
> I can't thank enough for all of you who wait for this release, and for those who are new to this project, welcome :)
**For users who is using `v2.x.x` please be sure to read [this](#please-note-for-user-who-is-using-v231-or-below) before updating WGDashboard** > Also, huge thanks to who contributed to this major release:
> @bolgovrussia, @eduardorosabales, @Profik, @airgapper, @tokon2000, @bkeenke, @kontorskiy777, @bugsse, @Johnnykson, @DaanSelen, @shuricksumy and many others!
<hr> <hr>
## Table of Content ## 📋 Table of Content
<!-- TOC -->
- [💡 Features](#-features) * [📣 What's New: v4.0](#-whats-new-v40)
- [📝 Requirement](#-requirement) * [🎉 New Features](#-new-features)
- [🛠 Install](#-install) * [🧐 Other Changes](#-other-changes)
- [🪜 Usage](#-usage) * [🥘 New Experimental Features](#-new-experimental-features)
- [Start/Stop/Restart WGDashboard](#startstoprestart-wgdashboard) * [📋 Table of Content](#-table-of-content)
- [Autostart WGDashboard on boot](#autostart-wgdashboard-on-boot--v22) * [💡 Features](#-features)
- [✂️ Dashboard Configuration](#%EF%B8%8F-dashboard-configuration) * [📝 Requirements](#-requirements)
- [Dashboard Configuration file](#dashboard-configuration-file) * [Supported Operating Systems](#supported-operating-systems)
- [Generating QR code and peer configuration file (.conf)](#generating-qr-code-and-peer-configuration-file-conf) * [Existing WireGuard Configurations](#existing-wireguard-configurations)
- [❓ How to update the dashboard?](#-how-to-update-the-dashboard) * [🛠 Install](#-install)
- [🥘 Experimental Functions](#-experimental-functions) * [Install Commands](#install-commands)
- [🔍 Screenshot](#-screenshot) * [Ubuntu 20.04 LTS](#ubuntu-2004-lts)
- [⏰ Changelog](#--changelog) * [Ubuntu 22.04 LTS & Ubuntu 24.02 LTS](#ubuntu-2204-lts--ubuntu-2402-lts)
- [🛒 Dependencies](#-dependencies) * [Debian 12.6](#debian-126)
- [✨ Contributors](#-contributors) * [Debian 11.10](#debian-1110)
* [Red Hat Enterprise Linux 9.4 & CentOS 9-Stream](#red-hat-enterprise-linux-94--centos-9-stream)
* [Fedora 40 & Fedora 39 & Fedora 38](#fedora-40--fedora-39--fedora-38)
* [Manual Installation](#manual-installation)
* [🪜 Usage](#-usage)
* [Start/Stop/Restart WGDashboard](#startstoprestart-wgdashboard)
* [Autostart WGDashboard on boot (>= v2.2)](#autostart-wgdashboard-on-boot--v22)
* [✂️ Dashboard Configuration](#-dashboard-configuration)
* [Dashboard Configuration file](#dashboard-configuration-file)
* [Generating QR code and peer configuration file (.conf)](#generating-qr-code-and-peer-configuration-file-conf)
* [❓ How to update the dashboard?](#-how-to-update-the-dashboard)
* [**Please note for users who are using `v3 - v3.0.6` want to update to `v4.0`**](#please-note-for-users-who-are-using-v3---v306-want-to-update-to-v40)
* [**Please note for users who are using `v2.3.1` or below**](#please-note-for-users-who-are-using-v231-or-below)
* [🐬 Docker Solutions](#-docker-solutions)
* [Solution 1 from @DaanSelen](#solution-1-from-daanselen)
* [Solution 2 from @shuricksumy](#solution-2-from-shuricksumy)
* [📖 WGDashboard REST API Documentation & How to use API Key](#-wgdashboard-rest-api-documentation--how-to-use-api-key)
* [🥘 Experimental Features](#-experimental-features)
* [Cross-Server Access](#cross-server-access)
* [Desktop App](#desktop-app)
* [🔍 Screenshot](#-screenshot)
* [🕰️ Changelogs](#-changelogs)
<!-- TOC -->
## 💡 Features ## 💡 Features
@@ -70,27 +107,36 @@
- Edit peer information - Edit peer information
- Delete peers with ease - Delete peers with ease
- Restrict peers - Restrict peers
- Generate QR Code and `.conf` file for peers - Generate QR Code and `.conf` file for peers, share it through a public link
- Schedule jobs to delete / restrict peer when conditions are met - Schedule jobs to delete / restrict peer when conditions are met
- View real time peer status - View real time peer status
- Testing tool: Ping and Traceroute to your peer - Testing tool: Ping and Traceroute to your peer
## 📝 Requirement ## 📝 Requirements
- Recommend the following OS, tested by our beloved users: 1. Supported operating systems. Please view the list below.
- [x] Ubuntu 18.04.1 LTS, 20.04.1 LTS, 22.04.4 LTS [@Me] 2. WireGuard & WireGuard-Tools (`wg-quick`)
- [x] Debian GNU/Linux 10 (buster) [❤️ @[robchez](https://github.com/robchez)] 3. Python 3.10 / 3.11 / 3.12
- [x] AlmaLinux 8.4 (Electric Cheetah) [❤️ @[barry-smithjr](https://github.com/)] 4. `git`, `net-tools`, `sudo` (_This should only apply to RHEL 9 & 8, interestingly it doesn't have it preinstalled)_
- [x] CentOS 7 [❤️ @[PrzemekSkw](https://github.com/PrzemekSkw)]
> **If you have tested on other OS and it works perfectly please provide it to me in [#31](https://github.com/donaldzou/wireguard-dashboard/issues/31). Thank you!** ### Supported Operating Systems
> [!NOTE]
> All operating systems below are tested by myself. All are ARM64 ran in UTM Virtual Machine.
- **WireGuard** and **WireGuard-Tools (`wg-quick`)** are installed. | Ubuntu | Debian | Red Hat Enterprise Linux | CentOS | Fedora |
|-----------|--------|--------------------------|----------|--------|
| 20.04 LTS | 12.6 | 9.4 | 9-Stream | 40 |
| 22.04 LTS | 11.10 | | | 39 |
| 24.02 LTS | | | | 38 |
> Don't know how? Check this <a href="https://www.wireguard.com/install/">official documentation</a> > [!TIP]
> If you installed WGDashboard on other systems without any issues, please let me know. Thank you!
- Configuration files under **`/etc/wireguard`**, but please note the following sample ### Existing WireGuard Configurations
> [!NOTE]
> This only applies to existing WireGuard Configuration under `/etc/wireguard`
```ini ```ini
[Interface] [Interface]
@@ -101,20 +147,124 @@
# make any changes with the dashboard, you can set it to false. # make any changes with the dashboard, you can set it to false.
[Peer] [Peer]
#Name# = Donald's iPhone
PublicKey = abcd1234 PublicKey = abcd1234
AllowedIPs = 1.2.3.4/32 AllowedIPs = 1.2.3.4/32
# Must have for each peer
``` ```
> [!TIP]
- Python 3.7+ & Pip3 > With `v4`, WGDashboard will look for entry with `#Name# = abc...` in each peer and use that for the name.
- Browser support CSS3 and ES6
## 🛠 Install ## 🛠 Install
### Install Commands
These commands are tested by myself in each OS. It contains commands to install WireGuard, Git, Net Tools, and even Python on some OS.
> [!WARNING]
> Please make sure you understand these commands before you run them.
#### Ubuntu 20.04 LTS
```shell
sudo add-apt-repository ppa:deadsnakes/ppa -y && \
sudo apt-get update -y && \
sudo apt-get install python3.10 python3.10-distutils wireguard-tools net-tools --no-install-recommends -y && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p
```
#### Ubuntu 22.04 LTS & Ubuntu 24.02 LTS
```shell
sudo apt-get update -y && \
sudo apt install wireguard-tools net-tools --no-install-recommends -y && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf
```
#### Debian 12.6
```shell
apt-get install sudo git iptables -y && \
sudo apt-get update && \
sudo apt install wireguard-tools net-tools && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf
```
#### Debian 11.10
> [!WARNING]
> This commands will download Python 3.10's source code and build from it, since Debian 11.10 doesn't comes with Python 3.10
```shell
apt-get install sudo -y && \
sudo apt-get update && \
sudo apt install -y git iptables build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev wget libbz2-dev wireguard-tools net-tools && \
wget https://www.python.org/ftp/python/3.10.0/Python-3.10.0.tgz && \
tar -xvf Python-3.10.0.tgz && \
cd Python-3.10.0 && \
sudo ./configure --enable-optimizations && \
sudo make && \
sudo make altinstall && \
cd .. && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf
```
#### Red Hat Enterprise Linux 9.4 & CentOS 9-Stream
```shell
sudo yum install wireguard-tools net-tools git python3.11 -y && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf && \
firewall-cmd --add-port=10086/tcp --permanent && \
firewall-cmd --add-port=51820/udp --permanent && \
firewall-cmd --reload
```
#### Fedora 40 & Fedora 39 & Fedora 38
```shell
sudo yum install wireguard-tools net-tools git -y && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf && \
firewall-cmd --add-port=10086/tcp --permanent && \
firewall-cmd --add-port=51820/udp --permanent && \
firewall-cmd --reload
```
### Manual Installation
> [!NOTE]
> To ensure a smooth installation process, please make sure Python 3.10/3.11/3.12, `git`, `wireguard-tools` and `net-tools` are installed :)
1. Download WGDashboard 1. Download WGDashboard
```shell ```shell
git clone -b v4.0 https://github.com/donaldzou/WGDashboard.git wgdashboard git clone https://github.com/donaldzou/WGDashboard.git wgdashboard
2. Open the WGDashboard folder 2. Open the WGDashboard folder
@@ -125,7 +275,7 @@
3. Install WGDashboard 3. Install WGDashboard
```shell ```shell
sudo chmod u+x wgd.sh sudo chmod u+x wgd.sh && \
sudo ./wgd.sh install sudo ./wgd.sh install
``` ```
@@ -141,14 +291,12 @@
sudo ./wgd.sh start sudo ./wgd.sh start
``` ```
**Note**:
> For [`pivpn`](https://github.com/pivpn/pivpn) user, please use `sudo ./wgd.sh start` to run if your current account does not have the permission to run `wg show` and `wg-quick`.
6. Access dashboard 6. Access dashboard
Access your server with port `10086` (e.g. http://your_server_ip:10086), using username `admin` and password `admin`. See below how to change port and ip that the dashboard is running with. Access your server with port `10086` (e.g. http://your_server_ip:10086), using username `admin` and password `admin`. See below how to change port and ip that the dashboard is running with.
## 🪜 Usage ## 🪜 Usage
#### Start/Stop/Restart WGDashboard #### Start/Stop/Restart WGDashboard
@@ -195,40 +343,33 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
```ini ```ini
[Unit] [Unit]
After=network.service After=syslog.target network-online.target
Wants=wg-quick.target
ConditionPathIsDirectory=/etc/wireguard
[Service] [Service]
WorkingDirectory=<your dashboard directory full path here> Type=forking
ExecStart=/usr/bin/python3 <your dashboard directory full path here>/dashboard.py PIDFile=<absolute_path_of_wgdashboard_src>/gunicorn.pid
WorkingDirectory=<absolute_path_of_wgdashboard_src>
ExecStart=<absolute_path_of_wgdashboard_src>/wgd.sh start
ExecStop=<absolute_path_of_wgdashboard_src>/wgd.sh stop
ExecReload=<absolute_path_of_wgdashboard_src>/wgd.sh restart
TimeoutSec=120
PrivateTmp=yes
Restart=always Restart=always
[Install] [Install]
WantedBy=default.target WantedBy=multi-user.target
``` ```
Now, we need to replace both `<your dashboard directory full path here>` to the one you just copied from step 2. After doing this, the file will become something like this, your file might be different: Now, we need to replace all `<absolute_path_of_wgdashboard_src>` to the one you just copied from step 2. After doing this, the file will become something like this, your file might be different:
```ini
[Unit]
After=netword.service
[Service]
WorkingDirectory=/root/wgdashboard/src
ExecStart=/usr/bin/python3 /root/wgdashboard/src/dashboard.py
Restart=always
[Install]
WantedBy=default.target
```
**Be aware that after the value of `WorkingDirectory`, it does not have a `/` (slash).** And then save the file after you edited it **Be aware that after the value of `WorkingDirectory`, it does not have a `/` (slash).** And then save the file after you edited it
4. Copy the service file to systemd folder 4. Copy the service file to systemd folder
```bash ```bash
$ cp wg-dashboard.service /etc/systemd/system/wg-dashboard.service $ sudo cp wg-dashboard.service /etc/systemd/system/wg-dashboard.service
``` ```
To make sure you copy the file successfully, you can use this command `cat /etc/systemd/system/wg-dashboard.service` to see if it will output the file you just edited. To make sure you copy the file successfully, you can use this command `cat /etc/systemd/system/wg-dashboard.service` to see if it will output the file you just edited.
@@ -247,28 +388,31 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
```bash ```bash
$ sudo systemctl status wg-dashboard.service $ sudo systemctl status wg-dashboard.service
``` ```
And you should see something like this And you should see something like this
```shell ```shell
● wg-dashboard.service ● wg-dashboard.service
Loaded: loaded (/etc/systemd/system/wg-dashboard.service; enabled; vendor preset: enabled) Loaded: loaded (/etc/systemd/system/wg-dashboard.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2021-08-03 22:31:26 UTC; 4s ago Active: active (running) since Wed 2024-08-14 22:21:47 EDT; 55s ago
Main PID: 6602 (python3) Process: 494968 ExecStart=/home/donaldzou/Wireguard-Dashboard/src/wgd.sh start (code=exited, status=0/SUCCESS)
Tasks: 1 (limit: 453) Main PID: 495005 (gunicorn)
Memory: 26.1M Tasks: 5 (limit: 4523)
Memory: 36.8M
CPU: 789ms
CGroup: /system.slice/wg-dashboard.service CGroup: /system.slice/wg-dashboard.service
└─6602 /usr/bin/python3 /root/wgdashboard/src/dashboard.py ├─495005 /home/donaldzou/Wireguard-Dashboard/src/venv/bin/python3 ./venv/bin/gunicorn --config ./gunicorn.conf.py
└─495007 /home/donaldzou/Wireguard-Dashboard/src/venv/bin/python3 ./venv/bin/gunicorn --config ./gunicorn.conf.py
Aug 03 22:31:26 ubuntu-wg systemd[1]: Started wg-dashboard.service. Aug 14 22:21:40 wg sudo[494978]: root : PWD=/home/donaldzou/Wireguard-Dashboard/src ; USER=root ; COMMAND=./venv/bin/gunicorn --config ./gunicorn.conf.py
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Serving Flask app1 "WGDashboard" (lazy loading) Aug 14 22:21:40 wg sudo[494978]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=0)
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Environment: production Aug 14 22:21:40 wg wgd.sh[494979]: [WGDashboard] WGDashboard w/ Gunicorn will be running on 0.0.0.0:10086
Aug 03 22:31:27 ubuntu-wg python3[6602]: WARNING: This is a development server. Do not use it in a production deployment. Aug 14 22:21:40 wg wgd.sh[494979]: [WGDashboard] Access log file is at ./log/access_2024_08_14_22_21_40.log
Aug 03 22:31:27 ubuntu-wg python3[6602]: Use a production WSGI server instead. Aug 14 22:21:40 wg wgd.sh[494979]: [WGDashboard] Error log file is at ./log/error_2024_08_14_22_21_40.log
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Debug mode: off Aug 14 22:21:40 wg sudo[494978]: pam_unix(sudo:session): session closed for user root
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Running on all addresses. Aug 14 22:21:45 wg wgd.sh[494968]: [WGDashboard] Checking if WGDashboard w/ Gunicorn started successfully
Aug 03 22:31:27 ubuntu-wg python3[6602]: WARNING: This is a development server. Do not use it in a production deployment. Aug 14 22:21:47 wg wgd.sh[494968]: [WGDashboard] WGDashboard w/ Gunicorn started successfully
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Running on http://0.0.0.0:10086/ (Press CTRL+C to quit) Aug 14 22:21:47 wg wgd.sh[494968]: ------------------------------------------------------------
Aug 14 22:21:47 wg systemd[1]: Started wg-dashboard.service.
``` ```
If you see `Active:` followed by `active (running) since...` then it means it run correctly. If you see `Active:` followed by `active (running) since...` then it means it run correctly.
@@ -290,7 +434,7 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following configurations: Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following configurations:
| | Description | Default | Edit Available | | | Description | Default | Edit Available |
| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | -------------- | |------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------|----------------|
| **`[Account]`** | *Configuration on account* | | | | **`[Account]`** | *Configuration on account* | | |
| `username` | Dashboard login username | `admin` | Yes | | `username` | Dashboard login username | `admin` | Yes |
| `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes | | `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes |
@@ -300,9 +444,10 @@ Since version 2.0, WGDashboard will be using a configuration file called `wg-das
| `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes | | `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes |
| `app_port` | Port the the dashboard will run with | `10086` | Yes | | `app_port` | Port the the dashboard will run with | `10086` | Yes |
| `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** | | `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** |
| `version` | Dashboard Version | `v3.0.5` | **No** | | `version` | Dashboard Version | `v4.0` | **No** |
| `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes | | `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes |
| `dashboard_sort` | How configuration is sorting | `status` | Yes | | `dashboard_sort` | How configuration is sorting | `status` | Yes |
| `dashboard_theme` | Dashboard Theme | `dark` | Yes |
| | | | | | | | | |
| **`[Peers]`** | *Default Settings on a new peer* | | | | **`[Peers]`** | *Default Settings on a new peer* | | |
| `peer_global_dns` | DNS Server | `1.1.1.1` | Yes | | `peer_global_dns` | DNS Server | `1.1.1.1` | Yes |
@@ -341,29 +486,12 @@ Endpoint = 0.0.0.0:51820
## ❓ How to update the dashboard? ## ❓ How to update the dashboard?
#### **Please note for user who is using `v2.3.1` or below** #### **Please note for users who are using `v3 - v3.0.6` want to update to `v4.0`**
- Although theoretically updating through `wgd.sh` should work, but I still suggest you to update the dashboard manually.
- For user who is using `v2.3.1` or below, please notice that all data that stored in the current database will **not** transfer to the new database. This is hard decision to move from TinyDB to SQLite. But SQLite does provide a thread-safe access and TinyDB doesn't. I couldn't find a safe way to transfer the data, so you need to do them manually... Sorry about that :pensive: . But I guess this would be a great start for future development :sunglasses:. #### **Please note for users who are using `v2.3.1` or below**
<hr> - For user who is using `v2.3.1` or below, please notice that all data that stored in the current database will **not** transfer to the new database. This is hard decision to move from TinyDB to SQLite. But SQLite does provide a thread-safe access and TinyDB doesn't. I couldn't find a safe way to transfer the data, so you need to do them manually... Sorry about that :pensive:。 But I guess this would be a great start for future development :sunglasses:.
#### Update Method 1 (For `v3.0` or above)
1. Change your directory to `wgdashboard/src`
```bash
cd wgdashboard/src
```
2. Update the dashboard with the following
```bash
./wgd.sh update
```
> If this doesn't work, please use the method below. Sorry about that :(
#### Update Method 2
1. Change your directory to `wgdashboard` 1. Change your directory to `wgdashboard`
@@ -374,191 +502,65 @@ Endpoint = 0.0.0.0:51820
2. Update the dashboard 2. Update the dashboard
```shell ```shell
git pull https://github.com/donaldzou/WGDashboard.git v3.0.5 --force git pull https://github.com/donaldzou/WGDashboard.git v4.0 --force
``` ```
3. Install 3. Install
```shell ```shell
./wgd.sh install sudo ./wgd.sh install
``` ```
Starting with `v3.0`, you can simply do `sudo ./wgd.sh update` !! (I hope)
## 🐬 Docker Solutions
Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol) Current, we have 2 beloved contributors provided solutions for hosting WGDashboard with Docker
## 🥘 Experimental Functions ### Solution 1 from @DaanSelen
#### Progressive Web App (PWA) for WGDashboard Please visit [Docker-explain.md](./docker/Docker-explain.md)
- With `v3.0`, I've added a `manifest.json` into the dashboard, so user could add their dashboard as a PWA to their browser or mobile device. ### Solution 2 from @shuricksumy
<img src="img/PWA.gif"/> Please visit [shuricksumy/docker-wgdashboard](https://github.com/shuricksumy/docker-wgdashboard)
> For questions or issues related to Docker, please visit [#272](https://github.com/donaldzou/WGDashboard/issues/272)
## 📖 WGDashboard REST API Documentation & How to use API Key
Please visit the [API Documentation](./docs/api-documents.md)
## 🥘 Experimental Features
### Cross-Server Access
Starting with `v4.0`, you can access WGDashboards on other server through one WGDashboard with API Keys
![Cross Server Example](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/cross-server.gif)
### Desktop App
Since the major changes for `v4.0` is to move the whole front-end code to Vue.js. And with this change, we can take the
advantage of combining ElectronJS and Vue.js to create a Desktop version of WGDashboard. Currently, we provide an Universal macOS app and a Windows app.
To download the app, please visit the [latest release](https://github.com/donaldzou/WGDashboard/releases).
![ElectronJS App Demo](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/electronjs-app.gif)
## 🔍 Screenshot ## 🔍 Screenshot
![Sign In Page](img/SignIn.png) ![Sign In](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/sign-in.png)
![Cross Server](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/cross-server.png)
![Index](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/index.png)
![New Configuration](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/new-configuration.png)
![Settings](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/settings.png)
![Light-Dark Mode](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/light-dark.png)
![Configuration](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/configuration.png)
![Add Peers](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/add-peers.png)
![Ping](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/ping.png)
![Traceroute](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/traceroute.png)
![Index Image](img/HomePage.png) ## 🕰️ Changelogs
![Configuration](img/Configuration.png) Please visit the [Changelogs.md](./docs/changelogs.md)
![Add Peer](img/AddPeer.png)
![Edit Peer](img/EditPeer.png)
![Delete Peer](img/DeleteBulk.png)
![Dashboard Setting](img/DashboardSetting.png)
![Ping](img/Ping.png)
![Traceroute](img/Traceroute.png)
## ⏰ Changelog
#### v3.0.0 - v3.0.6.2 - Jan 18, 2022
- 🎉 **New Features**
- **Moved from TinyDB to SQLite**: SQLite provide a better performance and loading speed when getting peers! Also avoided crashing the database due to **race condition**.
- **Added Gunicorn WSGI Server**: This could provide more stable on handling HTTP request, and more flexibility in the future (such as HTTPS support). **BIG THANKS to @pgalonza :heart:**
- **Add Peers by Bulk:** User can add peers by bulk, just simply set the amount and click add.
- **Delete Peers by Bulk**: User can delete peers by bulk, without deleting peers one by one.
- **Download Peers in Zip**: User can download all *downloadable* peers in a zip.
- **Added Pre-shared Key to peers:** Now each peer can add with a pre-shared key to enhance security. Previously added peers can add the pre-shared key through the peer setting button.
- **Redirect Back to Previous Page:** The dashboard will now redirect you back to your previous page if the current session got timed out and you need to sign in again.
- **Added Some [🥘 Experimental Functions](#-experimental-functions)**
- 🪚 **Bug Fixed**
- [IP Sorting range issues #99](https://github.com/donaldzou/WGDashboard/issues/99) [❤️ @barryboom]
- [INvalid character written to tunnel json file #108](https://github.com/donaldzou/WGDashboard/issues/108) [❤️ @ikidd]
- [Add IPv6 #91](https://github.com/donaldzou/WGDashboard/pull/91) [❤️ @pgalonza]
- [Added MTU and PersistentKeepalive to QR code and download files #112](https://github.com/donaldzou/WGDashboard/pull/112) [:heart: @reafian]
- **And many other bugs provided by our beloved users** :heart:
- **🧐 Other Changes**
- **Key generating moved to front-end**: No longer need to use the server's WireGuard to generate keys, thanks to the `wireguard.js` from the [official repository](https://git.zx2c4.com/wireguard-tools/tree/contrib/keygen-html/wireguard.js)!
- **Peer transfer calculation**: each peer will now show all transfer amount (previously was only showing transfer amount from the last configuration start-up).
- **UI adjustment on running peers**: peers will have a new style indicating that it is running.
- **`wgd.sh` finally can update itself**: So now user could update the whole dashboard from `wgd.sh`, with the `update` command.
- **Minified JS and CSS files**: Although only a small changes on the file size, but I think is still a good practice to save a bit of bandwidth ;)
*And many other small changes for performance and bug fixes! :laughing:*
#### v2.3.1 - Sep 8, 2021
- Updated dashboard's name to **WGDashboard**!!
#### v2.3 - Sep 8, 2021
- 🎉 **New Features**
- **Update directly from `wgd.sh`:** Now you can update WGDashboard directly from the bash script.
- **Displaying Peers:** You can switch the display mode between list and table in the configuration page.
- 🪚 **Bug Fixed**
- [Peer DNS Validation Fails #67](issues/67): Added DNS format check. [❤️ @realfian]
- [configparser.NoSectionError: No section: 'Interface' #66](issues/66): Changed permission requirement for `etc/wireguard` from `744` to `755`. [❤️ @ramalmaty]
- [Feature request: Interface not loading when information missing #73](issues/73): Fixed when Configuration Address and Listen Port is missing will crash the dashboard. [❤️ @js32]
- [Remote Peer, MTU and PersistentKeepalives added #70](pull/70): Added MTU, remote peer and Persistent Keepalive. [❤️ @realfian]
- [Fixes DNS check to support search domain #65](pull/65): Added allow input domain into DNS. [❤️@davejlong]
- **🧐 Other Changes**
- Moved Add Peer Button into the right bottom corner.
#### v2.2.1 - Aug 16, 2021
Bug Fixed:
- Added support for full subnet on Allowed IP
- Peer setting Save button
#### v2.2 - Aug 14, 2021
- 🎉 **New Features**
- **Add new peers**: Now you can add peers directly on dashboard, it will generate a pair of private key and public key. You can also set its DNS, endpoint allowed IPs. Both can set a default value in the setting page. [❤️ in [#44](https://github.com/donaldzou/wireguard-dashboard/issues/44)]
- **QR Code:** You can add the private key in peer setting of your existed peer to create a QR code. Or just create a new one, dashboard will now be able to auto generate a private key and public key ;) Don't worry, all keys will be generated on your machine, and **will delete all key files after they got generated**. [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
- **Peer configuration file download:** Same as QR code, you now can download the peer configuration file, so you don't need to manually input all the details on the peer machine! [❤️ in [#40](https://github.com/donaldzou/wireguard-dashboard/issues/40)]
- **Search peers**: You can now search peers by their name.
- **Autostart on boot:** Added a tutorial on how to start the dashboard to on boot! Please read the [tutorial below](#autostart-wireguard-dashboard-on-boot). [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
- **Click to copy**: You can now click and copy all peer's public key and configuration's public key.
- ....
- 🪚 **Bug Fixed**
- When there are comments in the wireguard config file, will cause the dashboard to crash.
- Used regex to search for config files.
- **🧐 Other Changes**
- Moved all external CSS and JavaScript file to local hosting (Except Bootstrap Icon, due to large amount of SVG files).
- Updated Python dependencies
- Flask: `v1.1.2 => v2.0.1`
- Jinja: `v2.10.1 => v3.0.1`
- icmplib: `v2.1.1 => v3.0.1`
- Updated CSS/JS dependencies
- Bootstrap: `v4.5.3 => v4.6.0`
- UI adjustment
- Adjusted how peers will display in larger screens, used to be 1 row per peer, now is 3 peers in 1 row.
#### v2.1 - Jul 2, 2021
- Added **Ping** and **Traceroute** tools!
- Adjusted the calculation of data usage on each peers
- Added refresh interval of the dashboard
- Bug fixed when no configuration on fresh install ([#23](https://github.com/donaldzou/wireguard-dashboard/issues/23))
- Fixed crash when too many peers ([#22](https://github.com/donaldzou/wireguard-dashboard/issues/22))
#### v2.0 - May 5, 2021
- Added login function to dashboard
- ***I'm not using the most ideal way to store the username and password, feel free to provide a better way to do this if you any good idea!***
- Added a config file to the dashboard
- Dashboard config can be change within the **Setting** tab on the side bar
- Adjusted UI
- And much more!
#### v1.1.2 - Apr 3, 2021
- Resolved issue [#3](https://github.com/donaldzou/wireguard-dashboard/issues/3).
#### v1.1.1 - Apr 2, 2021
- Able to add a friendly name to each peer. Thanks [#2](https://github.com/donaldzou/wireguard-dashboard/issues/2) !
#### v1.0 - Dec 27, 2020
- Added the function to remove peers
## 🛒 Dependencies
- CSS/JS
- [Bootstrap](https://getbootstrap.com/docs/4.6/getting-started/introduction/) `v4.6.0`
- [Bootstrap Icon](https://icons.getbootstrap.com) `v1.4.0`
- [jQuery](https://jquery.com) `v3.5.1`
- Python
- [Flask](https://pypi.org/project/Flask/) `v2.0.1`
- [ifcfg](https://pypi.org/project/ifcfg/) `v0.21`
- [icmplib](https://pypi.org/project/icmplib/) `v2.1.1`
- [flask-qrcode](https://pypi.org/project/Flask-QRcode/) `v3.0.0`
## ✨ Contributors
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/antonioag95"><img src="https://avatars.githubusercontent.com/u/30556866?v=4?s=100" width="100px;" alt=""/><br /><sub><b>antonioag95</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=antonioag95" title="Tests">⚠️</a> <a href="https://github.com/donaldzou/WGDashboard/commits?author=antonioag95" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tonjo"><img src="https://avatars.githubusercontent.com/u/4726289?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tonjo</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=tonjo" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/reafian"><img src="https://avatars.githubusercontent.com/u/11992416?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Richard Newton</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=reafian" title="Code">💻</a></td>
<td align="center"><a href="http://www.davejlong.com"><img src="https://avatars.githubusercontent.com/u/175317?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Long</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=davejlong" title="Code">💻</a></td>
<td align="center"><a href="http://www.std-soft.com"><img src="https://avatars.githubusercontent.com/u/5978293?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Markus Neubauer</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=marneu" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
+10
View File
@@ -0,0 +1,10 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
-19
View File
@@ -1,19 +0,0 @@
version: "3.5"
services:
wgdashboard:
image: donaldzou/wgdashboard
build:
context: ./
dockerfile: ./Dockerfile
cap_add:
- NET_ADMIN
- SYS_MODULE
restart: unless-stopped
volumes:
- /lib/modules:/lib/modules
- ./src:/opt/wgdashboard
- ./config:/etc/wireguard
ports:
- 10086:10086
- 51820:51820/udp
+82
View File
@@ -0,0 +1,82 @@
# WG-Dashboard Docker Explanation:
Author: DaanSelen<br>
This document delves into how the WG-Dashboard Docker container has been built.<br>
Of course there are two stages, one before run-time and one at/after run-time.<br>
The `Dockerfile` describes how the container image is made, and the `entrypoint.sh` is executed after running the container. <br>
In this example, WireGuard is integrated into the container itself, so it should be a run-and-go.<br>
For more details on the source-code specific to this Docker image, refer to the source files, they have lots of comments.
I have tried to embed some new features such as `isolated_peers` and interface startup on container-start (through `enable_wg0`).
<img src="https://raw.githubusercontent.com/donaldzou/WGDashboard/main/img/logo.png" alt="WG-Dashboard Logo" title="WG-Dashboard Logo" width="150" height="150" />
## Getting the container running:
To get the container running you either pull the image from the repository, at the moment: `repo.nerthus.nl/app/wireguard-dashboard:latest`.<br>
From there either use the environment variables describe below as parameters or use the Docker Compose file: `compose.yaml`.
An example of a simple command to get the container running is show below:<br>
```shell
docker run -d \
--name wireguard-dashboard \
--restart unless-stopped \
-e enable_wg0=true \
-e isolated_peers=true \
-p 10086:10086/tcp \
-p 51820:51820/udp \
--cap-add NET_ADMIN \
repo.nerthus.nl/app/wireguard-dashboard:latest
```
<br>
If you want to use Compose instead of a raw Docker command, refer to the example in the `compose.yaml` or the one pasted below:
<br><br>
```yaml
services:
wireguard-dashboard:
image: repo.nerthus.nl/app/wireguard-dashboard:latest
restart: unless-stopped
container_name: wire-dash
environment:
#- tz=
#- global_dns=
- enable_wg0=true
- isolated_peers=false
#- public_ip=
ports:
- 10086:10086/tcp
- 51820:51820/udp
volumes:
- conf:/etc/wireguard
- app:/opt/wireguarddashboard/app
cap_add:
- NET_ADMIN
volumes:
conf:
app:
```
If you want to customize the yaml, make sure the core stays the same, but for example volume PATHs can be freely changed.<br>
This setup is just generic and will use the Docker volumes.
## Working with the container and environment variables:
Once the container is running, the installation process is essentially the same as running it on bare-metal.<br>
So go to the assign TCP port in this case HTTP, like the default 10086 one in the example and log into the WEB-GUI.<br>
| Environment variable | Accepted arguments | Default value | Verbose |
| -------------- | ------- | ------- | ------- |
| tz | Europe/Amsterdam or any confirming timezone notation. | Europe/Amsterdam | Sets the timezone of the Docker container. This is to timesync the container to any other processes which would need it. |
| global_dns | Any IPv4 address, such as my personal recommendation: 9.9.9.9 (QUAD9) | 1.1.1.1 | Set the default DNS given to clients once they connect to the WireGuard tunnel (VPN).
| enable_wg0 | `true` or `false` | `false` | Enables or disables the starting of the WireGuard interface on container 'boot-up'.
| isolated_peers | `true` or `false` | `true` | For security the default is true, and it disables peers to ping or reach eachother, the WireGuard interface IS able to reach the peers (Done through `iptables`).
| public_ip | Any IPv4 (public recommended) address, such as the one returned by default | Default uses the return of `curl ifconfig.me` | To reach your VPN from outside your own network, you need WG-Dashboard to know what your public IP-address is, otherwise it will generate faulty config files for clients.
## Closing remarks:
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.
+77
View File
@@ -0,0 +1,77 @@
# Pull from small Debian stable image.
FROM debian:stable-slim
LABEL maintainer="dselen@nerthus.nl"
# Declaring environment variables, change Peernet to an address you like, standard is a 24 bit subnet.
ENV wg_net="10.0.0.1"
# wg_net is used functionally as an ARG for its environment variable nature, do not change unless you know what you are doing.
# Following ENV variables are changable on container runtime because /entrypoint.sh handles that. See compose.yaml for more info.
ENV tz="Europe/Amsterdam"
ENV global_dns="1.1.1.1"
ENV enable_wg0="false"
ENV isolated_peers="true"
ENV public_ip="0.0.0.0"
# Doing basic system maintenance. Change the timezone to the desired timezone.
RUN ln -sf /usr/share/zoneinfo/${tz} /etc/localtime
# Doing package management operations, such as upgrading
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y --no-install-recommends curl \
git \
iproute2 \
iptables \
iputils-ping \
openresolv \
procps \
python3 \
python3-pip \
python3-venv \
traceroute \
wireguard \
wireguard-tools \
&& apt-get remove linux-image-* --autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Removing the Linux Image package to preserve space on the image, for this reason also deleting apt lists, to be able to install packages: run apt update.
# Using WGDASH -- like wg_net functionally as a ARG command. But it is needed in entrypoint.sh so it needs to be exported as environment variable.
ENV WGDASH=/opt/wireguarddashboard
RUN python3 -m venv ${WGDASH}/venv
# Doing WireGuard Dashboard installation measures. Modify the git clone command to get the preferred version, with a specific branch for example.
RUN . ${WGDASH}/venv/bin/activate \
&& git clone https://github.com/donaldzou/WGDashboard.git ${WGDASH}/app \
&& pip3 install -r ${WGDASH}/app/src/requirements.txt \
&& chmod +x ${WGDASH}/app/src/wgd.sh \
&& .${WGDASH}/app/src/wgd.sh install
# Set the volume to be used for persistency.
VOLUME /etc/wireguard
# Generate basic WireGuard interface. Echoing the WireGuard interface config for readability, adjust if you want it for efficiency.
# Also setting the pipefail option, verbose: https://github.com/hadolint/hadolint/wiki/DL4006.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN wg genkey | tee /etc/wireguard/wg0_privatekey \
&& echo "[Interface]" > /wg0.conf \
&& echo "SaveConfig = true" >> /wg0.conf \
&& echo "Address = ${wg_net}/24" >> /wg0.conf \
&& echo "PrivateKey = $(cat /etc/wireguard/wg0_privatekey)" >> /wg0.conf \
&& echo "PostUp = iptables -t nat -I POSTROUTING 1 -s ${wg_net}/24 -o $(ip -o -4 route show to default | awk '{print $NF}') -j MASQUERADE" >> /wg0.conf \
&& echo "PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP" >> /wg0.conf \
&& echo "PreDown = iptables -t nat -D POSTROUTING 1" >> /wg0.conf \
&& echo "PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP" >> /wg0.conf \
&& echo "ListenPort = 51820" >> /wg0.conf \
#&& echo "DNS = ${global_dns}" >> /wg0.conf \
&& rm /etc/wireguard/wg0_privatekey
# Defining a way for Docker to check the health of the container. In this case: checking the login URL.
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD curl -f http://localhost:10086/signin || exit 1
# Copy the basic entrypoint.sh script.
COPY entrypoint.sh /entrypoint.sh
# Exposing the default WireGuard Dashboard port for web access.
EXPOSE 10086
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
+23
View File
@@ -0,0 +1,23 @@
services:
wireguard-dashboard:
image: repo.nerthus.nl/app/wireguard-dashboard:latest
restart: unless-stopped
container_name: wire-dash
environment:
#- tz= # <--- Set container timezone, default: Europe/Amsterdam.
#- global_dns= # <--- Set global DNS address, default: 1.1.1.1.
- enable_wg0=true # <--- If true, wg0 will be started on container startup. default: false.
- isolated_peers=false # <--- When set to true, it disallows peers to talk to eachother, setting to false, allows it, default: true.
#- public_ip= # <--- Set public IP to ensure the correct one is chosen, defaulting to the IP give by ifconfig.me.
ports:
- 10086:10086/tcp
- 51820:51820/udp
volumes:
- conf:/etc/wireguard
- app:/opt/wireguarddashboard/app
cap_add:
- NET_ADMIN
volumes:
conf:
app:
+109
View File
@@ -0,0 +1,109 @@
#!/bin/bash
echo "Starting the WireGuard Dashboard Docker container."
clean_up() {
# Cleaning out previous data such as the .pid file and starting the WireGuard Dashboard. Making sure to use the python venv.
echo "Looking for remains of previous instances..."
if [ -f "/opt/wireguarddashboard/app/src/gunicorn.pid" ]; then
echo "Found old .pid file, removing."
rm /opt/wireguarddashboard/app/src/gunicorn.pid
else
echo "No remains found, continuing."
fi
}
start_core() {
# This first step is to ensure the wg0.conf file exists, and if not, then its copied over from the ephemeral container storage.
if [ ! -f "/etc/wireguard/wg0.conf" ]; then
cp "/wg0.conf" "/etc/wireguard/wg0.conf"
echo "WireGuard interface file copied over."
else
echo "WireGuard interface file looks to already be existing."
fi
echo "Activating Python venv and executing the WireGuard Dashboard service."
. "${WGDASH}"/venv/bin/activate
cd "${WGDASH}"/app/src || return # If changing the directory fails (permission or presence error), then bash will exist this function, causing the WireGuard Dashboard to not be succesfully launched.
bash wgd.sh start
# The following section takes care of the firewall rules regarding the 'isolated_peers' feature, which allows or drops packets destined from the wg0 to the wg0 interface.
if [ "${isolated_peers,,}" = "false" ]; then
echo "Isolated peers disabled, adjusting."
sed -i '/PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP/d' /etc/wireguard/wg0.conf
sed -i '/PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP/d' /etc/wireguard/wg0.conf
elif [ "${isolated_peers,,}" = "true" ]; then
upblocking=$(grep -c "PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP" /etc/wireguard/wg0.conf)
downblocking=$(grep -c "PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP" /etc/wireguard/wg0.conf)
if [ "$upblocking" -lt 1 ] && [ "$downblocking" -lt 1 ]; then
echo "Isolated peers enabled, adjusting."
sed -i '/PostUp = iptables -t nat -I POSTROUTING 1 -s/a PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP' /etc/wireguard/wg0.conf
sed -i '/PreDown = iptables -t nat -D POSTROUTING 1 -s/a PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP' /etc/wireguard/wg0.conf
fi
fi
# The following section takes care of
if [ "${enable_wg0,,}" = "true" ]; then
echo "Preference for wg0 to be turned on found."
wg-quick up wg0
else
echo "Preference for wg0 to be turned off found."
fi
}
set_envvars() {
echo "Setting relevant variables for operation."
# If the timezone is different, for example in North-America or Asia.
if [ "${tz}" != "$(cat /etc/timezone)" ]; then
echo "Changing timezone."
ln -sf /usr/share/zoneinfo/"${tz}" /etc/localtime
echo "${tz}" > /etc/timezone
fi
# Changing the DNS used for clients and the dashboard itself.
if [ "${global_dns}" != "$(grep "peer_global_dns = " /opt/wireguarddashboard/app/src/wg-dashboard.ini | awk '{print $NF}')" ]; then
echo "Changing default dns."
#sed -i "s/^DNS = .*/DNS = ${global_dns}/" /etc/wireguard/wg0.conf # Uncomment if you want to have DNS on server-level.
sed -i "s/^peer_global_dns = .*/peer_global_dns = ${global_dns}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
fi
# Setting the public IP of the WireGuard Dashboard container host. If not defined, it will trying fetching it using a curl to ifconfig.me.
if [ "${public_ip}" = "0.0.0.0" ]; then
default_ip=$(curl -s ifconfig.me)
echo "Trying to fetch the Public-IP using ifconfig.me: ${default_ip}"
sed -i "s/^remote_endpoint = .*/remote_endpoint = ${default_ip}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
elif [ "${public_ip}" != "$(grep "remote_endpoint = " /opt/wireguarddashboard/app/src/wg-dashboard.ini | awk '{print $NF}')" ]; then
echo "Setting the Public-IP using given variable: ${public_ip}"
sed -i "s/^remote_endpoint = .*/remote_endpoint = ${public_ip}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
fi
}
ensure_blocking() {
sleep 1s
echo "Ensuring container continuation."
# This function checks if the latest error log is created and tails it for docker logs uses.
if find "/opt/wireguarddashboard/app/src/log" -mindepth 1 -maxdepth 1 -type f | read -r; then
latestErrLog=$(find /opt/wireguarddashboard/app/src/log -name "error_*.log" | head -n 1)
latestAccLog=$(find /opt/wireguarddashboard/app/src/log -name "access_*.log" | head -n 1)
tail -f "${latestErrLog}" "${latestAccLog}"
fi
# Blocking command in case of erroring. So the container does not quit.
sleep infinity
}
# Execute functions for the WireGuard Dashboard services, then set the environment variables
clean_up
start_core
set_envvars
ensure_blocking
+612
View File
@@ -0,0 +1,612 @@
# 📖 API Document for WGDashboard
**Version: v4.0**
**Created by: Donald Zou**
<!-- TOC -->
* [📖 API Document for WGDashboard](#-api-document-for-wgdashboard)
* [🔑 How to use API Key?](#-how-to-use-api-key)
* [Create API Key](#create-api-key)
* [Use API Key](#use-api-key)
* [API Endpoints](#api-endpoints)
* [Handshake to Server](#handshake-to-server)
* [Request](#request)
* [Response](#response)
* [Validate Authentication](#validate-authentication)
* [Request](#request-1)
* [Response](#response-1)
* [Authenticate](#authenticate)
* [Request](#request-2)
* [Body Parameters](#body-parameters)
* [Response](#response-2)
* [Sign Out](#sign-out)
* [Request](#request-3)
* [Response](#response-3)
* [Get WireGuard Configurations](#get-wireguard-configurations)
* [Request](#request-4)
* [Response](#response-4)
* [Add WireGuard Configuration](#add-wireguard-configuration)
* [Request](#request-5)
* [Body Parameters](#body-parameters-1)
* [Response](#response-5)
* [Toggle WireGuard Configuration](#toggle-wireguard-configuration)
* [Request](#request-6)
* [Query String Parameter](#query-string-parameter)
* [Response](#response-6)
* [Get WGDashboard Configuration](#get-wgdashboard-configuration)
* [Request](#request-7)
* [Response](#response-7)
* [Update WGDashboard Configuration Item](#update-wgdashboard-configuration-item)
* [Request](#request-8)
* [Body Parameters](#body-parameters-2)
* [Response](#response-8)
* [Get WGDashboard API Keys](#get-wgdashboard-api-keys)
* [Request](#request-9)
* [Response](#response-9)
* [Add WGDashboard API Key](#add-wgdashboard-api-key)
* [Request](#request-10)
* [Body Parameters](#body-parameters-3)
* [Response](#response-10)
* [Endpoint](#endpoint)
* [Request](#request-11)
* [Response](#response-11)
<!-- TOC -->
<hr>
## 🔑 How to use API Key?
### Create API Key
1. To request an API Key, simply login to your WGDashboard, go to **Settings**, scroll to the very bottom. Click the **switch** on the right to enable API Key.
2. Click the blur **Create** button, set an **expiry date** you want or **never expire**, then click **Done**.
### Use API Key
- Simply add `wg-dashboard-apikey` with the value of your API key into the HTTP Header.
```javascript
fetch('http://server:10086/api/handshake', {
headers: {
'content-type': 'application/json',
'wg-dashboard-apikey': 'insert your api key here'
},
method: "GET"
})
```
## API Endpoints
### Handshake to Server
This endpoint is designed for a simple handshake when using API key to connect. If `status` is `true` that means
#### Request
`GET /api/handshake`
#### Response
`200 - OK`
```json
{
"data": null,
"message": null,
"status": true
}
```
`401 - UNAUTHORIZED`
```json
{
"data": null,
"message": "Unauthorized access.",
"status": false
}
```
> Notice: this `401` response will return at all endpoint if your API Key or session is invalid.
<hr>
### Validate Authentication
This endpoint if needed for non-cross-server access. This will check if the cookie on the client side is still valid on the server side.
#### Request
`GET /api/validateAuthentication`
#### Response
`200 - OK`
Session is still valid
```json
{
"data": null,
"message": null,
"status": true
}
```
Session is invalid
```json
{
"data": null,
"message": "Invalid authentication.",
"status": false
}
```
<hr>
### Authenticate
This endpoint is dedicated for non-cross-server access. It is used to authenticate user's username, password and TOTP
#### Request
`POST /api/authenticate`
##### Body Parameters
```json
{
"username": "admin",
"password": "admin",
"totp": "123456"
}
```
| Parameter | Type |
|------------|--------|
| `username` | string |
| `password` | string |
| `totp` | string |
#### Response
`200 - OK`
If username, password and TOTP matched
```json
{
"data": null,
"message": null,
"status": true
}
```
If username, password or TOTP is not match
```json
{
"data": null,
"message": "Sorry, your username, password or OTP is incorrect.",
"status": false
}
```
<hr>
### Sign Out
To remove the current session on server side
#### Request
`GET /api/signout`
#### Response
`200 - OK`
```json
{
"data": null,
"message": null,
"status": true
}
```
<hr>
### Get WireGuard Configurations
To get all WireGuard configurations in `/etc/wireguard`
#### Request
`GET /api/getWireguardConfigurations`
#### Response
`200 - OK`
```json
{
"data": [
{
"Address": "10.200.200.1/24",
"ConnectedPeers": 0,
"DataUsage": {
"Receive": 0.1582,
"Sent": 2.1012999999999997,
"Total": 2.2595
},
"ListenPort": "51820",
"Name": "wg0",
"PostDown": "iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp0s1 -j MASQUERADE;",
"PostUp": "iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp0s1 -j MASQUERADE;",
"PreDown": "",
"PreUp": "",
"PrivateKey": "8DsSMli3okgUx5frKbFQ0fMW5ZMyqyxOdOW7+g21L18=",
"PublicKey": "GQlGi8QJ93hWY7L2xlJyh+7S6+ekER9xP11T92T0O0Q=",
"SaveConfig": true,
"Status": false
}
],
"message": null,
"status": true
}
```
<hr>
### Add WireGuard Configuration
Add a new WireGuard Configuration
#### Request
`POST /api/addWireguardConfiguration`
##### Body Parameters
```json
{
"ConfigurationName": "wg0",
"Address": "10.0.0.1/24",
"ListenPort": 51820,
"PrivateKey": "eJuuamCgakVs2xUZGHh/g7C6Oy89JGh7eE2jjEGbbFc=",
"PublicKey": "3Ruirgw9qNRwNpBepkiVjjSe82tY+lDZr6WaFC4wO2g=",
"PresharedKey": "GMMLKWdJlgsKVoR26BJPsNbDXyfILL+x1Nd6Ecmn4lg=",
"PreUp": "",
"PreDown": "iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp0s1 -j MASQUERADE;",
"PostUp": "iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp0s1 -j MASQUERADE;",
"PostDown": ""
}
```
| Parameter | Type |
|---------------------|--------|
| `ConfigurationName` | string |
| `Address` | string |
| `ListenPort` | int |
| `PrivateKey` | string |
| `PublicKey` | string |
| `PresharedKey` | string |
| `PreUp` | string |
| `PreDown` | string |
| `PostUp` | string |
| `PostDown` | string |
#### Response
`200 - OK`
If everything is good
```json
{
"data": null,
"message": null,
"status": true
}
```
If the new configuration's `ConfigurationName` is already existed
```json
{
"data": null,
"message": "Already have a configuration with the name \"wg0\"",
"status": false
}
```
If the new configuration's `ListenPort` is used by another configuration
```json
{
"data": null,
"message": "Already have a configuration with the port \"51820\"",
"status": false
}
```
If the new configuration's `Address` is used by another configuration
```json
{
"data": null,
"message": "Already have a configuration with the address \"10.0.0.1/24\"",
"status": false
}
```
<hr>
### Toggle WireGuard Configuration
To turn on/off of a WireGuard Configuration
#### Request
`GET /api/toggleWireguardConfiguration/?configurationName=`
##### Query String Parameter
| Parameter | Type |
|---------------------|--------|
| `configurationName` | string |
#### Response
`200 - OK`
If toggle is successful, server will return the current status in `status`: `true` or `false` indicating if the configuration is up or not.
```json
{
"data": true,
"message": null,
"status": true
}
```
If the `configurationName` provided does not exist
```json
{
"data": null,
"message": "Please provide a valid configuration name",
"status": false
}
```
<hr>
### Get WGDashboard Configuration
Get the WGDashboard Configuration, such as `dashboard_theme`...
#### Request
`GET /api/getDashboardConfiguration`
#### Response
`200 - OK`
```json
{
"data": {
"Account": {
"enable_totp": false,
"password": "some hashed value :(",
"totp_verified": false,
"username": "admin"
},
"Database": {
"type": "sqlite"
},
"Other": {
"welcome_session": false
},
"Peers": {
"peer_display_mode": "grid",
"peer_endpoint_allowed_ip": "0.0.0.0/0",
"peer_global_dns": "1.1.1.1",
"peer_keep_alive": "21",
"peer_mtu": "1420",
"remote_endpoint": "192.168.2.38"
},
"Server": {
"app_ip": "0.0.0.0",
"app_port": "10086",
"app_prefix": "",
"auth_req": true,
"dashboard_api_key": true,
"dashboard_refresh_interval": "5000",
"dashboard_sort": "status",
"dashboard_theme": "dark",
"version": "v4.0",
"wg_conf_path": "/etc/wireguard"
}
},
"message": null,
"status": true
}
```
<hr>
### Update WGDashboard Configuration Item
Update the WGDashboard Configuration one at a time
#### Request
`POST /api/updateDashboardConfigurationItem`
##### Body Parameters
```json
{
"section": "Server",
"key": "dashboard_theme",
"value": "dark"
}
```
| Parameter | Type | |
|-----------|--------|----------------------------------------------------------|
| `section` | string | Each section in the `wg-dashboard.ini` |
| `key` | string | Each key/value pair under each in the `wg-dashboard.ini` |
| `value` | string | Value for this key/value pair |
#### Response
`200 - OK`
If update is success
```json
{
"data": true,
"message": null,
"status": true
}
```
If update failed
```json
{
"data": true,
"message": "Message related to the error will appear here",
"status": false
}
```
<hr>
### Get WGDashboard API Keys
Get a list of active API key in WGDashboard
#### Request
`GET /api/getDashboardAPIKeys`
#### Response
`200 - OK`
If API Key function is enabled and there are active API keys
> If `ExpiredAt` is `null`, that means this API key will never expire
```json
{
"data": [
{
"CreatedAt": "2024-08-15 00:42:31",
"ExpiredAt": null,
"Key": "AXt1x3TZMukmA-eSnAyESy08I14n20boppSsknHOB-Y"
},
{
"CreatedAt": "2024-08-14 22:50:44",
"ExpiredAt": "2024-08-21 22:50:43",
"Key": "ry0Suo0BrypSMzbq0C_TjkEcgrFHHj6UBZGmC2-KI2o"
}
],
"message": null,
"status": true
}
```
If API key function is disabled
```json
{
"data": null,
"message": "Dashboard API Keys function is disabled",
"status": false
}
```
<hr>
### Add WGDashboard API Key
Add a new API Key in WGDashboard
#### Request
`POST /api/newDashboardAPIKey`
##### Body Parameters
```json
{
"neverExpire": false,
"ExpiredAt": "2024-12-31 16:00:00"
}
```
| Parameter | Type | |
|---------------|--------|-----------------------------------------------------------------------------------|
| `neverExpire` | bool | If this is `false`, please specify a date in `ExpiredAt` |
| `ExpiredAt` | string | If `neverExpire` is `true`, this can be omitted. Format is `YYYY-MM-DD hh:mm:ss`. |
#### Response
`200 - OK`
If success, it will return the latest list of API Keys
```json
{
"data": [
{
"CreatedAt": "2024-08-15 00:42:31",
"ExpiredAt": null,
"Key": "AXt1x3TZMukmA-eSnAyESy08I14n20boppSsknHOB-Y"
},
{
"CreatedAt": "2024-08-14 22:50:44",
"ExpiredAt": "2024-12-31 16:50:43",
"Key": "ry0Suo0BrypSMzbq0C_TjkEcgrFHHj6UBZGmC2-KI2o"
}
],
"message": null,
"status": true
}
```
If API key function is disabled
```json
{
"data": null,
"message": "Dashboard API Keys function is disabled",
"status": false
}
```
<hr>
### Endpoint
Description
#### Request
`GET`
#### Response
`200 - OK`
```json
{
"data": true,
"message": null,
"status": true
}
```
+105
View File
@@ -0,0 +1,105 @@
# ⏰ Changelogs of WGDashboard
#### v3.0.0 - v3.0.6.2 - Jan 18, 2022
- 🎉 **New Features**
- **Moved from TinyDB to SQLite**: SQLite provide a better performance and loading speed when getting peers! Also avoided crashing the database due to **race condition**.
- **Added Gunicorn WSGI Server**: This could provide more stable on handling HTTP request, and more flexibility in the future (such as HTTPS support). **BIG THANKS to @pgalonza :heart:**
- **Add Peers by Bulk:** User can add peers by bulk, just simply set the amount and click add.
- **Delete Peers by Bulk**: User can delete peers by bulk, without deleting peers one by one.
- **Download Peers in Zip**: User can download all *downloadable* peers in a zip.
- **Added Pre-shared Key to peers:** Now each peer can add with a pre-shared key to enhance security. Previously added peers can add the pre-shared key through the peer setting button.
- **Redirect Back to Previous Page:** The dashboard will now redirect you back to your previous page if the current session got timed out and you need to sign in again.
- **Added Some [🥘 Experimental Functions](#-experimental-functions)**
- 🪚 **Bug Fixed**
- [IP Sorting range issues #99](https://github.com/donaldzou/WGDashboard/issues/99) [❤️ @barryboom]
- [INvalid character written to tunnel json file #108](https://github.com/donaldzou/WGDashboard/issues/108) [❤️ @ikidd]
- [Add IPv6 #91](https://github.com/donaldzou/WGDashboard/pull/91) [❤️ @pgalonza]
- [Added MTU and PersistentKeepalive to QR code and download files #112](https://github.com/donaldzou/WGDashboard/pull/112) [:heart: @reafian]
- **And many other bugs provided by our beloved users** :heart:
- **🧐 Other Changes**
- **Key generating moved to front-end**: No longer need to use the server's WireGuard to generate keys, thanks to the `wireguard.js` from the [official repository](https://git.zx2c4.com/wireguard-tools/tree/contrib/keygen-html/wireguard.js)!
- **Peer transfer calculation**: each peer will now show all transfer amount (previously was only showing transfer amount from the last configuration start-up).
- **UI adjustment on running peers**: peers will have a new style indicating that it is running.
- **`wgd.sh` finally can update itself**: So now user could update the whole dashboard from `wgd.sh`, with the `update` command.
- **Minified JS and CSS files**: Although only a small changes on the file size, but I think is still a good practice to save a bit of bandwidth ;)
*And many other small changes for performance and bug fixes! :laughing:*
#### v2.3.1 - Sep 8, 2021
- Updated dashboard's name to **WGDashboard**!!
#### v2.3 - Sep 8, 2021
- 🎉 **New Features**
- **Update directly from `wgd.sh`:** Now you can update WGDashboard directly from the bash script.
- **Displaying Peers:** You can switch the display mode between list and table in the configuration page.
- 🪚 **Bug Fixed**
- [Peer DNS Validation Fails #67](issues/67): Added DNS format check. [❤️ @realfian]
- [configparser.NoSectionError: No section: 'Interface' #66](issues/66): Changed permission requirement for `etc/wireguard` from `744` to `755`. [❤️ @ramalmaty]
- [Feature request: Interface not loading when information missing #73](issues/73): Fixed when Configuration Address and Listen Port is missing will crash the dashboard. [❤️ @js32]
- [Remote Peer, MTU and PersistentKeepalives added #70](pull/70): Added MTU, remote peer and Persistent Keepalive. [❤️ @realfian]
- [Fixes DNS check to support search domain #65](pull/65): Added allow input domain into DNS. [❤️@davejlong]
- **🧐 Other Changes**
- Moved Add Peer Button into the right bottom corner.
#### v2.2.1 - Aug 16, 2021
Bug Fixed:
- Added support for full subnet on Allowed IP
- Peer setting Save button
#### v2.2 - Aug 14, 2021
- 🎉 **New Features**
- **Add new peers**: Now you can add peers directly on dashboard, it will generate a pair of private key and public key. You can also set its DNS, endpoint allowed IPs. Both can set a default value in the setting page. [❤️ in [#44](https://github.com/donaldzou/wireguard-dashboard/issues/44)]
- **QR Code:** You can add the private key in peer setting of your existed peer to create a QR code. Or just create a new one, dashboard will now be able to auto generate a private key and public key ;) Don't worry, all keys will be generated on your machine, and **will delete all key files after they got generated**. [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
- **Peer configuration file download:** Same as QR code, you now can download the peer configuration file, so you don't need to manually input all the details on the peer machine! [❤️ in [#40](https://github.com/donaldzou/wireguard-dashboard/issues/40)]
- **Search peers**: You can now search peers by their name.
- **Autostart on boot:** Added a tutorial on how to start the dashboard to on boot! Please read the [tutorial below](#autostart-wireguard-dashboard-on-boot). [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
- **Click to copy**: You can now click and copy all peer's public key and configuration's public key.
- ....
- 🪚 **Bug Fixed**
- When there are comments in the wireguard config file, will cause the dashboard to crash.
- Used regex to search for config files.
- **🧐 Other Changes**
- Moved all external CSS and JavaScript file to local hosting (Except Bootstrap Icon, due to large amount of SVG files).
- Updated Python dependencies
- Flask: `v1.1.2 => v2.0.1`
- Jinja: `v2.10.1 => v3.0.1`
- icmplib: `v2.1.1 => v3.0.1`
- Updated CSS/JS dependencies
- Bootstrap: `v4.5.3 => v4.6.0`
- UI adjustment
- Adjusted how peers will display in larger screens, used to be 1 row per peer, now is 3 peers in 1 row.
#### v2.1 - Jul 2, 2021
- Added **Ping** and **Traceroute** tools!
- Adjusted the calculation of data usage on each peers
- Added refresh interval of the dashboard
- Bug fixed when no configuration on fresh install ([#23](https://github.com/donaldzou/wireguard-dashboard/issues/23))
- Fixed crash when too many peers ([#22](https://github.com/donaldzou/wireguard-dashboard/issues/22))
#### v2.0 - May 5, 2021
- Added login function to dashboard
- ***I'm not using the most ideal way to store the username and password, feel free to provide a better way to do this if you any good idea!***
- Added a config file to the dashboard
- Dashboard config can be change within the **Setting** tab on the side bar
- Adjusted UI
- And much more!
#### v1.1.2 - Apr 3, 2021
- Resolved issue [#3](https://github.com/donaldzou/wireguard-dashboard/issues/3).
#### v1.1.1 - Apr 2, 2021
- Able to add a friendly name to each peer. Thanks [#2](https://github.com/donaldzou/wireguard-dashboard/issues/2) !
#### v1.0 - Dec 27, 2020
- Added the function to remove peers
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
# if [ -z "$(ls -A /etc/wireguard)" ]; then
# mv /wg0.conf /etc/wireguard
# echo "Moved conf file to /etc/wireguard"
# else
# rm wg0.conf
# echo "Removed unneeded conf file"
# fi
# wg-quick up wg0
chmod u+x /opt/wgdashboard/wgd.sh
if [ ! -f "/opt/wgdashboard/wg-dashboard.ini" ]; then
/opt/wgdashboard/wgd.sh install
fi
/opt/wgdashboard/wgd.sh debug
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

+462 -180
View File
File diff suppressed because it is too large Load Diff
+15 -1
View File
@@ -1,8 +1,14 @@
import multiprocessing
import dashboard import dashboard
from datetime import datetime
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger
app_host, app_port = dashboard.gunicornConfig() app_host, app_port = dashboard.gunicornConfig()
date = datetime.today().strftime('%Y_%m_%d_%H_%M_%S')
def post_worker_init(worker):
dashboard.startThreads()
worker_class = 'gthread' worker_class = 'gthread'
workers = 1 workers = 1
@@ -10,3 +16,11 @@ threads = 1
bind = f"{app_host}:{app_port}" bind = f"{app_host}:{app_port}"
daemon = True daemon = True
pidfile = './gunicorn.pid' pidfile = './gunicorn.pid'
wsgi_app = "dashboard:app"
accesslog = f"./log/access_{date}.log"
log_level = "debug"
capture_output = True
errorlog = f"./log/error_{date}.log"
print(f"[WGDashboard] WGDashboard w/ Gunicorn will be running on {bind}", flush=True)
print(f"[WGDashboard] Access log file is at {accesslog}", flush=True)
print(f"[WGDashboard] Error log file is at {errorlog}", flush=True)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

File diff suppressed because one or more lines are too long
+30 -30
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

+2310 -2
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -1,14 +1,16 @@
{ {
"name": "app", "name": "app",
"version": "0.0.0", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"build electron": "vite build && vite build --mode electron && cd ../../../../WGDashboard-Desktop && electron-builder",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@vuepic/vue-datepicker": "^9.0.1",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"@vueuse/shared": "^10.9.0", "@vueuse/shared": "^10.9.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
@@ -16,6 +18,7 @@
"bootstrap-icons": "^1.11.2", "bootstrap-icons": "^1.11.2",
"cidr-tools": "^7.0.4", "cidr-tools": "^7.0.4",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"electron-builder": "^24.13.3",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"i": "^0.3.7", "i": "^0.3.7",
"is-cidr": "^5.0.3", "is-cidr": "^5.0.3",
+1
View File
@@ -0,0 +1 @@
export const proxy = "http://wg.local:10086/"
+45 -8
View File
@@ -1,18 +1,45 @@
<script setup> <script setup>
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {computed, watch} from "vue";
const store = DashboardConfigurationStore(); const store = DashboardConfigurationStore();
store.initCrossServerConfiguration();
if (window.IS_WGDASHBOARD_DESKTOP){
store.IsElectronApp = true;
store.CrossServerConfiguration.Enable = true;
}
watch(store.CrossServerConfiguration, () => {
store.syncCrossServerConfiguration()
}, {
deep: true
});
const getActiveCrossServer = computed(() => {
if (store.ActiveServerConfiguration){
return store.CrossServerConfiguration.ServerList[store.ActiveServerConfiguration]
}
return undefined
})
</script> </script>
<template> <template>
<nav class="navbar bg-dark sticky-top" data-bs-theme="dark"> <nav class="navbar bg-dark sticky-top" data-bs-theme="dark">
<div class="container-fluid"> <div class="container-fluid d-flex text-body align-items-center">
<span class="navbar-brand mb-0 h1">WGDashboard</span> <span class="navbar-brand mb-0 h1">WGDashboard</span>
<small class="ms-auto text-muted" v-if="getActiveCrossServer !== undefined">
<i class="bi bi-server me-2"></i>{{getActiveCrossServer.host}}
</small>
<a role="button" class="navbarBtn text-body"
@click="store.ShowNavBar = !store.ShowNavBar"
style="line-height: 0; font-size: 2rem">
<i class="bi bi-list"></i></a>
</div> </div>
</nav> </nav>
<Suspense> <Suspense>
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition name="fade2" mode="out-in"> <Transition name="app" mode="out-in">
<Component :is="Component"></Component> <Component :is="Component"></Component>
</Transition> </Transition>
</RouterView> </RouterView>
@@ -22,14 +49,24 @@ const store = DashboardConfigurationStore();
<style scoped> <style scoped>
.app-enter-active, .app-enter-active,
.app-leave-active { .app-leave-active {
transition: all 0.3s ease-in-out; transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 0.9);
/*position: absolute;*/
/*padding-top: 50px*/
} }
.app-enter-from, .app-enter-from{
.app-leave-to { transform: translateY(20px);
transform: translateX(-30px);
opacity: 0; opacity: 0;
} }
.app-leave-to {
transform: translateY(-20px);
opacity: 0;
}
@media screen and (min-width: 768px) {
.navbarBtn{
display: none;
}
}
</style> </style>
@@ -14,7 +14,6 @@ export default {
data(){ data(){
return { return {
allowedIp: [], allowedIp: [],
availableIpSearchString: "", availableIpSearchString: "",
customAvailableIp: "", customAvailableIp: "",
allowedIpFormatError: false allowedIpFormatError: false
@@ -45,8 +44,16 @@ export default {
watch: { watch: {
customAvailableIp(){ customAvailableIp(){
this.allowedIpFormatError = false; this.allowedIpFormatError = false;
},
availableIp(){
if (this.availableIp !== undefined && this.availableIp.length > 0){
this.addAllowedIp(this.availableIp[0])
} }
} }
},
mounted() {
}
} }
</script> </script>
@@ -86,6 +86,7 @@ export default {
@setting="this.$emit('setting')" @setting="this.$emit('setting')"
@jobs="this.$emit('jobs')" @jobs="this.$emit('jobs')"
@refresh="this.$emit('refresh')" @refresh="this.$emit('refresh')"
@share="this.$emit('share')"
:Peer="Peer" :Peer="Peer"
v-if="this.subMenuOpened" v-if="this.subMenuOpened"
ref="target" ref="target"
@@ -108,7 +108,6 @@ export default {
</h3> </h3>
<h3 class="text-body mb-0">Add Peers</h3> <h3 class="text-body mb-0">Add Peers</h3>
</RouterLink> </RouterLink>
</div> </div>
<div class="d-flex flex-column gap-2"> <div class="d-flex flex-column gap-2">
<BulkAdd :saving="saving" :data="this.data" :availableIp="this.availableIp"></BulkAdd> <BulkAdd :saving="saving" :data="this.data" :availableIp="this.availableIp"></BulkAdd>
@@ -40,6 +40,7 @@ import PeerJobs from "@/components/configurationComponents/peerJobs.vue";
import PeerJobsAllModal from "@/components/configurationComponents/peerJobsAllModal.vue"; import PeerJobsAllModal from "@/components/configurationComponents/peerJobsAllModal.vue";
import PeerJobsLogsModal from "@/components/configurationComponents/peerJobsLogsModal.vue"; import PeerJobsLogsModal from "@/components/configurationComponents/peerJobsLogsModal.vue";
import {ref} from "vue"; import {ref} from "vue";
import PeerShareLinkModal from "@/components/configurationComponents/peerShareLinkModal.vue";
Chart.register( Chart.register(
ArcElement, ArcElement,
@@ -70,6 +71,7 @@ Chart.register(
export default { export default {
name: "peerList", name: "peerList",
components: { components: {
PeerShareLinkModal,
PeerJobsLogsModal, PeerJobsLogsModal,
PeerJobsAllModal, PeerJobs, PeerCreate, PeerQRCode, PeerSettings, PeerSearch, Peer, Line, Bar}, PeerJobsAllModal, PeerJobs, PeerCreate, PeerQRCode, PeerSettings, PeerSearch, Peer, Line, Bar},
setup(){ setup(){
@@ -131,6 +133,10 @@ export default {
}, },
peerScheduleJobsLogs: { peerScheduleJobsLogs: {
modalOpen: false modalOpen: false
},
peerShare:{
modalOpen: false,
selectedPeer: undefined
} }
} }
}, },
@@ -141,26 +147,21 @@ export default {
'$route': { '$route': {
immediate: true, immediate: true,
handler(){ handler(){
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval); clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
this.loading = true; this.loading = true;
let id = this.$route.params.id; let id = this.$route.params.id;
this.configurationInfo = []; this.configurationInfo = [];
this.configurationPeers = []; this.configurationPeers = [];
if (id){ if (id){
this.getPeers(id) this.getPeers(id)
console.log("Changed..")
this.setPeerInterval(); this.setPeerInterval();
} }
} }
}, },
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){ 'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
console.log("Changed?")
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval); clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
this.setPeerInterval(); this.setPeerInterval();
}, }
}, },
beforeRouteLeave(){ beforeRouteLeave(){
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval); clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
@@ -211,7 +212,8 @@ export default {
{ {
label: 'Data Sent', label: 'Data Sent',
data: [...this.historySentData.datasets[0].data, data: [...this.historySentData.datasets[0].data,
((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000).toFixed(4)], ((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000)
.toFixed(4)],
fill: false, fill: false,
borderColor: '#198754', borderColor: '#198754',
tension: 0 tension: 0
@@ -231,7 +233,8 @@ export default {
{ {
label: 'Data Received', label: 'Data Received',
data: [...this.historyReceiveData.datasets[0].data, data: [...this.historyReceiveData.datasets[0].data,
((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000).toFixed(4)], ((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000)
.toFixed(4)],
fill: false, fill: false,
borderColor: '#0d6efd', borderColor: '#0d6efd',
tension: 0 tension: 0
@@ -248,17 +251,24 @@ export default {
this.dashboardConfigurationStore.Peers.RefreshInterval = setInterval(() => { this.dashboardConfigurationStore.Peers.RefreshInterval = setInterval(() => {
this.getPeers() this.getPeers()
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval)) }, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
}, },
}, },
computed: { computed: {
configurationSummary(){ configurationSummary(){
return { const k = {
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length, connectedPeers: this.configurationPeers.filter(x => x.status === "running").length,
totalUsage: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b) : 0, totalUsage: this.configurationPeers.length > 0 ?
totalReceive: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b) : 0, this.configurationPeers.filter(x => !x.restricted)
totalSent: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b) : 0 .map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b).toFixed(4) : 0,
totalReceive: this.configurationPeers.length > 0 ?
this.configurationPeers.filter(x => !x.restricted)
.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b).toFixed(4) : 0,
totalSent: this.configurationPeers.length > 0 ?
this.configurationPeers.filter(x => !x.restricted)
.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b).toFixed(4) : 0
} }
return k
}, },
receiveData(){ receiveData(){
return this.historyReceiveData return this.historyReceiveData
@@ -392,7 +402,7 @@ export default {
</script> </script>
<template> <template>
<div v-if="!this.loading"> <div v-if="!this.loading" class="container-md">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div> <div>
<small CLASS="text-muted">CONFIGURATION</small> <small CLASS="text-muted">CONFIGURATION</small>
@@ -468,7 +478,7 @@ export default {
<div class="card-body d-flex"> <div class="card-body d-flex">
<div> <div>
<p class="mb-0 text-muted"><small>Total Usage</small></p> <p class="mb-0 text-muted"><small>Total Usage</small></p>
<strong class="h4">{{configurationSummary.totalUsage.toFixed(4)}} GB</strong> <strong class="h4">{{configurationSummary.totalUsage}} GB</strong>
</div> </div>
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i> <i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
</div> </div>
@@ -479,7 +489,7 @@ export default {
<div class="card-body d-flex"> <div class="card-body d-flex">
<div> <div>
<p class="mb-0 text-muted"><small>Total Received</small></p> <p class="mb-0 text-muted"><small>Total Received</small></p>
<strong class="h4 text-primary">{{configurationSummary.totalReceive.toFixed(4)}} GB</strong> <strong class="h4 text-primary">{{configurationSummary.totalReceive}} GB</strong>
</div> </div>
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i> <i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
</div> </div>
@@ -490,7 +500,7 @@ export default {
<div class="card-body d-flex"> <div class="card-body d-flex">
<div> <div>
<p class="mb-0 text-muted"><small>Total Sent</small></p> <p class="mb-0 text-muted"><small>Total Sent</small></p>
<strong class="h4 text-success">{{configurationSummary.totalSent.toFixed(4)}} GB</strong> <strong class="h4 text-success">{{configurationSummary.totalSent}} GB</strong>
</div> </div>
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i> <i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
</div> </div>
@@ -535,10 +545,7 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-3">
<!-- <div class="d-flex align-items-center gap-3 mb-2">-->
<!-- <h3>Peers</h3>-->
<!-- </div>-->
<PeerSearch <PeerSearch
@jobsAll="this.peerScheduleJobsAll.modalOpen = true" @jobsAll="this.peerScheduleJobsAll.modalOpen = true"
@jobLogs="this.peerScheduleJobsLogs.modalOpen = true" @jobLogs="this.peerScheduleJobsLogs.modalOpen = true"
@@ -548,7 +555,7 @@ export default {
:key="peer.id" :key="peer.id"
v-for="peer in this.searchPeers"> v-for="peer in this.searchPeers">
<Peer :Peer="peer" <Peer :Peer="peer"
@share="this.peerShare.selectedPeer = peer.id; this.peerShare.modalOpen = true;"
@refresh="this.getPeers()" @refresh="this.getPeers()"
@jobs="peerScheduleJobs.modalOpen = true; peerScheduleJobs.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)" @jobs="peerScheduleJobs.modalOpen = true; peerScheduleJobs.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"
@setting="peerSetting.modalOpen = true; peerSetting.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)" @setting="peerSetting.modalOpen = true; peerSetting.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"
@@ -595,6 +602,12 @@ export default {
> >
</PeerJobsLogsModal> </PeerJobsLogsModal>
</Transition> </Transition>
<Transition name="zoom">
<PeerShareLinkModal
v-if="this.peerShare.modalOpen"
@close="this.peerShare.modalOpen = false; this.peerShare.selectedPeer = undefined;"
:peer="this.configurationPeers.find(x => x.id === this.peerShare.selectedPeer)"></PeerShareLinkModal>
</Transition>
</div> </div>
</template> </template>
@@ -7,7 +7,6 @@ export default {
}, },
mounted() { mounted() {
QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfigData , (error) => { QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfigData , (error) => {
console.log(this.peerConfigData)
if (error) console.error(error) if (error) console.error(error)
}) })
} }
@@ -3,10 +3,12 @@ import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJ
import {ref} from "vue"; import {ref} from "vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchPost} from "@/utilities/fetch.js"; import {fetchPost} from "@/utilities/fetch.js";
import VueDatePicker from "@vuepic/vue-datepicker";
import dayjs from "dayjs";
export default { export default {
name: "schedulePeerJob", name: "schedulePeerJob",
components: {ScheduleDropdown}, components: {VueDatePicker, ScheduleDropdown},
props: { props: {
dropdowns: Array[Object], dropdowns: Array[Object],
pjob: Object, pjob: Object,
@@ -94,6 +96,11 @@ export default {
}) })
} }
this.$emit('delete') this.$emit('delete')
},
parseTime(modelData){
if(modelData){
this.job.Value = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
}
} }
}, },
} }
@@ -128,12 +135,26 @@ export default {
:data="this.job.Operator" :data="this.job.Operator"
@update="(value) => this.job.Operator = value" @update="(value) => this.job.Operator = value"
></ScheduleDropdown> ></ScheduleDropdown>
<input class="form-control form-control-sm form-control-dark rounded-3 flex-grow-1"
<VueDatePicker
:is24="true"
:min-date="new Date()"
:model-value="this.job.Value"
@update:model-value="this.parseTime" time-picker-inline
format="yyyy-MM-dd HH:mm:ss"
preview-format="yyyy-MM-dd HH:mm:ss"
:clearable="false"
:disabled="!edit" :disabled="!edit"
type="datetime-local"
v-if="this.job.Field === 'date'" v-if="this.job.Field === 'date'"
v-model="this.job.Value" :dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
style="width: auto"> />
<!-- <input class="form-control form-control-sm form-control-dark rounded-3 flex-grow-1"-->
<!-- :disabled="!edit"-->
<!-- type="datetime-local"-->
<!-- v-if="this.job.Field === 'date'"-->
<!-- v-model="this.job.Value"-->
<!-- style="width: auto">-->
<input class="form-control form-control-sm form-control-dark rounded-3 flex-grow-1" <input class="form-control form-control-sm form-control-dark rounded-3 flex-grow-1"
:disabled="!edit" :disabled="!edit"
v-else v-else
@@ -188,4 +209,12 @@ input:disabled{
background-color: rgba(13, 110, 253, 0.09); background-color: rgba(13, 110, 253, 0.09);
color: #0d6efd; color: #0d6efd;
} }
.dp__main{
width: auto;
flex-grow: 1;
--dp-input-padding: 2.5px 30px 2.5px 12px;
--dp-border-radius: 0.5rem;
}
</style> </style>
@@ -30,7 +30,9 @@ export default {
'60000': '1 Minutes' '60000': '1 Minutes'
}, },
searchString: "", searchString: "",
searchStringTimeout: undefined searchStringTimeout: undefined,
showDisplaySettings: false,
showMoreSettings: false
} }
}, },
methods: { methods: {
@@ -83,86 +85,144 @@ export default {
<template> <template>
<div class="mb-3"> <div class="mb-3">
<div class="d-flex gap-2 z-3"> <div class="d-flex gap-2 z-3 peerSearchContainer">
<RouterLink <RouterLink
to="create" to="create"
class="text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm"> class="text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm">
<i class="bi bi-plus-lg me-2"></i>Peer <i class="bi bi-plus-lg me-2"></i>Peer
</RouterLink> </RouterLink>
<!-- <RouterLink-->
<!-- to="jobs"-->
<!-- class="text-decoration-none btn btn-primary rounded-3 btn-sm">-->
<!-- <i class="bi bi-app-indicator me-2"></i>Jobs-->
<!-- </RouterLink>-->
<button class="btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm" <button class="btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm"
@click="this.downloadAllPeer()"> @click="this.downloadAllPeer()">
<i class="bi bi-download me-2"></i> Download All <i class="bi bi-download me-2"></i> Download All
</button> </button>
<div class="d-flex align-items-center ms-auto"> <div class="flex-grow-1 mt-3 mt-md-0">
<!-- <label class="d-flex me-2 text-muted" for="searchPeers"><i class="bi bi-search me-1"></i></label>--> <input class="form-control rounded-3 bg-secondary-subtle border-1 border-secondary-subtle shadow-sm w-100"
<input class="form-control rounded-3 bg-secondary-subtle border-1 border-secondary-subtle shadow-sm"
placeholder="Search..." placeholder="Search..."
id="searchPeers" id="searchPeers"
@keyup="this.debounce()" @keyup="this.debounce()"
v-model="this.searchString"> v-model="this.searchString">
</div> </div>
<div class="dropdown"> <button
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm" @click="this.showDisplaySettings = true"
type="button" data-bs-toggle="dropdown" aria-expanded="false"> class="btn text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
type="button" aria-expanded="false">
<i class="bi bi-filter-circle me-2"></i> <i class="bi bi-filter-circle me-2"></i>
Sort Display
</button> </button>
<ul class="dropdown-menu mt-2 shadow rounded-3"> <button class="btn text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
<li v-for="(value, key) in this.sort"> @click="this.showMoreSettings = true"
<a class="dropdown-item d-flex" role="button" @click="this.updateSort(key)"> type="button" aria-expanded="false">
<i class="bi bi-three-dots"></i>
</button>
<Transition name="zoom">
<div
v-if="this.showDisplaySettings"
class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll displayModal">
<div class="container-md d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal">
<div class="card rounded-3 shadow w-100">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
<h4 class="mb-0 fw-normal">Display
</h4>
<button type="button" class="btn-close ms-auto" @click="this.showDisplaySettings = false"></button>
</div>
<div class="card-body px-4 pb-4 d-flex gap-3 flex-column">
<div>
<p class="text-muted fw-bold mb-2"><small>Sort by</small></p>
<div class="list-group">
<a v-for="(value, key) in this.sort" class="list-group-item list-group-item-action d-flex" role="button" @click="this.updateSort(key)">
<span class="me-auto">{{value}}</span> <span class="me-auto">{{value}}</span>
<i class="bi bi-check text-primary" <i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_sort === key"></i> v-if="store.Configuration.Server.dashboard_sort === key"></i>
</a> </a>
</li>
</ul>
</div> </div>
<div class="dropdown"> </div>
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm" <div>
type="button" data-bs-toggle="dropdown" aria-expanded="false"> <p class="text-muted fw-bold mb-2"><small>Refresh interval</small></p>
<i class="bi bi-arrow-repeat me-2"></i>Refresh Interval <div class="list-group">
</button> <a v-for="(value, key) in this.interval"
<ul class="dropdown-menu shadow mt-2 rounded-3"> class="list-group-item list-group-item-action d-flex" role="button"
<li v-for="(value, key) in this.interval"> @click="this.updateRefreshInterval(key)">
<a class="dropdown-item d-flex" role="button" @click="updateRefreshInterval(key)">
<span class="me-auto">{{value}}</span> <span class="me-auto">{{value}}</span>
<i class="bi bi-check text-primary" <i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i> v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i>
</a></li>
</ul>
</div>
<div class="dropdown">
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots me-2"></i>More
</button>
<ul class="dropdown-menu shadow mt-2 rounded-3">
<li>
<h6 class="dropdown-header">Peer Jobs</h6>
</li>
<li>
<a role="button" class="dropdown-item" @click="this.$emit('jobsAll')">
All Active Jobs
</a> </a>
</li> </div>
<li> </div>
<a role="button" class="dropdown-item" @click="this.$emit('jobLogs')"> </div>
</div>
</div>
</div>
</div>
</Transition>
<Transition name="zoom">
<div
v-if="this.showMoreSettings"
class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll displayModal">
<div class="container-md d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal">
<div class="card rounded-3 shadow w-100">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
<h4 class="mb-0 fw-normal">Configuration Settings
</h4>
<button type="button" class="btn-close ms-auto" @click="this.showMoreSettings = false"></button>
</div>
<div class="card-body px-4 pb-4 d-flex gap-3 flex-column">
<div>
<p class="text-muted fw-bold mb-2"><small>Peer Jobs</small></p>
<div class="list-group">
<a class="list-group-item list-group-item-action d-flex" role="button"
@click="this.$emit('jobsAll')">
Active Jobs
</a>
<a class="list-group-item list-group-item-action d-flex" role="button"
@click="this.$emit('jobLogs')">
Logs Logs
</a> </a>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
.animation__fadeInDropdown{
animation-name: fadeInDropdown;
animation-duration: 0.2s;
animation-timing-function: cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
@keyframes fadeInDropdown{
0%{
opacity: 0;
filter: blur(3px);
transform: translateY(-60px);
}
100%{
opacity: 1;
filter: blur(0px);
transform: translateY(-40px);
}
}
.displayModal .dashboardModal{
width: 400px !important;
}
@media screen and (max-width: 768px) {
.peerSearchContainer{
flex-direction: column;
}
.peerSettingContainer .dashboardModal{
width: 100% !important;
}
}
</style> </style>
@@ -37,6 +37,21 @@ export default {
} }
this.$emit("refresh") this.$emit("refresh")
}) })
},
resetPeerData(type){
this.saving = true
fetchPost(`/api/resetPeerData/${this.$route.params.id}`, {
id: this.data.id,
type: type
}, (res) => {
this.saving = false;
if (res.status){
this.dashboardConfigurationStore.newMessage("Server", "Peer data usage reset successfully.", "success")
}else{
this.dashboardConfigurationStore.newMessage("Server", res.message, "danger")
}
this.$emit("refresh")
})
} }
}, },
beforeMount() { beforeMount() {
@@ -57,15 +72,15 @@ export default {
<div class="container d-flex h-100 w-100"> <div class="container d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal"> <div class="m-auto modal-dialog-centered dashboardModal">
<div class="card rounded-3 shadow flex-grow-1"> <div class="card rounded-3 shadow flex-grow-1">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4"> <div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
<h4 class="mb-0">Peer Settings</h4> <h4 class="mb-0">Peer Settings</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button> <button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div> </div>
<div class="card-body px-4 pb-4" v-if="this.data"> <div class="card-body px-4 pb-4" v-if="this.data">
<div class="d-flex flex-column gap-2 mb-4"> <div class="d-flex flex-column gap-2 mb-4">
<div> <div class="d-flex align-items-center">
<small class="text-muted">Public Key</small><br> <small class="text-muted">Public Key</small>
<small><samp>{{this.data.id}}</samp></small> <small class="ms-auto"><samp>{{this.data.id}}</samp></small>
</div> </div>
<div> <div>
<label for="peer_name_textbox" class="form-label"> <label for="peer_name_textbox" class="form-label">
@@ -121,8 +136,7 @@ export default {
v-model="this.data.DNS" v-model="this.data.DNS"
id="peer_DNS_textbox"> id="peer_DNS_textbox">
</div> </div>
<hr> <div class="accordion mt-3" id="peerSettingsAccordion">
<div class="accordion mt-2" id="peerSettingsAccordion">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button rounded-3 collapsed" type="button" <button class="accordion-button rounded-3 collapsed" type="button"
@@ -162,12 +176,37 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<hr>
<div class="d-flex gap-2 align-items-center">
<strong>Reset Data Usage</strong>
<div class="d-flex gap-2 ms-auto">
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
@click="this.resetPeerData('total')"
>
<i class="bi bi-arrow-down-up me-2"></i>
Total
</button>
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
@click="this.resetPeerData('receive')"
>
<i class="bi bi-arrow-down me-2"></i>
Received
</button>
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
@click="this.resetPeerData('sent')"
>
<i class="bi bi-arrow-up me-2"></i>
Sent
</button>
</div>
</div>
</div> </div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<button class="btn btn-secondary rounded-3 shadow" <button class="btn btn-secondary rounded-3 shadow"
@click="this.reset()" @click="this.reset()"
:disabled="!this.dataChanged || this.saving"> :disabled="!this.dataChanged || this.saving">
Reset <i class="bi bi-arrow-clockwise ms-2"></i> Revert <i class="bi bi-arrow-clockwise ms-2"></i>
</button> </button>
<button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow" <button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow"
@@ -93,8 +93,23 @@ export default {
set for this peer set for this peer
</small> </small>
</li> </li>
<li><hr class="dropdown-divider"></li>
</template> </template>
<template v-else>
<li class="d-flex" style="padding-left: var(--bs-dropdown-item-padding-x); padding-right: var(--bs-dropdown-item-padding-x);">
<a class="dropdown-item text-center px-0 rounded-3" role="button" @click="this.downloadPeer()">
<i class="me-auto bi bi-download"></i>
</a>
<a class="dropdown-item text-center px-0 rounded-3" role="button"
@click="this.downloadQRCode()">
<i class="me-auto bi bi-qr-code"></i>
</a>
<a class="dropdown-item text-center px-0 rounded-3" role="button" @click="this.$emit('share')">
<i class="me-auto bi bi-share"></i>
</a>
</li>
</template>
<li><hr class="dropdown-divider"></li>
<li> <li>
<a class="dropdown-item d-flex" role="button" <a class="dropdown-item d-flex" role="button"
@click="this.$emit('setting')" @click="this.$emit('setting')"
@@ -109,20 +124,7 @@ export default {
<i class="me-auto bi bi-app-indicator"></i> Schedule Jobs <i class="me-auto bi bi-app-indicator"></i> Schedule Jobs
</a> </a>
</li> </li>
<template v-if="this.Peer.private_key">
<li>
<a class="dropdown-item d-flex" role="button" @click="this.downloadPeer()">
<i class="me-auto bi bi-download"></i> Download
</a>
</li>
<li>
<a class="dropdown-item d-flex" role="button"
@click="this.downloadQRCode()"
>
<i class="me-auto bi bi-qr-code"></i> QR Code
</a>
</li>
</template>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li> <li>
@@ -0,0 +1,172 @@
<script>
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchPost} from "@/utilities/fetch.js";
import dayjs from "dayjs";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import VueDatePicker from '@vuepic/vue-datepicker';
export default {
name: "peerShareLinkModal",
props: {
peer: Object
},
components: {
VueDatePicker
},
data(){
return {
dataCopy: undefined,
loading: false
}
},
setup(){
const store = DashboardConfigurationStore();
return {store}
},
mounted() {
this.dataCopy = JSON.parse(JSON.stringify(this.peer.ShareLink)).at(0);
},
watch: {
'peer.ShareLink': {
deep: true,
handler(newVal, oldVal){
if (oldVal.length !== newVal.length){
this.dataCopy = JSON.parse(JSON.stringify(this.peer.ShareLink)).at(0);
}
}
}
},
methods: {
startSharing(){
this.loading = true;
fetchPost("/api/sharePeer/create", {
Configuration: this.peer.configuration.Name,
Peer: this.peer.id,
ExpireDate: dayjs().add(7, 'd').format("YYYY-MM-DD HH:mm:ss")
}, (res) => {
if (res.status){
this.peer.ShareLink = res.data;
this.dataCopy = res.data.at(0);
this.store.newMessage("Server", "Share link created successfully", "success")
}else{
this.store.newMessage("Server",
"Share link failed to create. Reason: " + res.message, "danger")
}
this.loading = false;
})
},
updateLinkExpireDate(){
fetchPost("/api/sharePeer/update", this.dataCopy, (res) => {
if (res.status){
this.dataCopy = res.data.at(0)
this.peer.ShareLink = res.data;
this.store.newMessage("Server", "Link expire date updated", "success")
}else{
this.store.newMessage("Server",
"Link expire date failed to update. Reason: " + res.message, "danger")
}
this.loading = false
});
},
stopSharing(){
this.loading = true;
this.dataCopy.ExpireDate = dayjs().format("YYYY-MM-DD HH:mm:ss")
this.updateLinkExpireDate()
},
parseTime(modelData){
if(modelData){
this.dataCopy.ExpireDate = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
}else{
this.dataCopy.ExpireDate = undefined
}
this.updateLinkExpireDate()
}
},
computed: {
getUrl(){
const crossServer = this.store.getActiveCrossServer();
if(crossServer){
return `${crossServer.host}/${this.$router.resolve(
{path: "/share", query: {"ShareID": this.dataCopy.ShareID}}).href}`
}
return window.location.origin
+ window.location.pathname
+ this.$router.resolve(
{path: "/share", query: {"ShareID": this.dataCopy.ShareID}}).href;
}
}
}
</script>
<template>
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
<div class="container d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 500px">
<div class="card rounded-3 shadow flex-grow-1">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
<h4 class="mb-0">Share Peer</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div>
<div class="card-body px-4 pb-4" v-if="this.peer.ShareLink">
<div v-if="!this.dataCopy">
<h6 class="mb-3 text-muted">
Currently the peer is not sharing
</h6>
<button
@click="this.startSharing()"
:disabled="this.loading"
class="w-100 btn bg-success-subtle text-success-emphasis border-1 border-success-subtle rounded-3 shadow-sm">
<span :class="{'animate__animated animate__flash animate__infinite animate__slower': this.loading}">
<i class="bi bi-send-fill me-2" ></i>
</span>
{{this.loading ? "Sharing...":"Start Sharing"}}
</button>
</div>
<div v-else>
<div class="d-flex gap-2 mb-4">
<i class="bi bi-link-45deg"></i>
<a :href="this.getUrl"
class="text-decoration-none" target="_blank">
{{ getUrl }}
</a>
</div>
<div class="d-flex flex-column gap-2 mb-3">
<small>
<i class="bi bi-calendar me-2"></i>
Expire Date
</small>
<VueDatePicker
:is24="true"
:min-date="new Date()"
:model-value="this.dataCopy.ExpireDate"
@update:model-value="this.parseTime" time-picker-inline
format="yyyy-MM-dd HH:mm:ss"
preview-format="yyyy-MM-dd HH:mm:ss"
:dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
/>
</div>
<button
@click="this.stopSharing()"
:disabled="this.loading"
class="w-100 btn bg-danger-subtle text-danger-emphasis border-1 border-danger-subtle rounded-3 shadow-sm">
<span :class="{'animate__animated animate__flash animate__infinite animate__slower': this.loading}">
<i class="bi bi-send-slash-fill me-2" ></i>
</span>
{{this.loading ? "Stop Sharing...":"Stop Sharing"}}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>
@@ -8,7 +8,6 @@ export default {
components: {ConfigurationCard}, components: {ConfigurationCard},
async setup(){ async setup(){
const wireguardConfigurationsStore = WireguardConfigurationsStore(); const wireguardConfigurationsStore = WireguardConfigurationsStore();
return {wireguardConfigurationsStore} return {wireguardConfigurationsStore}
}, },
data(){ data(){
@@ -19,20 +18,28 @@ export default {
async mounted() { async mounted() {
await this.wireguardConfigurationsStore.getConfigurations(); await this.wireguardConfigurationsStore.getConfigurations();
this.configurationLoaded = true; this.configurationLoaded = true;
this.wireguardConfigurationsStore.ConfigurationListInterval = setInterval(() => {
this.wireguardConfigurationsStore.getConfigurations()
}, 10000)
},
beforeUnmount() {
clearInterval(this.wireguardConfigurationsStore.ConfigurationListInterval)
} }
} }
</script> </script>
<template> <template>
<div class="mt-5"> <div class="mt-md-5 mt-3">
<div class="container"> <div class="container-md">
<div class="d-flex mb-4 "> <div class="d-flex mb-4 configurationListTitle">
<h3 class="text-body">WireGuard Configurations</h3> <h3 class="text-body d-flex">
<i class="bi bi-body-text me-2"></i>
<span>WireGuard Configurations</span></h3>
<RouterLink to="/new_configuration" class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto rounded-3"> <RouterLink to="/new_configuration" class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto rounded-3">
<i class="bi bi-plus-circle-fill me-2"></i> <i class="bi bi-plus-circle-fill me-2"></i>
Configuration Configuration
</RouterLink> </RouterLink>
</div> </div>
<Transition name="fade" mode="out-in"> <Transition name="fade" mode="out-in">
@@ -40,7 +47,7 @@ export default {
<p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0"> <p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0">
You don't have any WireGuard configurations yet. Please check the configuration folder or change it in "Settings". By default the folder is "/etc/wireguard". You don't have any WireGuard configurations yet. Please check the configuration folder or change it in "Settings". By default the folder is "/etc/wireguard".
</p> </p>
<div class="d-flex gap-3 flex-column" v-else> <div class="d-flex gap-3 flex-column mb-3" v-else>
<ConfigurationCard v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name" :c="c"></ConfigurationCard> <ConfigurationCard v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name" :c="c"></ConfigurationCard>
</div> </div>
</div> </div>
@@ -52,5 +59,18 @@ export default {
</template> </template>
<style scoped> <style scoped>
@media screen and (max-width: 768px) {
.configurationListTitle{
flex-direction: column;
gap: 0.5rem;
h3 span{
margin-left: auto !important;
}
.btn{
width: 100%;
}
}
}
</style> </style>
@@ -10,7 +10,6 @@ export default {
Status: Boolean, Status: Boolean,
PublicKey: String, PublicKey: String,
PrivateKey: String PrivateKey: String
} }
}, },
data(){ data(){
@@ -52,9 +51,24 @@ export default {
<i class="bi bi-chevron-right"></i> <i class="bi bi-chevron-right"></i>
</h6> </h6>
</RouterLink> </RouterLink>
<div class="card-footer d-flex align-items-center"> <div class="card-footer d-flex gap-2 flex-column">
<small class="me-2 text-muted"> <div class="row">
<strong>PUBLIC KEY</strong> <small class="col-6 col-md-3">
<i class="bi bi-arrow-down-up me-2"></i>{{c.DataUsage.Total > 0 ? c.DataUsage.Total.toFixed(4) : 0}} GB
</small>
<small class="text-primary-emphasis col-6 col-md-3">
<i class="bi bi-arrow-down me-2"></i>{{c.DataUsage.Receive > 0 ? c.DataUsage.Receive.toFixed(4) : 0}} GB
</small>
<small class="text-success-emphasis col-6 col-md-3">
<i class="bi bi-arrow-up me-2"></i>{{c.DataUsage.Sent > 0 ? c.DataUsage.Sent.toFixed(4) : 0}} GB
</small>
<small class="text-md-end col-6 col-md-3">
<span class="dot me-2" :class="{active: c.ConnectedPeers > 0}"></span>{{c.ConnectedPeers}} Peers
</small>
</div>
<div class="d-flex align-items-center gap-2">
<small class="text-muted">
<strong style="word-break: keep-all">Public Key</strong>
</small> </small>
<small class="mb-0 d-block d-lg-inline-block "> <small class="mb-0 d-block d-lg-inline-block ">
<samp style="line-break: anywhere">{{c.PublicKey}}</samp> <samp style="line-break: anywhere">{{c.PublicKey}}</samp>
@@ -76,6 +90,7 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
+96 -24
View File
@@ -2,6 +2,7 @@
import {wgdashboardStore} from "@/stores/wgdashboardStore.js"; import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"; import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchGet} from "@/utilities/fetch.js";
export default { export default {
name: "navbar", name: "navbar",
@@ -9,53 +10,97 @@ export default {
const wireguardConfigurationsStore = WireguardConfigurationsStore(); const wireguardConfigurationsStore = WireguardConfigurationsStore();
const dashboardConfigurationStore = DashboardConfigurationStore(); const dashboardConfigurationStore = DashboardConfigurationStore();
return {wireguardConfigurationsStore, dashboardConfigurationStore} return {wireguardConfigurationsStore, dashboardConfigurationStore}
},
data(){
return {
updateAvailable: false,
updateMessage: "Checking for update...",
updateUrl: ""
}
},
mounted() {
fetchGet("/api/getDashboardUpdate", {}, (res) => {
if (res.status){
if (res.data){
this.updateAvailable = true
this.updateUrl = res.data
}
this.updateMessage = res.message
}else{
this.updateMessage = "Failed to check available update"
console.log(`Failed to get update: ${res.message}`)
}
})
} }
} }
</script> </script>
<template> <template>
<div class="col-md-3 col-lg-2 d-md-block p-3" style="height: calc(-50px + 100vh);"> <div class="col-md-3 col-lg-2 d-md-block p-3 navbar-container"
:class="{active: this.dashboardConfigurationStore.ShowNavBar}"
:data-bs-theme="dashboardConfigurationStore.Configuration.Server.dashboard_theme"
style="height: calc(-50px + 100vh);">
<nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow overflow-y-scroll" > <nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow overflow-y-scroll" >
<div class="sidebar-sticky pt-3"> <div class="sidebar-sticky pt-3">
<ul class="nav flex-column"> <ul class="nav flex-column px-2">
<li class="nav-item"> <li class="nav-item">
<RouterLink class="nav-link" to="/" exact-active-class="active">Home</RouterLink></li> <RouterLink class="nav-link rounded-3"
to="/" exact-active-class="active">
<i class="bi bi-house me-2"></i>
Home</RouterLink></li>
<li class="nav-item"> <li class="nav-item">
<RouterLink class="nav-link" to="/settings" <RouterLink class="nav-link rounded-3" to="/settings"
exact-active-class="active">Settings</RouterLink></li> exact-active-class="active">
<i class="bi bi-gear me-2"></i>
Settings</RouterLink></li>
</ul> </ul>
<hr> <hr class="text-body">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted text-center">
<span>Configurations</span> <i class="bi bi-body-text me-2"></i>
Configurations
</h6> </h6>
<ul class="nav flex-column"> <ul class="nav flex-column px-2">
<li class="nav-item"> <li class="nav-item">
<RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link" <RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link rounded-3"
active-class="active" active-class="active"
v-for="c in this.wireguardConfigurationsStore.Configurations"> v-for="c in this.wireguardConfigurationsStore.Configurations">
<samp>{{c.Name}}</samp> <span class="dot me-2" :class="{active: c.Status}"></span>
{{c.Name}}
</RouterLink> </RouterLink>
</li> </li>
</ul> </ul>
<hr> <hr class="text-body">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted text-center">
<span>Tools</span> <i class="bi bi-tools me-2"></i>
Tools
</h6> </h6>
<ul class="nav flex-column"> <ul class="nav flex-column px-2">
<li class="nav-item"> <li class="nav-item">
<RouterLink to="/ping" class="nav-link" active-class="active">Ping</RouterLink></li> <RouterLink to="/ping" class="nav-link rounded-3" active-class="active">Ping</RouterLink></li>
<li class="nav-item"> <li class="nav-item">
<RouterLink to="/traceroute" class="nav-link" active-class="active">Traceroute</RouterLink> <RouterLink to="/traceroute" class="nav-link rounded-3" active-class="active">Traceroute</RouterLink>
</li> </li>
</ul> </ul>
<hr> <hr class="text-body">
<ul class="nav flex-column"> <ul class="nav flex-column px-2">
<li class="nav-item"><a class="nav-link text-danger" @click="this.dashboardConfigurationStore.signOut()" role="button" style="font-weight: bold">Sign Out</a></li> <li class="nav-item"><a class="nav-link text-danger rounded-3"
</ul> @click="this.dashboardConfigurationStore.signOut()"
<ul class="nav flex-column"> role="button" style="font-weight: bold">
<li class="nav-item"><a href="https://github.com/donaldzou/WGDashboard/releases/tag/"><small class="nav-link text-muted"></small></a></li> <i class="bi bi-box-arrow-left me-2"></i>
Sign Out</a>
</li>
<li class="nav-item" style="font-size: 0.8rem">
<a :href="this.updateUrl" v-if="this.updateAvailable" class="text-decoration-none" target="_blank">
<small class="nav-link text-muted rounded-3" >
{{ this.updateMessage }}
</small>
</a>
<small class="nav-link text-muted" v-else>
{{ this.updateMessage }}
</small>
</li>
</ul> </ul>
</div> </div>
</nav> </nav>
</div> </div>
@@ -64,5 +109,32 @@ export default {
</template> </template>
<style scoped> <style scoped>
@media screen and (max-width: 768px) {
.navbar-container{
position: absolute;
z-index: 1000;
animation-duration: 0.4s;
animation-fill-mode: both;
display: none;
animation-timing-function: cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.navbar-container.active{
animation-direction: normal;
display: block !important;
animation-name: zoomInFade
}
}
@keyframes zoomInFade {
0%{
opacity: 0;
transform: translateY(60px);
filter: blur(3px);
}
100%{
opacity: 1;
transform: translateY(0px);
filter: blur(0px);
}
}
</style> </style>
@@ -0,0 +1,63 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "accountSettingsMFA",
setup(){
const store = DashboardConfigurationStore();
const uuid = `input_${v4()}`;
return {store, uuid};
},
data(){
return {
status: false
}
},
mounted() {
this.status = this.store.Configuration.Account["enable_totp"]
},
methods: {
async resetMFA(){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Account",
key: "totp_verified",
value: "false"
}, async (res) => {
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Account",
key: "enable_totp",
value: "false"
}, (res) => {
if (res.status){
this.$router.push("/2FASetup")
}
})
})
}
}
}
</script>
<template>
<div>
<div class="d-flex align-items-center">
<strong>Multi-Factor Authentication</strong>
<div class="form-check form-switch ms-3">
<input class="form-check-input" type="checkbox"
v-model="this.status"
role="switch" id="allowMFAKeysSwitch">
</div>
<button class="btn bg-warning-subtle text-warning-emphasis border-1 border-warning-subtle ms-auto rounded-3 shadow-sm"
v-if="this.status" @click="this.resetMFA()">
<i class="bi bi-shield-lock-fill me-2"></i>
{{this.store.Configuration.Account["totp_verified"] ? "Reset" : "Setup" }} MFA
</button>
</div>
</div>
</template>
<style scoped>
</style>
@@ -65,7 +65,7 @@ export default {
<div class="card mb-4 shadow rounded-3"> <div class="card mb-4 shadow rounded-3">
<div class="card-header d-flex"> <div class="card-header d-flex">
API Keys API Keys
<div class="form-check form-switch ms-auto"> <div class="form-check form-switch ms-auto" v-if="!this.store.getActiveCrossServer()">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
v-model="this.value" v-model="this.value"
@change="this.toggleDashboardAPIKeys()" @change="this.toggleDashboardAPIKeys()"
@@ -78,6 +78,7 @@ export default {
<div class="card-body position-relative d-flex flex-column gap-2" v-if="this.value"> <div class="card-body position-relative d-flex flex-column gap-2" v-if="this.value">
<button class="ms-auto btn bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle rounded-3 shadow-sm" <button class="ms-auto btn bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle rounded-3 shadow-sm"
@click="this.newDashboardAPIKey = true" @click="this.newDashboardAPIKey = true"
v-if="!this.store.getActiveCrossServer()"
> >
<i class="bi bi-key me-2"></i> Create <i class="bi bi-key me-2"></i> Create
</button> </button>
@@ -35,26 +35,31 @@ export default {
<template> <template>
<div class="card rounded-3 shadow-sm"> <div class="card rounded-3 shadow-sm">
<div class="card-body d-flex gap-3 align-items-center" v-if="!this.confirmDelete"> <div class="card-body d-flex gap-3 align-items-center apiKey-card-body" v-if="!this.confirmDelete">
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<small class="text-muted">Key</small>{{this.apiKey.Key}} <small class="text-muted">Key</small>
<span style="word-break: break-all">{{this.apiKey.Key}}</span>
</div> </div>
<div class="d-flex align-items-center gap-2 ms-auto"> <div class="d-flex align-items-center gap-2 ms-auto">
<small class="text-muted">Expire At</small> <small class="text-muted">Expire At</small>
{{this.apiKey.ExpiredAt ? this.apiKey.ExpiredAt : 'Never'}} {{this.apiKey.ExpiredAt ? this.apiKey.ExpiredAt : 'Never'}}
</div> </div>
<a role="button" class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3" @click="this.confirmDelete = true"> <a role="button" class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3"
v-if="!this.store.getActiveCrossServer()"
@click="this.confirmDelete = true">
<i class="bi bi-trash-fill"></i> <i class="bi bi-trash-fill"></i>
</a> </a>
</div> </div>
<div v-else class="card-body d-flex gap-3 align-items-center justify-content-end"> <div v-else class="card-body d-flex gap-3 align-items-center justify-content-end"
v-if="!this.store.getActiveCrossServer()">
Are you sure to delete this API key? Are you sure to delete this API key?
<a role="button" class="btn btn-sm bg-success-subtle text-success-emphasis rounded-3" <a role="button" class="btn btn-sm bg-success-subtle text-success-emphasis rounded-3"
@click="this.deleteAPIKey()" @click="this.deleteAPIKey()"
> >
<i class="bi bi-check-lg"></i> <i class="bi bi-check-lg"></i>
</a> </a>
<a role="button" class="btn btn-sm bg-secondary-subtle text-secondary-emphasis rounded-3" @click="this.confirmDelete = false"> <a role="button" class="btn btn-sm bg-secondary-subtle text-secondary-emphasis rounded-3"
@click="this.confirmDelete = false">
<i class="bi bi-x-lg"></i> <i class="bi bi-x-lg"></i>
</a> </a>
</div> </div>
@@ -62,5 +67,23 @@ export default {
</template> </template>
<style scoped> <style scoped>
@media screen and (max-width: 992px) {
.apiKey-card-body{
flex-direction: column !important;
align-items: start !important;
div.ms-auto{
margin-left: 0 !important;
}
div{
width: 100%;
align-items: start !important;
}
small{
margin-right: auto;
}
}
}
</style> </style>
@@ -2,13 +2,15 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import {fetchPost} from "@/utilities/fetch.js"; import {fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import VueDatePicker from "@vuepic/vue-datepicker";
export default { export default {
name: "newDashboardAPIKey", name: "newDashboardAPIKey",
components: {VueDatePicker},
data(){ data(){
return{ return{
newKeyData:{ newKeyData:{
ExpiredAt: dayjs().add(1, 'd').format("YYYY-MM-DDTHH:mm:ss"), ExpiredAt: dayjs().add(7, 'd').format("YYYY-MM-DD HH:mm:ss"),
neverExpire: false neverExpire: false
}, },
submitting: false submitting: false
@@ -39,6 +41,13 @@ export default {
fixDate(date){ fixDate(date){
console.log(dayjs(date).format("YYYY-MM-DDTHH:mm:ss")) console.log(dayjs(date).format("YYYY-MM-DDTHH:mm:ss"))
return dayjs(date).format("YYYY-MM-DDTHH:mm:ss") return dayjs(date).format("YYYY-MM-DDTHH:mm:ss")
},
parseTime(modelData){
if(modelData){
this.newKeyData.ExpiredAt = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
}else{
this.newKeyData.ExpiredAt = undefined
}
} }
} }
} }
@@ -49,16 +58,23 @@ export default {
style="background-color: #00000060; backdrop-filter: blur(3px)"> style="background-color: #00000060; backdrop-filter: blur(3px)">
<div class="card m-auto rounded-3 mt-5"> <div class="card m-auto rounded-3 mt-5">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0"> <div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
Create API Key <h6 class="mb-0">Create API Key</h6>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button> <button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div> </div>
<div class="card-body d-flex gap-2 p-4 flex-column"> <div class="card-body d-flex gap-2 p-4 flex-column">
<small class="text-muted">When should this API Key expire?</small> <small class="text-muted">When should this API Key expire?</small>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<input class="form-control" type="datetime-local" <VueDatePicker
@change="this.newKeyData.ExpiredAt = this.fixDate(this.newKeyData.ExpiredAt)" :is24="true"
:min-date="new Date()"
:model-value="this.newKeyData.ExpiredAt"
@update:model-value="this.parseTime" time-picker-inline
format="yyyy-MM-dd HH:mm:ss"
preview-format="yyyy-MM-dd HH:mm:ss"
:clearable="false"
:disabled="this.newKeyData.neverExpire || this.submitting" :disabled="this.newKeyData.neverExpire || this.submitting"
v-model="this.newKeyData.ExpiredAt"> :dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
/>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
@@ -1,15 +1,17 @@
<script> <script>
import {fetchGet, fetchPost} from "@/utilities/fetch.js"; import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import QRCode from "qrcode"; import QRCode from "qrcode";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default { export default {
name: "totp", name: "totp",
async setup(){ async setup(){
const store = DashboardConfigurationStore();
let l = "" let l = ""
await fetchGet("/api/Welcome_GetTotpLink", {}, (res => { await fetchGet("/api/Welcome_GetTotpLink", {}, (res => {
if (res.status) l = res.data; if (res.status) l = res.data;
})); }));
return {l} return {l, store}
}, },
mounted() { mounted() {
if (this.l) { if (this.l) {
@@ -58,7 +60,12 @@ export default {
</script> </script>
<template> <template>
<div class="mb-3"> <div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
:data-bs-theme="this.store.Configuration.Server.dashboard_theme">
<div class="m-auto text-body" style="width: 500px">
<div class="d-flex flex-column">
<div>
<h1 class="dashboardLogo display-4">Multi-Factor Authentication</h1>
<p class="mb-2"><small class="text-muted">1. Please scan the following QR Code to generate TOTP</small></p> <p class="mb-2"><small class="text-muted">1. Please scan the following QR Code to generate TOTP</small></p>
<canvas id="qrcode" class="rounded-3 mb-2"></canvas> <canvas id="qrcode" class="rounded-3 mb-2"></canvas>
<div class="p-3 bg-body-secondary rounded-3 border mb-3"> <div class="p-3 bg-body-secondary rounded-3 border mb-3">
@@ -66,7 +73,7 @@ export default {
</p><a :href="this.l"><code style="line-break: anywhere">{{this.l}}</code></a> </p><a :href="this.l"><code style="line-break: anywhere">{{this.l}}</code></a>
</div> </div>
<label for="totp" class="mb-2"><small class="text-muted">2. Enter the TOTP generated by your authenticator to verify</small></label> <label for="totp" class="mb-2"><small class="text-muted">2. Enter the TOTP generated by your authenticator to verify</small></label>
<div class="form-group"> <div class="form-group mb-2">
<input class="form-control text-center totp" <input class="form-control text-center totp"
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code" id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
v-model="this.totp" v-model="this.totp"
@@ -79,7 +86,32 @@ export default {
TOTP verified! TOTP verified!
</div> </div>
</div> </div>
<div class="alert alert-warning rounded-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i> If you ever lost your TOTP and can't login, please follow instruction on
<a href="https://github.com/donaldzou/WGDashboard" target="_blank">readme.md</a> to reset.
</div> </div>
</div>
<hr>
<div class="d-flex gap-3 mt-5 flex-column">
<RouterLink
to="/"
v-if="!this.verified"
class="btn bg-secondary-subtle text-secondary-emphasis
rounded-3
flex-grow-1 btn-lg border-1 border-secondary-subtle shadow d-flex">
I don't need MFA <i class="bi bi-chevron-right ms-auto"></i>
</RouterLink>
<RouterLink
to="/"
v-else class="btn btn-dark btn-lg d-flex btn-brand shadow align-items-center flex-grow-1 rounded-3">
Complete <i class="bi bi-chevron-right ms-auto"></i>
</RouterLink>
</div>
</div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
@@ -0,0 +1,165 @@
<script>
import dayjs from "dayjs";
export default {
name: "RemoteServer",
props: {
server: Object
},
data(){
return{
active: false,
startTime: undefined,
endTime: undefined,
errorMsg: "",
refreshing: false
}
},
methods: {
async handshake(){
this.active = false;
if (this.server.host && this.server.apiKey){
this.refreshing = true;
this.startTime = undefined;
this.endTime = undefined;
this.startTime = dayjs()
await fetch(`${this.server.host}/api/handshake`, {
headers: {
"content-type": "application/json",
"wg-dashboard-apikey": this.server.apiKey
},
method: "GET",
signal: AbortSignal.timeout(5000)
}).then(res => {
if (res.status === 200){
return res.json()
}
throw new Error(res.statusText)
}).then(() => {
this.endTime = dayjs()
this.active = true;
}).catch((res) => {
this.active = false;
this.errorMsg = res;
});
this.refreshing = false;
}
},
async connect(){
await fetch(`${this.server.host}/api/authenticate`, {
headers: {
"content-type": "application/json",
"wg-dashboard-apikey": this.server.apiKey
},
body: JSON.stringify({
host: window.location.hostname
}),
method: "POST",
signal: AbortSignal.timeout(5000),
}).then(res => res.json()).then(res => {
this.$emit("setActiveServer")
this.$router.push('/')
})
}
},
mounted() {
this.handshake()
},
computed: {
getHandshakeTime(){
if (this.startTime && this.endTime){
return `${dayjs().subtract(this.startTime).millisecond()}ms`
}else{
if (this.refreshing){
return `Pinging...`
}
return this.errorMsg ? this.errorMsg : "N/A"
}
}
}
}
</script>
<template>
<div class="card rounded-3">
<div class="card-body">
<div class="d-flex gap-3 w-100 remoteServerContainer">
<div class="d-flex gap-3 align-items-center flex-grow-1">
<i class="bi bi-server"></i>
<input class="form-control form-control-sm"
@blur="this.handshake()"
v-model="this.server.host"
type="url">
</div>
<div class="d-flex gap-3 align-items-center flex-grow-1">
<i class="bi bi-key-fill"></i>
<input class="form-control form-control-sm"
@blur="this.handshake()"
v-model="this.server.apiKey"
type="text">
</div>
<div class="d-flex gap-2 button-group">
<button
@click="this.$emit('delete')"
class="ms-auto btn btn-sm bg-danger-subtle text-danger-emphasis border-1 border-danger-subtle">
<i class="bi bi-trash"></i>
</button>
<button
@click="this.connect()"
:class="{disabled: !this.active}"
class="ms-auto btn btn-sm bg-success-subtle text-success-emphasis border-1 border-success-subtle">
<i class="bi bi-arrow-right-circle"></i>
</button>
</div>
</div>
</div>
<div class="card-footer gap-2 d-flex align-items-center">
<span class="dot ms-0 me-2" :class="[this.active ? 'active':'inactive']"></span>
<small>{{this.getHandshakeTime}}</small>
<div class="spin ms-auto text-primary-emphasis" v-if="this.refreshing">
<i class="bi bi-arrow-clockwise"></i>
</div>
<a role="button"
v-else
@click="this.handshake()"
class="text-primary-emphasis text-decoration-none ms-auto disabled">
<i class="bi bi-arrow-clockwise me"></i>
</a>
</div>
</div>
</template>
<style scoped>
.dot.inactive{
background-color: #dc3545;
box-shadow: 0 0 0 0.2rem #dc354545;
}
.spin{
animation: spin 1s infinite cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
@keyframes spin {
0%{
transform: rotate(0deg);
}
100%{
transform: rotate(360deg);
}
}
@media screen and (max-width: 768px) {
.remoteServerContainer{
flex-direction: column;
}
.remoteServerContainer .button-group button{
width: 100%;
}
}
</style>
@@ -0,0 +1,40 @@
<script>
import RemoteServer from "@/components/signInComponents/RemoteServer.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "RemoteServerList",
setup(){
const store = DashboardConfigurationStore();
return {store}
},
components: {RemoteServer}
}
</script>
<template>
<div class="w-100 mt-3">
<div class="d-flex align-items-center mb-3">
<h5 class="mb-0">Server List</h5>
<button
@click="this.store.addCrossServerConfiguration()"
class="btn bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle shadow-sm ms-auto">
<i class="bi bi-plus-circle-fill me-2"></i>Server
</button>
</div>
<div class="w-100 d-flex gap-3 flex-column p-3 border border-1 border-secondary-subtle rounded-3"
style="height: 400px; overflow-y: scroll">
<RemoteServer v-for="(server, key) in this.store.CrossServerConfiguration.ServerList"
@setActiveServer="this.store.setActiveCrossServer(key)"
@delete="this.store.deleteCrossServerConfiguration(key)"
:key="key"
:server="server"></RemoteServer>
<h6 class="text-muted m-auto" v-if="Object.keys(this.store.CrossServerConfiguration.ServerList).length === 0">
Click<i class="bi bi-plus-circle-fill mx-1"></i>to add your server</h6>
</div>
</div>
</template>
<style scoped>
</style>
+1
View File
@@ -3,6 +3,7 @@ import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.js' import 'bootstrap/dist/js/bootstrap.js'
import 'bootstrap-icons/font/bootstrap-icons.css' import 'bootstrap-icons/font/bootstrap-icons.css'
import 'animate.css/animate.compat.css' import 'animate.css/animate.compat.css'
import '@vuepic/vue-datepicker/dist/main.css'
import {createApp, markRaw} from 'vue' import {createApp, markRaw} from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
+28 -6
View File
@@ -4,20 +4,18 @@ import Index from "@/views/index.vue"
import Signin from "@/views/signin.vue"; import Signin from "@/views/signin.vue";
import ConfigurationList from "@/components/configurationList.vue"; import ConfigurationList from "@/components/configurationList.vue";
import {fetchGet} from "@/utilities/fetch.js"; import {fetchGet} from "@/utilities/fetch.js";
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import Settings from "@/views/settings.vue"; import Settings from "@/views/settings.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"; import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Setup from "@/views/setup.vue"; import Setup from "@/views/setup.vue";
import NewConfiguration from "@/views/newConfiguration.vue"; import NewConfiguration from "@/views/newConfiguration.vue";
import Configuration from "@/views/configuration.vue"; import Configuration from "@/views/configuration.vue";
import PeerSettings from "@/components/configurationComponents/peerSettings.vue";
import PeerList from "@/components/configurationComponents/peerList.vue"; import PeerList from "@/components/configurationComponents/peerList.vue";
import PeerCreate from "@/components/configurationComponents/peerCreate.vue"; import PeerCreate from "@/components/configurationComponents/peerCreate.vue";
import RestrictedPeers from "@/components/configurationComponents/restrictedPeers.vue";
import Ping from "@/views/ping.vue"; import Ping from "@/views/ping.vue";
import Traceroute from "@/views/traceroute.vue"; import Traceroute from "@/views/traceroute.vue";
import PeerJobs from "@/components/configurationComponents/peerJobs.vue"; import Totp from "@/components/setupComponent/totp.vue";
import Share from "@/views/share.vue";
const checkAuth = async () => { const checkAuth = async () => {
let result = false let result = false
@@ -30,6 +28,7 @@ const checkAuth = async () => {
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: [ routes: [
{ {
name: "Index", name: "Index",
path: '/', path: '/',
@@ -90,7 +89,6 @@ const router = createRouter({
path: 'create', path: 'create',
component: PeerCreate component: PeerCreate
}, },
] ]
}, },
@@ -105,8 +103,22 @@ const router = createRouter({
{ {
path: '/welcome', component: Setup, path: '/welcome', component: Setup,
meta: { meta: {
requiresAuth: true requiresAuth: true,
title: "Welcome to WGDashboard"
}, },
},
{
path: '/2FASetup', component: Totp,
meta: {
requiresAuth: true,
title: "Multi-Factor Authentication Setup"
},
},
{
path: '/share', component: Share,
meta: {
title: "Share"
}
} }
] ]
}); });
@@ -124,8 +136,10 @@ router.beforeEach(async (to, from, next) => {
}else{ }else{
document.title = "WGDashboard" document.title = "WGDashboard"
} }
dashboardConfigurationStore.ShowNavBar = false;
if (to.meta.requiresAuth){ if (to.meta.requiresAuth){
if (!dashboardConfigurationStore.getActiveCrossServer()){
if (cookie.getCookie("authToken") && await checkAuth()){ if (cookie.getCookie("authToken") && await checkAuth()){
await dashboardConfigurationStore.getConfiguration() await dashboardConfigurationStore.getConfiguration()
if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){ if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){
@@ -136,6 +150,14 @@ router.beforeEach(async (to, from, next) => {
}else{ }else{
dashboardConfigurationStore.Redirect = to; dashboardConfigurationStore.Redirect = to;
next("/signin") next("/signin")
dashboardConfigurationStore.newMessage("WGDashboard", "Session Ended", "warning")
}
}else{
await dashboardConfigurationStore.getConfiguration()
if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){
await wireguardConfigurationsStore.getConfigurations();
}
next()
} }
}else { }else {
next(); next();
@@ -10,23 +10,69 @@ export const DashboardConfigurationStore = defineStore('DashboardConfigurationSt
Peers: { Peers: {
Selecting: false, Selecting: false,
RefreshInterval: undefined RefreshInterval: undefined
} },
CrossServerConfiguration:{
Enable: false,
ServerList: {}
},
ActiveServerConfiguration: undefined,
IsElectronApp: false,
ShowNavBar: false
}), }),
actions: { actions: {
initCrossServerConfiguration(){
const currentConfiguration = localStorage.getItem('CrossServerConfiguration');
if (localStorage.getItem("ActiveCrossServerConfiguration") !== null){
this.ActiveServerConfiguration = localStorage.getItem("ActiveCrossServerConfiguration");
}
if (currentConfiguration === null){
window.localStorage.setItem('CrossServerConfiguration', JSON.stringify(this.CrossServerConfiguration))
}else{
this.CrossServerConfiguration = JSON.parse(currentConfiguration)
}
},
syncCrossServerConfiguration(){
window.localStorage.setItem('CrossServerConfiguration', JSON.stringify(this.CrossServerConfiguration))
},
addCrossServerConfiguration(){
this.CrossServerConfiguration.ServerList[v4().toString()] = {host: "", apiKey: "", active: false}
},
deleteCrossServerConfiguration(key){
delete this.CrossServerConfiguration.ServerList[key];
},
getActiveCrossServer(){
const key = localStorage.getItem('ActiveCrossServerConfiguration');
if (key !== null){
return this.CrossServerConfiguration.ServerList[key]
}
return undefined
},
setActiveCrossServer(key){
this.ActiveServerConfiguration = key;
localStorage.setItem('ActiveCrossServerConfiguration', key)
},
removeActiveCrossServer(){
this.ActiveServerConfiguration = undefined;
localStorage.removeItem('ActiveCrossServerConfiguration')
},
async getConfiguration(){ async getConfiguration(){
await fetchGet("/api/getDashboardConfiguration", {}, (res) => { await fetchGet("/api/getDashboardConfiguration", {}, (res) => {
if (res.status) this.Configuration = res.data if (res.status) this.Configuration = res.data
}); });
}, },
async updateConfiguration(){ // async updateConfiguration(){
await fetchPost("/api/updateDashboardConfiguration", { // await fetchPost("/api/updateDashboardConfiguration", {
DashboardConfiguration: this.Configuration // DashboardConfiguration: this.Configuration
}, (res) => { // }, (res) => {
console.log(res) // console.log(res)
}) // })
}, // },
async signOut(){ async signOut(){
await fetchGet("/api/signout", {}, (res) => { await fetchGet("/api/signout", {}, (res) => {
this.removeActiveCrossServer();
this.$router.go('/signin') this.$router.go('/signin')
}); });
}, },
@@ -6,6 +6,7 @@ export const WireguardConfigurationsStore = defineStore('WireguardConfigurations
state: () => ({ state: () => ({
Configurations: undefined, Configurations: undefined,
searchString: "", searchString: "",
ConfigurationListInterval: undefined,
PeerScheduleJobs: { PeerScheduleJobs: {
dropdowns: { dropdowns: {
Field: [ Field: [
+65 -21
View File
@@ -1,31 +1,75 @@
export const fetchGet = async (url, params=undefined, callback=undefined) => { import router from "@/router/index.js";
const urlSearchParams = new URLSearchParams(params); import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
await fetch(`${url}?${urlSearchParams.toString()}`, {
headers: { const getHeaders = () => {
let headers = {
"content-type": "application/json" "content-type": "application/json"
} }
const store = DashboardConfigurationStore();
const apiKey = store.getActiveCrossServer();
if (apiKey){
headers['wg-dashboard-apikey'] = apiKey.apiKey
}
return headers
}
const getUrl = (url) => {
const store = DashboardConfigurationStore();
const apiKey = store.getActiveCrossServer();
if (apiKey){
return `${apiKey.host}${url}`
}
// console.log("URL fetching: ", import.meta.env.MODE === 'development' ? url
// : `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`)
return import.meta.env.MODE === 'development' ? url
: `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`
}
export const fetchGet = async (url, params=undefined, callback=undefined) => {
const urlSearchParams = new URLSearchParams(params);
await fetch(`${getUrl(url)}?${urlSearchParams.toString()}`, {
headers: getHeaders()
}) })
.then(x => x.json()) .then((x) => {
.then(x => callback ? callback(x) : undefined) const store = DashboardConfigurationStore();
.catch(x => { if (!x.ok){
// let router = useRouter() if (x.status !== 200){
// if (x.status === 401){ if (x.status === 401){
// router.push('/signin')
// } store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
router.push({path: '/signin'})
}) })
} }
export const fetchPost = async (url, body, callback) => { export const fetchPost = async (url, body, callback) => {
await fetch(`${url}`, { await fetch(`${getUrl(url)}`, {
headers: { headers: getHeaders(),
"content-type": "application/json"
},
method: "POST", method: "POST",
body: JSON.stringify(body) body: JSON.stringify(body)
}) }).then((x) => {
.then(x => x.json()) const store = DashboardConfigurationStore();
.then(x => callback ? callback(x) : undefined) if (!x.ok){
// .catch(() => { if (x.status !== 200){
// alert("Error occurred! Check console") if (x.status === 401){
// });
store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
router.push({path: '/signin'})
})
} }
+1 -1
View File
@@ -5,7 +5,7 @@ export default {
</script> </script>
<template> <template>
<div class="mt-5 text-body"> <div class="mt-md-5 mt-3 text-body">
<RouterView v-slot="{ Component, route }"> <RouterView v-slot="{ Component, route }">
<Transition name="fade2" mode="out-in"> <Transition name="fade2" mode="out-in">
<Suspense> <Suspense>
@@ -56,7 +56,7 @@ export default {
this.errorMessage = res.message; this.errorMessage = res.message;
document.querySelector(`#${res.data}`).classList.remove("is-valid") document.querySelector(`#${res.data}`).classList.remove("is-valid")
document.querySelector(`#${res.data}`).classList.add("is-invalid") document.querySelector(`#${res.data}`).classList.add("is-invalid")
this.loading = false;
} }
}) })
} }
@@ -126,15 +126,14 @@ export default {
</script> </script>
<template> <template>
<div class="mt-4"> <div class="mt-5">
<div class="container mb-4"> <div class="container mb-4">
<div class="mb-4 d-flex align-items-center gap-4"> <div class="mb-4 d-flex align-items-center gap-4">
<RouterLink to="/"> <RouterLink to="/" class="text-decoration-none">
<h3 class="mb-0 text-body"> <h3 class="mb-0 text-body">
<i class="bi bi-chevron-left"></i> <i class="bi bi-chevron-left me-4"></i> New Configuration
</h3> </h3>
</RouterLink> </RouterLink>
<h3 class="text-body mb-0">New Configuration</h3>
</div> </div>
<form class="text-body d-flex flex-column gap-3" <form class="text-body d-flex flex-column gap-3"
+1 -1
View File
@@ -60,7 +60,7 @@ export default {
</script> </script>
<template> <template>
<div class="mt-5 text-body"> <div class="mt-md-5 mt-3 text-body">
<div class="container"> <div class="container">
<h3 class="mb-3 text-body">Ping</h3> <h3 class="mb-3 text-body">Ping</h3>
<div class="row"> <div class="row">
+9 -5
View File
@@ -11,11 +11,13 @@ import DashboardTheme from "@/components/settingsComponent/dashboardTheme.vue";
import DashboardSettingsInputIPAddressAndPort import DashboardSettingsInputIPAddressAndPort
from "@/components/settingsComponent/dashboardSettingsInputIPAddressAndPort.vue"; from "@/components/settingsComponent/dashboardSettingsInputIPAddressAndPort.vue";
import DashboardAPIKeys from "@/components/settingsComponent/dashboardAPIKeys.vue"; import DashboardAPIKeys from "@/components/settingsComponent/dashboardAPIKeys.vue";
import AccountSettingsMFA from "@/components/settingsComponent/accountSettingsMFA.vue";
export default { export default {
name: "settings", name: "settings",
methods: {ipV46RegexCheck}, methods: {ipV46RegexCheck},
components: { components: {
AccountSettingsMFA,
DashboardAPIKeys, DashboardAPIKeys,
DashboardSettingsInputIPAddressAndPort, DashboardSettingsInputIPAddressAndPort,
DashboardTheme, DashboardTheme,
@@ -37,8 +39,8 @@ export default {
</script> </script>
<template> <template>
<div class="mt-5"> <div class="mt-md-5 mt-3">
<div class="container"> <div class="container-md">
<h3 class="mb-3 text-body">Settings</h3> <h3 class="mb-3 text-body">Settings</h3>
<DashboardTheme></DashboardTheme> <DashboardTheme></DashboardTheme>
<div class="card mb-4 shadow rounded-3"> <div class="card mb-4 shadow rounded-3">
@@ -49,7 +51,7 @@ export default {
<PeersDefaultSettingsInput targetData="peer_mtu" title="MTU (Max Transmission Unit)"></PeersDefaultSettingsInput> <PeersDefaultSettingsInput targetData="peer_mtu" title="MTU (Max Transmission Unit)"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput> <PeersDefaultSettingsInput targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput targetData="remote_endpoint" title="Peer Remote Endpoint" <PeersDefaultSettingsInput targetData="remote_endpoint" title="Peer Remote Endpoint"
:warning="true" warningText="This will be change globally, and will be apply to all peer's QR code and configuration file." :warning="true" warningText="This will be changed globally, and will be apply to all peer's QR code and configuration file."
></PeersDefaultSettingsInput> ></PeersDefaultSettingsInput>
</div> </div>
</div> </div>
@@ -67,14 +69,16 @@ export default {
</div> </div>
<div class="card mb-4 shadow rounded-3"> <div class="card mb-4 shadow rounded-3">
<p class="card-header">Account Settings</p> <p class="card-header">Account Settings</p>
<div class="card-body"> <div class="card-body d-flex gap-4 flex-column">
<AccountSettingsInputUsername targetData="username" <AccountSettingsInputUsername targetData="username"
title="Username" title="Username"
></AccountSettingsInputUsername> ></AccountSettingsInputUsername>
<hr> <hr class="m-0">
<AccountSettingsInputPassword <AccountSettingsInputPassword
targetData="password"> targetData="password">
</AccountSettingsInputPassword> </AccountSettingsInputPassword>
<hr class="m-0" v-if="!this.dashboardConfigurationStore.getActiveCrossServer()">
<AccountSettingsMFA v-if="!this.dashboardConfigurationStore.getActiveCrossServer()"></AccountSettingsMFA>
</div> </div>
</div> </div>
<DashboardAPIKeys></DashboardAPIKeys> <DashboardAPIKeys></DashboardAPIKeys>
+16 -25
View File
@@ -1,11 +1,9 @@
<script> <script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import QRCode from 'qrcode'
import Totp from "@/components/setupComponent/totp.vue";
import {fetchPost} from "@/utilities/fetch.js"; import {fetchPost} from "@/utilities/fetch.js";
export default { export default {
name: "setup", name: "setup",
components: {Totp}, components: {},
setup(){ setup(){
const store = DashboardConfigurationStore(); const store = DashboardConfigurationStore();
return {store} return {store}
@@ -16,8 +14,7 @@ export default {
username: "", username: "",
newPassword: "", newPassword: "",
repeatNewPassword: "", repeatNewPassword: "",
enable_totp: false, enable_totp: true
verified_totp: false
}, },
loading: false, loading: false,
errorMessage: "", errorMessage: "",
@@ -30,7 +27,6 @@ export default {
&& this.setup.newPassword.length >= 8 && this.setup.newPassword.length >= 8
&& this.setup.repeatNewPassword.length >= 8 && this.setup.repeatNewPassword.length >= 8
&& this.setup.newPassword === this.setup.repeatNewPassword && this.setup.newPassword === this.setup.repeatNewPassword
&& ((this.setup.enable_totp && this.setup.verified_totp) || !this.setup.enable_totp)
} }
}, },
methods: { methods: {
@@ -39,9 +35,7 @@ export default {
fetchPost("/api/Welcome_Finish", this.setup, (res) => { fetchPost("/api/Welcome_Finish", this.setup, (res) => {
if (res.status){ if (res.status){
this.done = true; this.done = true;
setTimeout(() => { this.$router.push('/2FASetup')
this.$router.push('/')
}, 500)
}else{ }else{
document.querySelectorAll("#createAccount input").forEach(x => x.classList.add("is-invalid")) document.querySelectorAll("#createAccount input").forEach(x => x.classList.add("is-invalid"))
this.errorMessage = res.message; this.errorMessage = res.message;
@@ -62,7 +56,7 @@ export default {
<template> <template>
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll" <div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
:data-bs-theme="this.store.Configuration.Server.dashboard_theme"> :data-bs-theme="this.store.Configuration.Server.dashboard_theme">
<div class="mx-auto text-body" style="width: 500px"> <div class="m-auto text-body" style="width: 500px">
<span class="dashboardLogo display-4">Nice to meet you!</span> <span class="dashboardLogo display-4">Nice to meet you!</span>
<p class="mb-5">Please fill in the following fields to finish setup 😊</p> <p class="mb-5">Please fill in the following fields to finish setup 😊</p>
<div> <div>
@@ -94,26 +88,23 @@ export default {
class="form-control" id="confirmPassword" name="confirmPassword" placeholder="and you can remember it :)" required> class="form-control" id="confirmPassword" name="confirmPassword" placeholder="and you can remember it :)" required>
</div> </div>
</div> </div>
<hr> <!-- <div class="form-check form-switch">-->
<div class="form-check form-switch"> <!-- <input class="form-check-input" type="checkbox" role="switch" id="enable_totp" -->
<input class="form-check-input" type="checkbox" role="switch" id="enable_totp" <!-- v-model="this.setup.enable_totp">-->
v-model="this.setup.enable_totp"> <!-- <label class="form-check-label" -->
<label class="form-check-label" <!-- for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label>-->
for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label> <!-- </div>-->
</div> <!-- <Suspense>-->
<Suspense> <!-- <Transition name="fade">-->
<Transition name="fade"> <!-- <Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp>-->
<Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp> <!-- </Transition>-->
</Transition> <!-- </Suspense>-->
</Suspense>
<button class="btn btn-dark btn-lg mb-5 d-flex btn-brand shadow align-items-center" <button class="btn btn-dark btn-lg mb-5 d-flex btn-brand shadow align-items-center"
ref="signInBtn" ref="signInBtn"
:disabled="!this.goodToSubmit || this.loading || this.done" @click="this.submit()"> :disabled="!this.goodToSubmit || this.loading || this.done" @click="this.submit()">
<span class="d-flex align-items-center w-100" v-if="!this.loading && !this.done"> <span class="d-flex align-items-center w-100" v-if="!this.loading && !this.done">
Finish<i class="bi bi-chevron-right ms-auto"></i></span> Next<i class="bi bi-chevron-right ms-auto"></i></span>
<span class="d-flex align-items-center w-100" v-else-if="this.done">
Welcome to WGDashboard!</span>
<span class="d-flex align-items-center w-100" v-else> <span class="d-flex align-items-center w-100" v-else>
Saving...<span class="spinner-border ms-auto spinner-border-sm" role="status"> Saving...<span class="spinner-border ms-auto spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
+109
View File
@@ -0,0 +1,109 @@
<script>
import {useRoute} from "vue-router";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchGet} from "@/utilities/fetch.js";
import {ref} from "vue";
import QRCode from "qrcode";
export default {
name: "share",
async setup(){
const route = useRoute();
const loaded = ref(false)
const store = DashboardConfigurationStore();
const theme = ref("");
const peerConfiguration = ref("");
const blob = ref(new Blob())
await fetchGet("/api/getDashboardTheme", {}, (res) => {
theme.value = res.data
});
const id = route.query.ShareID
if(id === undefined || id.length === 0){
peerConfiguration.value = undefined
loaded.value = true;
}else{
await fetchGet("/api/sharePeer/get", {
ShareID: id
}, (res) => {
if (res.status){
peerConfiguration.value = res.data;
blob.value = new Blob([peerConfiguration.value.file], { type: "text/plain" });
}else{
peerConfiguration.value = undefined
}
loaded.value = true;
})
}
return {store, theme, peerConfiguration, blob}
},
mounted() {
QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfiguration.file , (error) => {
if (error) console.error(error)
})
},
methods:{
download(){
const blob = new Blob([this.peerConfiguration.file], { type: "text/plain" });
const jsonObjectUrl = URL.createObjectURL(blob);
const filename = `${this.peerConfiguration.fileName}.conf`;
const anchorEl = document.createElement("a");
anchorEl.href = jsonObjectUrl;
anchorEl.download = filename;
anchorEl.click();
}
},
computed:{
getBlob(){
return URL.createObjectURL(this.blob)
}
}
}
</script>
<template>
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
:data-bs-theme="this.theme">
<div class="m-auto text-body" style="width: 500px">
<div class="text-center position-relative" style=""
v-if="!this.peerConfiguration">
<div class="animate__animated animate__fadeInUp">
<h1 style="font-size: 20rem; filter: blur(1rem); animation-duration: 7s"
class="animate__animated animate__flash animate__infinite">
<i class="bi bi-file-binary"></i>
</h1>
</div>
<div class="position-absolute w-100 h-100 top-0 start-0 d-flex animate__animated animate__fadeInUp"
style="animation-delay: 0.1s;"
>
<h3 class="m-auto">Oh no... This link is either expired or invalid.</h3>
</div>
</div>
<div v-else class="d-flex align-items-center flex-column gap-3">
<div class="h1 dashboardLogo text-center animate__animated animate__fadeInUp">
<h6>WGDashboard</h6>
Scan QR Code from the WireGuard App
</div>
<canvas id="qrcode" class="rounded-3 shadow animate__animated animate__fadeInUp mb-3" ref="qrcode"></canvas>
<p class="text-muted animate__animated animate__fadeInUp mb-1"
style="animation-delay: 0.2s;"
>or click the button below to download the <samp>.conf</samp> file</p>
<a
:download="this.peerConfiguration.fileName + '.conf'"
:href="getBlob"
class="btn btn-lg bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle animate__animated animate__fadeInUp shadow-sm"
style="animation-delay: 0.25s;"
>
<i class="bi bi-download"></i>
</a>
</div>
</div>
</div>
</template>
<style scoped>
.animate__fadeInUp{
animation-timing-function: cubic-bezier(0.42, 0, 0.22, 1.0)
}
</style>
+46 -8
View File
@@ -1,19 +1,25 @@
<script> <script>
import {fetchGet, fetchPost} from "../utilities/fetch.js"; import {fetchGet, fetchPost} from "../utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Message from "@/components/messageCentreComponent/message.vue";
import RemoteServerList from "@/components/signInComponents/RemoteServerList.vue";
export default { export default {
name: "signin", name: "signin",
components: {RemoteServerList, Message},
async setup(){ async setup(){
const store = DashboardConfigurationStore() const store = DashboardConfigurationStore()
let theme = "" let theme = "dark"
let totpEnabled = false; let totpEnabled = false;
if (!store.IsElectronApp){
await fetchGet("/api/getDashboardTheme", {}, (res) => { await fetchGet("/api/getDashboardTheme", {}, (res) => {
theme = res.data theme = res.data
}); });
await fetchGet("/api/isTotpEnabled", {}, (res) => { await fetchGet("/api/isTotpEnabled", {}, (res) => {
totpEnabled = res.data totpEnabled = res.data
}); });
}
store.removeActiveCrossServer();
return {store, theme, totpEnabled} return {store, theme, totpEnabled}
}, },
data(){ data(){
@@ -26,6 +32,11 @@ export default {
loading: false loading: false
} }
}, },
computed: {
getMessages(){
return this.store.Messages.filter(x => x.show)
}
},
methods: { methods: {
async auth(){ async auth(){
if (this.username && this.password && ((this.totpEnabled && this.totp) || !this.totpEnabled)){ if (this.username && this.password && ((this.totpEnabled && this.totp) || !this.totpEnabled)){
@@ -75,15 +86,18 @@ export default {
</script> </script>
<template> <template>
<div class="container-fluid login-container-fluid d-flex main flex-column" :data-bs-theme="this.theme"> <div class="container-fluid login-container-fluid d-flex main flex-column py-4 text-body"
<div class="login-box m-auto" style="width: 500px;"> style="overflow-y: scroll"
:data-bs-theme="this.theme">
<div class="login-box m-auto" >
<div class="m-auto" style="width: 700px;">
<h4 class="mb-0 text-body">Welcome to</h4> <h4 class="mb-0 text-body">Welcome to</h4>
<span class="dashboardLogo display-3">WGDashboard</span> <span class="dashboardLogo display-3"><strong>WGDashboard</strong></span>
<div class="m-auto">
<div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError"> <div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError">
{{this.loginErrorMessage}} {{this.loginErrorMessage}}
</div> </div>
<form @submit="(e) => {e.preventDefault(); this.auth();}"> <form @submit="(e) => {e.preventDefault(); this.auth();}"
v-if="!this.store.CrossServerConfiguration.Enable">
<div class="form-group text-body"> <div class="form-group text-body">
<label for="username" class="text-left" style="font-size: 1rem"> <label for="username" class="text-left" style="font-size: 1rem">
<i class="bi bi-person-circle"></i></label> <i class="bi bi-person-circle"></i></label>
@@ -107,7 +121,7 @@ export default {
v-model="this.totp" v-model="this.totp"
> >
</div> </div>
<button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand shadow signInBtn" ref="signInBtn"> <button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand signInBtn" ref="signInBtn">
<span v-if="!this.loading" class="d-flex w-100"> <span v-if="!this.loading" class="d-flex w-100">
Sign In<i class="ms-auto bi bi-chevron-right"></i> Sign In<i class="ms-auto bi bi-chevron-right"></i>
</span> </span>
@@ -119,15 +133,39 @@ export default {
</span> </span>
</button> </button>
</form> </form>
<RemoteServerList v-else></RemoteServerList>
<div class="d-flex mt-3" v-if="!this.store.IsElectronApp">
<div class="form-check form-switch ms-auto">
<input
v-model="this.store.CrossServerConfiguration.Enable"
class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked">
<label class="form-check-label" for="flexSwitchCheckChecked">Access Remote Server</label>
</div> </div>
</div> </div>
<small class="text-muted pb-3 d-block w-100 text-center"> </div>
</div>
<small class="text-muted pb-3 d-block w-100 text-center mt-3">
WGDashboard v4.0 | Developed with by WGDashboard v4.0 | Developed with by
<a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a> <a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
</small> </small>
<div class="messageCentre text-body position-absolute end-0 m-3">
<TransitionGroup name="message" tag="div" class="position-relative">
<Message v-for="m in getMessages.slice().reverse()"
:message="m" :key="m.id"></Message>
</TransitionGroup>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
@media screen and (max-width: 768px) {
.login-box{
width: 100% !important;
}
.login-box div{
width: auto !important;
}
}
</style> </style>
+2 -2
View File
@@ -38,8 +38,8 @@ export default {
</script> </script>
<template> <template>
<div class="mt-5 text-body"> <div class="mt-md-5 mt-3 text-body">
<div class="container"> <div class="container-md">
<h3 class="mb-3 text-body">Traceroute</h3> <h3 class="mb-3 text-body">Traceroute</h3>
<div class="row"> <div class="row">
<div class="col-sm-4 d-flex gap-2 flex-column"> <div class="col-sm-4 d-flex gap-2 flex-column">
+33 -2
View File
@@ -5,7 +5,35 @@ import {proxy} from "./proxy.js";
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig(({mode}) => {
if (mode === 'electron'){
return {
emptyOutDir: false,
base: './',
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
outDir: '../../../../WGDashboard-Desktop',
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
}
}
}
return {
base: "/static/app/dist", base: "/static/app/dist",
plugins: [ plugins: [
vue(), vue(),
@@ -18,9 +46,11 @@ export default defineConfig({
server:{ server:{
proxy: { proxy: {
'/api': proxy '/api': proxy
} },
host: '0.0.0.0'
}, },
build: { build: {
outDir: 'dist',
rollupOptions: { rollupOptions: {
output: { output: {
entryFileNames: `assets/[name].js`, entryFileNames: `assets/[name].js`,
@@ -29,4 +59,5 @@ export default defineConfig({
} }
} }
} }
}
}) })
+16 -4
View File
@@ -1,3 +1,11 @@
*{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.dp__input{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
display: none; display: none;
} }
@@ -113,7 +121,8 @@
} }
[data-bs-theme="dark"].main, [data-bs-theme="dark"].main,
#app:has(.main[data-bs-theme="dark"]){ #app:has(.main[data-bs-theme="dark"]),
[data-bs-theme="dark"].navbar-container{
background-color: #1b1e21; background-color: #1b1e21;
} }
@@ -138,8 +147,7 @@
} }
.nav-link:hover { .nav-link:hover {
padding-left: 30px; background-color: #e8e8e8;
background-color: #dfdfdf;
} }
.sidebar .nav-link .feather { .sidebar .nav-link .feather {
@@ -149,6 +157,7 @@
.sidebar .nav-link.active, .bottomNavContainer .nav-link.active { .sidebar .nav-link.active, .bottomNavContainer .nav-link.active {
color: #007bff; color: #007bff;
} }
.sidebar .nav-link:hover .feather, .sidebar .nav-link:hover .feather,
@@ -1120,6 +1129,7 @@ pre.index-alert {
background-color: #00000060; background-color: #00000060;
z-index: 9999; z-index: 9999;
backdrop-filter: blur(1px); backdrop-filter: blur(1px);
-webkit-backdrop-filter: blur(1px);
} }
.dashboardModal{ .dashboardModal{
@@ -1153,4 +1163,6 @@ pre.index-alert {
opacity: 0; opacity: 0;
} }
.messageCentre{
z-index: 9999;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because it is too large Load Diff
-929
View File
@@ -1,929 +0,0 @@
let $body = $("body");
let available_ips = [];
let $add_peer = document.getElementById("save_peer");
$("#configuration_delete").on("click", function(){
configurations.configurationDeleteModal().toggle();
});
function ajaxPostJSON(url, data, doneFunc){
$.ajax({
url: url,
method: "POST",
data: JSON.stringify(data),
headers: {"Content-Type": "application/json"}
}).done(function (res) {
doneFunc(res);
});
}
function ajaxGetJSON(url, doneFunc){
$.ajax({
url: url,
headers: {"Content-Type": "application/json"}
}).done(function (res) {
doneFunc(res);
});
}
$("#sure_delete_configuration").on("click", function () {
configurations.removeConfigurationInterval();
let ele = $(this)
ele.attr("disabled", "disabled");
function done(res){
if (res.status){
$('#configuration_delete_modal button[data-dismiss="modal"]').remove();
ele.text("Delete Successful! Redirecting in 5 seconds.");
setTimeout(function(){
window.location.replace('/');
}, 5000)
}else{
$("#remove_configuration_alert").removeClass("d-none").text(res.reason);
}
}
ajaxPostJSON("/api/deleteConfiguration", {"name": configurations.getConfigurationName()}, done);
});
function loadPeerDataUsageChartDone(res){
if (res.status === true){
let t = new Date();
let string = `${t.getDate()}/${t.getMonth()}/${t.getFullYear()} ${t.getHours() > 10 ? t.getHours():`0${t.getHours()}`}:${t.getMinutes() > 10 ? t.getMinutes():`0${t.getMinutes()}`}:${t.getSeconds() > 10 ? t.getSeconds():`0${t.getSeconds()}`}`;
$(".peerDataUsageUpdateTime").html(`Updated on: ${string}`);
configurations.peerDataUsageChartObj().data.labels = [];
configurations.peerDataUsageChartObj().data.datasets[0].data = [];
configurations.peerDataUsageChartObj().data.datasets[1].data = [];
console.log(res);
let data = res.data;
if (data.length > 0){
configurations.peerDataUsageChartObj().data.labels.push(data[data.length - 1].time);
configurations.peerDataUsageChartObj().data.datasets[0].data.push(0);
configurations.peerDataUsageChartObj().data.datasets[1].data.push(0);
configurations.peerDataUsageChartObj().data.datasets[0].lastData = data[data.length - 1].total_sent
configurations.peerDataUsageChartObj().data.datasets[1].lastData = data[data.length - 1].total_receive
for(let i = data.length - 2; i >= 0; i--){
let sent = data[i].total_sent - configurations.peerDataUsageChartObj().data.datasets[0].lastData;
let receive = data[i].total_receive - configurations.peerDataUsageChartObj().data.datasets[1].lastData;
configurations.peerDataUsageChartObj().data.datasets[0].data.push(sent);
configurations.peerDataUsageChartObj().data.datasets[1].data.push(receive);
configurations.peerDataUsageChartObj().data.labels.push(data[i].time);
configurations.peerDataUsageChartObj().data.datasets[0].lastData = data[i].total_sent;
configurations.peerDataUsageChartObj().data.datasets[1].lastData = data[i].total_receive;
}
configurations.peerDataUsageChartObj().update();
}
}
}
let peerDataUsageInterval;
$body.on("click", ".btn-data-usage-peer", function(){
configurations.peerDataUsageChartObj().data.peerID = $(this).data("peer-id");
configurations.peerDataUsageModal().toggle();
peerDataUsageInterval = setInterval(function(){
ajaxPostJSON("/api/getPeerDataUsage", {"config": configurations.getConfigurationName(), "peerID": configurations.peerDataUsageChartObj().data.peerID, "interval": window.localStorage.getItem("peerTimePeriod")}, loadPeerDataUsageChartDone);
}, 30000);
ajaxPostJSON("/api/getPeerDataUsage", {"config": configurations.getConfigurationName(), "peerID": configurations.peerDataUsageChartObj().data.peerID, "interval": window.localStorage.getItem("peerTimePeriod")}, loadPeerDataUsageChartDone);
});
$('#peerDataUsage').on('shown.bs.modal', function() {
configurations.peerDataUsageChartObj().resize();
}).on('hidden.bs.modal', function() {
clearInterval(peerDataUsageInterval);
configurations.peerDataUsageChartObj().data.peerID = "";
configurations.peerDataUsageChartObj().data.labels = [];
configurations.peerDataUsageChartObj().data.datasets[0].data = [];
configurations.peerDataUsageChartObj().data.datasets[1].data = [];
configurations.peerDataUsageChartObj().update();
});
$(".switchTimePeriod").on("click", function(){
let peerTimePeriod = window.localStorage.peerTimePeriod;
$(".switchTimePeriod").removeClass("active");
$(this).addClass("active");
if ($(this).data('time') !== peerTimePeriod){
ajaxPostJSON("/api/getPeerDataUsage", {"config": configurations.getConfigurationName(), "peerID": configurations.peerDataUsageChartObj().data.peerID, "interval": $(this).data('time')}, loadPeerDataUsageChartDone);
window.localStorage.peerTimePeriod = $(this).data('time');
}
})
/**
* Edit Configuration
*/
$editConfiguration = $("#edit_configuration");
$editConfiguration.on("click", function(){
configurations.getConfigurationDetails();
configurations.configurationEditModal().toggle();
});
$saveConfiguration = $("#editConfigurationBtn");
$saveConfiguration.on("click", function(){
$(this).html("Saving...")
$(this).siblings().hide();
$(this).attr("disabled", "disabled");
let data = {
"configurationName": configurations.getConfigurationName(),
"ListenPort": $("#editConfigurationListenPort").val(),
"PostUp": $("#editConfigurationPostUp").val(),
"PostDown": $("#editConfigurationPostDown").val(),
"PreDown": $("#editConfigurationPreDown").val(),
"PreUp": $("#editConfigurationPreUp").val(),
}
function done(res){
console.log(res);
$saveConfiguration.removeAttr("disabled");
if (res.status){
configurations.configurationEditModal().toggle();
configurations.loadPeers("");
showToast("Configuration saved");
}else{
showToast(res.reason);
}
$saveConfiguration.html("Save");
$saveConfiguration.siblings().show();
}
ajaxPostJSON("/api/saveConfiguration", data, done);
})
/**
* ==========
* Add peers
* ==========
*/
/**
* Toggle add peers modal when add button clicked
*/
document.querySelector(".add_btn").addEventListener("click", () => {
configurations.addModal().toggle();
});
/**
* When configuration switch got click
*/
$(".toggle--switch").on("change", function(){
console.log('lol')
$(this).addClass("waiting").attr("disabled", "disabled");
let id = configurations.getConfigurationName();
let status = $(this).prop("checked");
let ele = $(this);
$.ajax({
url: `/switch/${id}`
}).done(function(res){
if (res.status){
if (status){
showToast(`${id} is running.`)
}else{
showToast(`${id} is stopped.`)
}
}else{
if (status){
ele.prop("checked", false)
}else{
ele.prop("checked", true)
}
showToast(res.reason, true);
$(".index-alert").removeClass("d-none").text(`Configuration toggle failed. Please check the following error message:\n${res.message}`);
}
ele.removeClass("waiting");
ele.removeAttr("disabled");
configurations.loadPeers($('#search_peer_textbox').val())
});
});
/**
* Generate Public key when private got change
*/
document.querySelector("#private_key").addEventListener("change", (event) => {
let publicKey = document.querySelector("#public_key");
if (event.target.value.length === 44) {
publicKey.value = window.wireguard.generatePublicKey(event.target.value);
publicKey.setAttribute("disabled", "disabled");
} else {
publicKey.attributes.removeNamedItem("disabled");
publicKey.value = "";
}
});
/**
* Handle when add modal is show and hide
*/
$('#add_modal').on('show.bs.modal', function() {
configurations.generateKeyPair();
configurations.getAvailableIps();
}).on('hide.bs.modal', function() {
$("#allowed_ips_indicator").html('');
});
/**
* Handle when user clicked the regenerate button
*/
$("#re_generate_key").on("click", function() {
$("#public_key").attr("disabled", "disabled");
$("#re_generate_key i").addClass("rotating");
configurations.generateKeyPair();
});
/**
* Handle when user is editing in allowed ips textbox
*/
$("#allowed_ips").on("keyup", function() {
let s = configurations.cleanIp($(this).val());
s = s.split(",");
if (available_ips.includes(s[s.length - 1])) {
$("#allowed_ips_indicator").removeClass().addClass("text-success")
.html('<i class="bi bi-check-circle-fill"></i>');
} else {
$("#allowed_ips_indicator").removeClass().addClass("text-warning")
.html('<i class="bi bi-exclamation-circle-fill"></i>');
}
});
/**
* Change peer name when user typing in peer name textbox
*/
$("#peer_name_textbox").on("keyup", function() {
$(".peer_name").html($(this).val());
});
/**
* When Add Peer button got clicked
*/
$add_peer.addEventListener("click", function() {
let $bulk_add = $("#bulk_add");
if ($bulk_add.prop("checked")) {
if (!$("#new_add_amount").hasClass("is-invalid")) {
configurations.addPeersByBulk();
}
} else {
let $public_key = $("#public_key");
let $private_key = $("#private_key");
let $allowed_ips = $("#allowed_ips");
$allowed_ips.val(configurations.cleanIp($allowed_ips.val()));
let $new_add_DNS = $("#new_add_DNS");
$new_add_DNS.val(configurations.cleanIp($new_add_DNS.val()));
let $new_add_endpoint_allowed_ip = $("#new_add_endpoint_allowed_ip");
$new_add_endpoint_allowed_ip.val(configurations.cleanIp($new_add_endpoint_allowed_ip.val()));
let $new_add_name = $("#new_add_name");
let $new_add_MTU = $("#new_add_MTU");
let $new_add_keep_alive = $("#new_add_keep_alive");
let $enable_preshare_key = $("#enable_preshare_key");
$add_peer.setAttribute("disabled", "disabled");
$add_peer.innerHTML = "Adding...";
if ($allowed_ips.val() !== "" && $public_key.val() !== "" && $new_add_DNS.val() !== "" && $new_add_endpoint_allowed_ip.val() !== "") {
let conf = configurations.getConfigurationName();
let data_list = [$private_key, $allowed_ips, $new_add_name, $new_add_DNS, $new_add_endpoint_allowed_ip, $new_add_MTU, $new_add_keep_alive];
data_list.forEach((ele) => ele.attr("disabled", "disabled"));
$.ajax({
method: "POST",
url: "/add_peer/" + conf,
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify({
"private_key": $private_key.val(),
"public_key": $public_key.val(),
"allowed_ips": $allowed_ips.val(),
"name": $new_add_name.val(),
"DNS": $new_add_DNS.val(),
"endpoint_allowed_ip": $new_add_endpoint_allowed_ip.val(),
"MTU": $new_add_MTU.val(),
"keep_alive": $new_add_keep_alive.val(),
"enable_preshared_key": $enable_preshare_key.prop("checked"),
"preshared_key": $enable_preshare_key.val()
}),
success: function(response) {
if (response !== "true") {
$("#add_peer_alert").html(response).removeClass("d-none");
data_list.forEach((ele) => ele.removeAttr("disabled"));
$add_peer.removeAttribute("disabled");
$add_peer.innerHTML = "Save";
} else {
configurations.loadPeers("");
data_list.forEach((ele) => ele.removeAttr("disabled"));
$("#add_peer_form").trigger("reset");
$add_peer.removeAttribute("disabled");
$add_peer.innerHTML = "Save";
showToast("Add peer successful!");
configurations.addModal().toggle();
}
}
});
} else {
$("#add_peer_alert").html("Please fill in all required box.").removeClass("d-none");
$add_peer.removeAttribute("disabled");
$add_peer.innerHTML = "Add";
}
}
});
/**
* Handle when user is typing the amount of peers they want to add, and will check if the amount is less than 1 or
* is larger than the amount of available ips
*/
$("#new_add_amount").on("keyup", function() {
let $bulk_amount_validation = $("#bulk_amount_validation");
// $(this).removeClass("is-valid").addClass("is-invalid");
if ($(this).val().length > 0) {
if (isNaN($(this).val())) {
$(this).removeClass("is-valid").addClass("is-invalid");
$bulk_amount_validation.html("Please enter a valid integer");
} else if ($(this).val() > available_ips.length) {
$(this).removeClass("is-valid").addClass("is-invalid");
$bulk_amount_validation.html(`Cannot create more than ${available_ips.length} peers.`);
} else if ($(this).val() < 1) {
$(this).removeClass("is-valid").addClass("is-invalid");
$bulk_amount_validation.html("Please enter at least 1 or more.");
} else {
$(this).removeClass("is-invalid").addClass("is-valid");
}
} else {
$(this).removeClass("is-invalid").removeClass("is-valid");
}
});
/**
* Handle when user toggled add peers by bulk
*/
$("#bulk_add").on("change", function() {
let hide = $(".non-bulk");
let amount = $("#new_add_amount");
if ($(this).prop("checked") === true) {
for (let i = 0; i < hide.length; i++) {
$(hide[i]).attr("disabled", "disabled");
}
amount.removeAttr("disabled");
} else {
for (let i = 0; i < hide.length; i++) {
if ($(hide[i]).attr('id') !== "public_key") {
$(hide[i]).removeAttr("disabled");
}
}
amount.attr("disabled", "disabled");
}
});
/**
* =======================
* Available IP Related
* =======================
*/
/**
* Handle when available ip modal show and hide
*/
$("#available_ip_modal").on("show.bs.modal", () => {
document.querySelector('#add_modal').classList.add("ip_modal_open");
}).on("hidden.bs.modal", () => {
document.querySelector('#add_modal').classList.remove("ip_modal_open");
let ips = [];
let $selected_ip_list = document.querySelector("#selected_ip_list");
for (let i = 0; i < $selected_ip_list.childElementCount; i++) {
ips.push($selected_ip_list.children[i].dataset.ip);
}
ips.forEach((ele) => configurations.triggerIp(ele));
});
/**
* When IP Badge got click
*/
$body.on("click", ".available-ip-badge", function() {
$(".available-ip-item[data-ip='" + $(this).data("ip") + "']").removeClass("active");
$(this).remove();
});
/**
* When available ip item got click
*/
$body.on("click", ".available-ip-item", function() {
configurations.triggerIp($(this).data("ip"));
});
/**
* When search IP button got clicked
*/
$("#search_available_ip").on("click", function() {
configurations.ipModal().toggle();
let $allowed_ips = document.querySelector("#allowed_ips");
if ($allowed_ips.value.length > 0) {
let s = $allowed_ips.value.split(",");
for (let i = 0; i < s.length; i++) {
s[i] = s[i].trim();
configurations.triggerIp(s[i]);
}
}
}).tooltip();
/**
* When confirm IP is clicked
*/
$("#confirm_ip").on("click", () => {
configurations.ipModal().toggle();
let ips = [];
let $selected_ip_list = $("#selected_ip_list");
$selected_ip_list.children().each(function() {
ips.push($(this).data("ip"));
});
$("#allowed_ips").val(ips.join(", "));
ips.forEach((ele) => configurations.triggerIp(ele));
});
/**
* =======
* QR Code
* =======
*/
/**
* When the QR-code button got clicked on each peer
*/
$body.on("click", ".btn-qrcode-peer", function() {
let src = $(this).data('imgsrc');
$.ajax({
"url": src,
"method": "GET"
}).done(function(res) {
$("#qrcode_img").attr('src', res);
configurations.qrcodeModal().toggle();
});
});
/**
* ===========
* Delete Peer
* ===========
*/
/**
* When the delete button got clicked on each peer
*/
$body.on("click", ".btn-delete-peer", function() {
let peer_id = $(this).data('peer-id')
$("#delete_peer").data("peer-id", peer_id);
configurations.deleteModal().toggle();
});
$body.on("click", ".btn-lock-peer", function() {
configurations.toggleAccess($(this).data('peer-id'), configurations.getConfigurationName());
if ($(this).hasClass("lock")) {
console.log($(this).data("peer-name"))
showToast(`Enabled ${$(this).children().data("peer-name")}`)
$(this).removeClass("lock")
$(this).children().tooltip('hide').attr('data-original-title', 'Peer enabled. Click to disable peer.').tooltip('show');
} else {
// Currently unlocked
showToast(`Disabled ${$(this).children().data("peer-name")}`)
$(this).addClass("lock");
$(this).children().tooltip('hide').attr('data-original-title', 'Peer disabled. Click to enable peer.').tooltip('show');
}
});
/**
* When the confirm delete button clicked
*/
$("#delete_peer").on("click", function() {
$(this).attr("disabled", "disabled");
$(this).html("Deleting...");
let config = configurations.getConfigurationName();
let peer_ids = [$(this).data("peer-id")];
configurations.deletePeers(config, peer_ids);
});
/**
* =============
* Peer Settings
* =============
*/
/**
* Handle when setting button got clicked for each peer
*/
$body.on("click", ".btn-setting-peer", function() {
// configurations.startProgressBar();
let peer_id = $(this).data("peer-id");
$("#save_peer_setting").attr("peer_id", peer_id);
$.ajax({
method: "POST",
url: "/get_peer_data/" + configurations.getConfigurationName(),
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify({ "id": peer_id }),
success: function(response) {
let peer_name = ((response.name === "") ? "Untitled" : response.name);
$("#setting_modal .peer_name").html(peer_name);
$("#setting_modal #peer_name_textbox").val(response.name);
$("#setting_modal #peer_private_key_textbox").val(response.private_key);
$("#setting_modal #peer_DNS_textbox").val(response.DNS);
$("#setting_modal #peer_allowed_ip_textbox").val(response.allowed_ip);
$("#setting_modal #peer_endpoint_allowed_ips").val(response.endpoint_allowed_ip);
$("#setting_modal #peer_mtu").val(response.mtu);
$("#setting_modal #peer_keep_alive").val(response.keep_alive);
$("#setting_modal #peer_preshared_key_textbox").val(response.preshared_key);
configurations.settingModal().toggle();
configurations.endProgressBar();
}
});
});
/**
* Handle when setting modal is closing
*/
$('#setting_modal').on('hidden.bs.modal', function() {
$("#setting_peer_alert").addClass("d-none");
});
/**
* Handle when private key text box in setting modal got changed
*/
$("#peer_private_key_textbox").on("change", function() {
let $save_peer_setting = $("#save_peer_setting");
if ($(this).val().length > 0) {
$.ajax({
"url": "/check_key_match/" + configurations.getConfigurationName(),
"method": "POST",
"headers": { "Content-Type": "application/json" },
"data": JSON.stringify({
"private_key": $("#peer_private_key_textbox").val(),
"public_key": $save_peer_setting.attr("peer_id")
})
}).done(function(res) {
if (res.status === "failed") {
$("#setting_peer_alert").html(res.status).removeClass("d-none");
} else {
$("#setting_peer_alert").addClass("d-none");
}
});
}
});
/**
* When save peer setting button got clicked
*/
$("#save_peer_setting").on("click", function() {
$(this).attr("disabled", "disabled");
$(this).html("Saving...");
let $peer_DNS_textbox = $("#peer_DNS_textbox");
let $peer_allowed_ip_textbox = $("#peer_allowed_ip_textbox");
let $peer_endpoint_allowed_ips = $("#peer_endpoint_allowed_ips");
let $peer_name_textbox = $("#peer_name_textbox");
let $peer_private_key_textbox = $("#peer_private_key_textbox");
let $peer_preshared_key_textbox = $("#peer_preshared_key_textbox");
let $peer_mtu = $("#peer_mtu");
let $peer_keep_alive = $("#peer_keep_alive");
if ($peer_DNS_textbox.val() !== "" &&
$peer_allowed_ip_textbox.val() !== "" && $peer_endpoint_allowed_ips.val() !== "") {
let peer_id = $(this).attr("peer_id");
let conf_id = $(this).attr("conf_id");
let data_list = [$peer_name_textbox, $peer_DNS_textbox, $peer_private_key_textbox, $peer_preshared_key_textbox, $peer_allowed_ip_textbox, $peer_endpoint_allowed_ips, $peer_mtu, $peer_keep_alive];
data_list.forEach((ele) => ele.attr("disabled", "disabled"));
$.ajax({
method: "POST",
url: "/save_peer_setting/" + conf_id,
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify({
id: peer_id,
name: $peer_name_textbox.val(),
DNS: $peer_DNS_textbox.val(),
private_key: $peer_private_key_textbox.val(),
allowed_ip: $peer_allowed_ip_textbox.val(),
endpoint_allowed_ip: $peer_endpoint_allowed_ips.val(),
MTU: $peer_mtu.val(),
keep_alive: $peer_keep_alive.val(),
preshared_key: $peer_preshared_key_textbox.val()
}),
success: function(response) {
if (response.status === "failed") {
$("#setting_peer_alert").html(response.msg).removeClass("d-none");
} else {
configurations.settingModal().toggle();
configurations.loadPeers($('#search_peer_textbox').val());
$('#alertToast').toast('show');
$('#alertToast .toast-body').html("Peer Saved!");
}
$("#save_peer_setting").removeAttr("disabled").html("Save");
data_list.forEach((ele) => ele.removeAttr("disabled"));
}
});
} else {
$("#setting_peer_alert").html("Please fill in all required box.").removeClass("d-none");
$("#save_peer_setting").removeAttr("disabled").html("Save");
}
});
/**
* Toggle show or hide for the private key textbox in the setting modal
*/
$(".peer_private_key_textbox_switch").on("click", function() {
let $peer_private_key_textbox = $("#peer_private_key_textbox");
let mode = (($peer_private_key_textbox.attr('type') === 'password') ? "text" : "password");
let icon = (($peer_private_key_textbox.attr('type') === 'password') ? "bi bi-eye-slash-fill" : "bi bi-eye-fill");
$peer_private_key_textbox.attr('type', mode);
$(".peer_private_key_textbox_switch i").removeClass().addClass(icon);
});
/**
* ===========
* Search Peer
* ===========
*/
let typingTimer; // Timeout object
let doneTypingInterval = 200; // Timeout interval
/**
* Handle when the user keyup and keydown on the search textbox
*/
$('#search_peer_textbox').on('keyup', function() {
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
configurations.loadPeers($(this).val());
}, doneTypingInterval);
}).on('keydown', function() {
clearTimeout(typingTimer);
});
/**
* Manage Peers
*/
/**
* Handle when sort peers changed
*/
$body.on("change", "#sort_by_dropdown", function() {
$.ajax({
method: "POST",
data: JSON.stringify({ 'sort': $("#sort_by_dropdown option:selected").val() }),
headers: { "Content-Type": "application/json" },
url: "/update_dashboard_sort",
success: function() {
configurations.loadPeers($('#search_peer_textbox').val());
}
});
});
/**
* Handle copy public key
*/
$body.on("mouseenter", ".key", function() {
let label = $(this).parent().siblings().children()[1];
label.style.opacity = "100";
}).on("mouseout", ".key", function() {
let label = $(this).parent().siblings().children()[1];
label.style.opacity = "0";
setTimeout(function() {
label.innerHTML = "CLICK TO COPY";
}, 200);
}).on("click", ".key", function() {
let label = $(this).parent().siblings().children()[1];
configurations.copyToClipboard($(this));
label.innerHTML = "COPIED!";
});
/**
* Handle when interval button got clicked
*/
$body.on("click", ".update_interval", function() {
$(".interval-btn-group button").removeClass("active");
let _new = $(this);
_new.addClass("active");
let interval = $(this).data("refresh-interval");
if ([5000, 10000, 30000, 60000].includes(interval)) {
configurations.updateRefreshInterval(interval);
}
// $.ajax({
// method:"POST",
// data: "interval="+$(this).data("refresh-interval"),
// url: "/update_dashboard_refresh_interval",
// success: function (res){
// configurations.updateRefreshInterval(res, interval);
// }
// });
});
/**
* Handle when refresh button got clicked
*/
$body.on("click", ".refresh", function() {
configurations.loadPeers($('#search_peer_textbox').val());
});
/**
* Handle when display mode button got clicked
*/
$body.on("click", ".display_mode", function() {
$(".display-btn-group button").removeClass("active");
$(this).addClass("active");
window.localStorage.setItem("displayMode", $(this).data("display-mode"));
configurations.updateDisplayMode();
if ($(this).data("display-mode") === "list") {
Array($(".peer_list").children()).forEach(function(child) {
$(child).removeClass().addClass("col-12");
});
showToast("Displaying as List");
} else {
Array($(".peer_list").children()).forEach(function(child) {
$(child).removeClass().addClass("col-sm-6 col-lg-4");
});
showToast("Displaying as Grids");
}
});
/**
* =================
* Configuration Menu
* =================
*/
let $setting_btn_menu = $(".setting_btn_menu");
$setting_btn_menu.css("top", ($setting_btn_menu.height() + 54) * (-1));
let $setting_btn = $(".setting_btn");
/**
* When the menu button got clicked
*/
$setting_btn.on("click", function() {
if ($setting_btn_menu.hasClass("show")) {
$setting_btn_menu.removeClass("showing");
setTimeout(function() {
$setting_btn_menu.removeClass("show");
}, 201);
} else {
$setting_btn_menu.addClass("show");
setTimeout(function() {
$setting_btn_menu.addClass("showing");
}, 10);
}
});
/**
* Whenever the user clicked, if it is outside the menu and the menu is opened, hide the menu
*/
$("html").on("click", function(r) {
if (document.querySelector(".setting_btn") !== r.target) {
if (!document.querySelector(".setting_btn").contains(r.target)) {
if (!document.querySelector(".setting_btn_menu").contains(r.target)) {
$setting_btn_menu.removeClass("showing");
setTimeout(function() {
$setting_btn_menu.removeClass("show");
}, 310);
}
}
}
});
/**
* ====================
* Delete Peers by Bulk
* ====================
*/
/**
* When delete peers by bulk clicked
*/
$("#delete_peers_by_bulk_btn").on("click", () => {
let $delete_bulk_modal_list = $("#delete_bulk_modal .list-group");
$delete_bulk_modal_list.html('');
peers.forEach((peer) => {
let name;
if (peer.name === "") { name = "Untitled Peer"; } else { name = peer.name; }
$delete_bulk_modal_list.append('<a class="list-group-item list-group-item-action delete-bulk-peer-item" style="cursor: pointer" data-id="' +
peer.id + '" data-name="' + name + '">' + name + '<br><code>' + peer.id + '</code></a>');
});
configurations.deleteBulkModal().toggle();
});
/**
* When the item or tag of delete peers by bulk got clicked
*/
$body.on("click", ".delete-bulk-peer-item", function() {
configurations.toggleDeleteByBulkIP($(this));
}).on("click", ".delete-peer-bulk-badge", function() {
configurations.toggleDeleteByBulkIP($(".delete-bulk-peer-item[data-id='" + $(this).data("id") + "']"));
});
let $selected_peer_list = document.getElementById("selected_peer_list");
/**
* The change observer to observe when user choose 1 or more peers to delete
* @type {MutationObserver}
*/
let changeObserver = new MutationObserver(function() {
if ($selected_peer_list.hasChildNodes()) {
$("#confirm_delete_bulk_peers").removeAttr("disabled");
} else {
$("#confirm_delete_bulk_peers").attr("disabled", "disabled");
}
});
changeObserver.observe($selected_peer_list, {
attributes: true,
childList: true,
characterData: true
});
let confirm_delete_bulk_peers_interval;
/**
* When the user clicked the delete button in the delete peers by bulk
*/
$("#confirm_delete_bulk_peers").on("click", function() {
let btn = $(this);
if (confirm_delete_bulk_peers_interval !== undefined) {
clearInterval(confirm_delete_bulk_peers_interval);
confirm_delete_bulk_peers_interval = undefined;
btn.html("Delete");
} else {
let timer = 5;
btn.html(`Deleting in ${timer} secs... Click to cancel`);
confirm_delete_bulk_peers_interval = setInterval(function() {
timer -= 1;
btn.html(`Deleting in ${timer} secs... Click to cancel`);
if (timer === 0) {
btn.html(`Deleting...`);
btn.attr("disabled", "disabled");
let ips = [];
$selected_peer_list.childNodes.forEach((ele) => ips.push(ele.dataset.id));
configurations.deletePeers(configurations.getConfigurationName(), ips);
clearInterval(confirm_delete_bulk_peers_interval);
confirm_delete_bulk_peers_interval = undefined;
}
}, 1000);
}
});
/**
* Select all peers to delete
*/
$("#select_all_delete_bulk_peers").on("click", function() {
$(".delete-bulk-peer-item").each(function() {
if (!$(this).hasClass("active")) {
configurations.toggleDeleteByBulkIP($(this));
}
});
});
/**
* When delete peers by bulk window is hidden
*/
$(configurations.deleteBulkModal()._element).on("hidden.bs.modal", function() {
$(".delete-bulk-peer-item").each(function() {
if ($(this).hasClass("active")) {
configurations.toggleDeleteByBulkIP($(this));
}
});
});
/**
* ==============
* Download Peers
* ==============
*/
/**
* When the download peers button got clicked
*/
$body.on("click", ".btn-download-peer", function(e) {
e.preventDefault();
let link = $(this).attr("href");
$.ajax({
"url": link,
"method": "GET",
success: function(res) {
configurations.downloadOneConfig(res);
}
});
});
/**
* When the download all peers got clicked
*/
$("#download_all_peers").on("click", function() {
$.ajax({
"url": `/download_all/${configurations.getConfigurationName()}`,
"method": "GET",
success: function(res) {
if (res.peers.length > 0) {
window.wireguard.generateZipFiles(res);
showToast("Peers' zip file download successful!");
} else {
showToast("Oops! There are no peer can be download.");
}
}
});
});
File diff suppressed because one or more lines are too long
-243
View File
@@ -1,243 +0,0 @@
let emptyInputFeedback = "Can't leave empty";
$('[data-toggle="tooltip"]').tooltip()
let $add_configuration = $("#add_configuration");
let addConfigurationModal = $("#addConfigurationModal");
$(".bottomNavHome").addClass("active");
addConfigurationModal.modal({
keyboard: false,
backdrop: 'static',
show: false
});
addConfigurationModal.on("hidden.bs.modal", function(){
$("#add_configuration_form").trigger("reset");
$("#add_configuration_form input").removeClass("is-valid").removeClass("is-invalid");
$(".addConfigurationAvailableIPs").text("N/A");
});
$(".toggle--switch").on("change", function(){
$(this).addClass("waiting").attr("disabled", "disabled");
let id = $(this).data("conf-id");
let status = $(this).prop("checked");
let ele = $(this);
let label = $(this).siblings("label");
$.ajax({
url: `/switch/${id}`
}).done(function(res){
let dot = $(`div[data-conf-id="${id}"] .dot`);
if (res.status){
if (status){
dot.removeClass("dot-stopped").addClass("dot-running");
dot.siblings().text("Running");
showToast(`${id} is running.`);
}else{
dot.removeClass("dot-running").addClass("dot-stopped");
showToast(`${id} is stopped.`);
}
}else{
ele.parents().children(".card-message").html(`<pre class="index-alert">Configuration toggle failed. Please check the following error message:<br><code>${res.message}</code></pre>`)
showToast(`${id} toggled failed.`, true);
if (status){
ele.prop("checked", false)
}else{
ele.prop("checked", true)
}
}
ele.removeClass("waiting").removeAttr("disabled");
})
});
$(".sb-home-url").addClass("active");
$(".card-body").on("click", function(handle){
if ($(handle.target).attr("class") !== "toggleLabel" && $(handle.target).attr("class") !== "toggle--switch") {
let c = $(".card");
for (let i of c){
if (i != $(this).parent()[0]){
$(i).css("transition", "ease-in-out 0.3s").css("opacity", "0.5")
}
}
window.open($(this).find("a").attr("href"), "_self");
}
});
function genKeyPair(){
let keyPair = window.wireguard.generateKeypair();
$("#addConfigurationPrivateKey").val(keyPair.privateKey).data("checked", true);
}
$("#reGeneratePrivateKey").on("click", function() {
genKeyPair();
});
$("#toggleAddConfiguration").on("click", function(){
addConfigurationModal.modal('toggle');
genKeyPair()
});
$("#addConfigurationPrivateKey").on("change", function() {
$privateKey = $(this);
$privateKeyFeedback = $("#addConfigurationPrivateKeyFeedback");
if ($privateKey.val().length != 44){
invalidInput($privateKey, $privateKeyFeedback, "Invalid length");
}else{
validInput($privateKey);
}
});
function ajaxPostJSON(url, data, doneFunc){
$.ajax({
url: url,
method: "POST",
data: JSON.stringify(data),
headers: {"Content-Type": "application/json"}
}).done(function (res) {
doneFunc(res);
});
}
function validInput(input){
input.removeClass("is-invalid").addClass("is-valid").removeAttr("disabled").data("checked", true);
}
function invalidInput(input, feedback, text){
input.removeClass("is-valid").addClass("is-invalid").removeAttr("disabled").data("checked", false);
feedback.addClass("invalid-feedback").text(text);
}
function checkPort($this){
let port = $this;
port.attr("disabled", "disabled");
let portFeedback = $("#addConfigurationListenPortFeedback");
if (port.val().length == 0){
invalidInput(port, portFeedback, emptyInputFeedback)
}else{
function done(res){
if(res.status){
validInput(port);
}else{
invalidInput(port, portFeedback, res.reason)
}
}
ajaxPostJSON('/api/addConfigurationPortCheck', {"port": port.val()}, done);
}
}
$("#addConfigurationListenPort").on("change", function(){
checkPort($(this));
})
function checkAddress($this){
let address = $this;
address.attr("disabled", "disabled");
let availableIPs = $(".addConfigurationAvailableIPs");
let addressFeedback = $("#addConfigurationAddressFeedback");
if (address.val().length == 0){
invalidInput(address, addressFeedback, emptyInputFeedback);
availableIPs.html(`N/A`);
}else{
function done(res){
if (res.status){
availableIPs.html(`<strong>${res.data}</strong>`);
validInput(address);
}else{
invalidInput(address, addressFeedback, res.reason);
availableIPs.html(`N/A`);
}
}
ajaxPostJSON("/api/addConfigurationAddressCheck", {"address": address.val()}, done)
}
}
$("#addConfigurationAddress").on("change", function(){
checkAddress($(this));
});
function checkName($this){
let name = $this;
let nameFeedback = $("#addConfigurationNameFeedback");
name.val(name.val().replace(/\s/g,'')).attr("disabled", "disabled");
if (name.val().length === 0){
invalidInput(name, nameFeedback, emptyInputFeedback)
}else{
function done(res){
if (res.status){
validInput(name);
}else{
invalidInput(name, nameFeedback, res.reason);
}
}
ajaxPostJSON("/api/addConfigurationNameCheck", {"name": name.val()}, done);
}
}
$("#addConfigurationName").on("change", function(){
checkName($(this));
});
$("#addConfigurationBtn").on("click", function(){
let btn = $(this);
let input = $("#add_configuration_form input");
let filled = true;
for (let i = 0; i < input.length; i++){
let $i = $(input[i]);
if ($i.attr("required") != undefined){
if ($i.val().length == 0 && $i.attr("name") !== "addConfigurationPrivateKey"){
invalidInput($i, $i.siblings(".input-feedback"), emptyInputFeedback);
filled = false;
}
if ($i.val().length != 44 && $i.attr("name") == "addConfigurationPrivateKey"){
invalidInput($i, $i.siblings(".input-feedback"), "Invalid length");
filled = false;
}
if (!$i.data("checked")){
filled = false;
}
}
}
if (filled){
$("#addConfigurationModal .modal-footer .btn").hide();
$(".addConfigurationStatus").removeClass("d-none");
let data = {};
let q = [];
for (let i = 0; i < input.length; i++){
let $i = $(input[i]);
data[$i.attr("name")] = $i.val();
q.push($i.attr("name"));
}
let done = (res) => {
let name = res.data;
$(".addConfigurationAddStatus").removeClass("text-primary").addClass("text-success").html(`<i class="bi bi-check-circle-fill"></i> ${name} added successfully.`);
if (res.status){
setTimeout(() => {
$(".addConfigurationToggleStatus").removeClass("waiting").html(`<div class="spinner-border spinner-border-sm" role="status"></div> Toggle Configuration`)
$.ajax({
url: `/switch/${name}`
}).done(function(res){
if (res.status){
$(".addConfigurationToggleStatus").removeClass("text-primary").addClass("text-success").html(`<i class="bi bi-check-circle-fill"></i> Toggle Successfully. Refresh in 5 seconds.`);
setTimeout(() => {
$(".addConfigurationToggleStatus").text("Refeshing...")
location.reload();
}, 5000);
}else{
$(".addConfigurationToggleStatus").removeClass("text-primary").addClass("text-danger").html(`<i class="bi bi-x-circle-fill"></i> ${name} toggle failed.`)
$("#addCconfigurationAlertMessage").removeClass("d-none").html(`${name} toggle failed. Please check the following error message:<br>${res.message}`);
}
});
}, 500);
}else{
$(".addConfigurationStatus").removeClass("text-primary").addClass("text-danger").html(`<i class="bi bi-x-circle-fill"></i> ${name} adding failed.`)
$("#addCconfigurationAlert").removeClass("d-none").children(".alert-body").text(res.reason);
}
};
ajaxPostJSON("/api/addConfiguration", data, done);
}
});
-1
View File
File diff suppressed because one or more lines are too long
-38
View File
@@ -1,38 +0,0 @@
let wrapper = $(".bottomNavWrapper");
$(".bottomNavConfigs").on("click", function(){
let subNav = $(this).children(".subNav");
subNav.removeClass("animate__fadeOutDown").addClass("active animate__fadeInUp");
wrapper.fadeIn();
});
$(".bottomNavHome").on("click", function(){
window.location.replace('/')
});
$(".bottomNavSettings").on("click", function(){
window.location.replace('/settings')
})
function hideBottomSubNav(){
$(".bottomNavButton .subNav").removeClass("animate__fadeInUp").addClass("animate__fadeOutDown");
wrapper.fadeOut();
setTimeout(function(){
$(".bottomNavButton .subNav").removeClass("active");
},350)
}
wrapper.on("click", function(){
hideBottomSubNav();
});
$(".bottomNavMore").on("click", function(){
let subNav = $(this).children(".subNav");
subNav.removeClass("animate__fadeOutDown").addClass("active animate__fadeInUp");
wrapper.fadeIn();
});
// $(".bottomNavButton .nav-conf-link").on("click", function(){
// hideBottomSubNav();
// })
-60
View File
@@ -1,60 +0,0 @@
$(".sb-settings-url").addClass("active")
$(".confirm_modal").click(function () {
$(".app_new_ip").html($("#app_ip")[0].value)
$(".app_new_port").html($("#app_port")[0].value)
})
$(".confirm_restart").click(function () {
$(".cancel_restart").remove()
countdown = 7;
$.post('/update_app_ip_port', $('.update_app_ip_port').serialize())
url = $("#app_ip")[0].value + ":" + $("#app_port")[0].value;
$(".confirm_restart").attr("disabled", "disabled")
setInterval(function () {
if (countdown === 0) {
window.location.replace("http://" + url);
}
$(".confirm_restart").html("Redirecting you in " + countdown + " seconds.")
countdown--;
}, 1000)
});
$(".change_path").click(function () {
$(this).attr("disabled", "disabled");
countdown = 5;
setInterval(function () {
if (countdown === 0) {
location.reload()
}
$(".change_path").html("Redirecting you in " + countdown + " seconds.")
countdown--;
}, 1000)
$.post('/update_wg_conf_path', $('.update_wg_conf_path').serialize())
});
$(".bottomNavSettings").addClass("active");
$(".theme-switch-btn").on("click", function(){
if (!$(this).hasClass("active")){
let theme = $(this).data("theme");
$(".theme-switch-btn").removeClass("active");
$(this).addClass("active");
$.ajax({
method: "POST",
url: "/api/settings/setTheme",
headers: {"Content-Type": "application/json"},
data: JSON.stringify({"theme": theme})
}).done(function(res){
if (res.status == true){
if (theme == "light"){
$("#darkThemeCSS").remove();
showToast("Switched to light theme");
}else{
$("head").append('<link rel="stylesheet" type="text/css" href="/static/css/theme/dark.min.css" id="darkThemeCSS">');
showToast("Switched to dark theme");
}
}
});
}
});
-1
View File
@@ -1 +0,0 @@
$(".sb-settings-url").addClass("active");$(".confirm_modal").click(function(){$(".app_new_ip").html($("#app_ip")[0].value);$(".app_new_port").html($("#app_port")[0].value)});$(".confirm_restart").click(function(){$(".cancel_restart").remove();countdown=7;$.post("/update_app_ip_port",$(".update_app_ip_port").serialize());url=$("#app_ip")[0].value+":"+$("#app_port")[0].value;$(".confirm_restart").attr("disabled","disabled");setInterval(function(){if(countdown===0){window.location.replace("http://"+url)}$(".confirm_restart").html("Redirecting you in "+countdown+" seconds.");countdown--},1e3)});$(".change_path").click(function(){$(this).attr("disabled","disabled");countdown=5;setInterval(function(){if(countdown===0){location.reload()}$(".change_path").html("Redirecting you in "+countdown+" seconds.");countdown--},1e3);$.post("/update_wg_conf_path",$(".update_wg_conf_path").serialize())});$(".bottomNavSettings").addClass("active");$(".theme-switch-btn").on("click",function(){if(!$(this).hasClass("active")){let theme=$(this).data("theme");$(".theme-switch-btn").removeClass("active");$(this).addClass("active");$.ajax({method:"POST",url:"/api/settings/setTheme",headers:{"Content-Type":"application/json"},data:JSON.stringify({theme:theme})}).done(function(res){if(res.status==true){if(theme=="light"){$("#darkThemeCSS").remove();showToast("Switched to light theme")}else{$("head").append('<link rel="stylesheet" type="text/css" href="/static/css/theme/dark.min.css" id="darkThemeCSS">');showToast("Switched to dark theme")}}})}});
-90
View File
@@ -1,90 +0,0 @@
/**
* tools.js - Copyright(C) 2021 Donald Zou [https://github.com/donaldzou]
*/
$(".ip_dropdown").on("change",function (){
$(".modal.show .btn").removeAttr("disabled");
});
$(".conf_dropdown").on("change", function (){
$(".modal.show .ip_dropdown").html('<option value="none" selected="selected" disabled>Loading...');
$.ajax({
url: "/get_ping_ip",
method: "POST",
data: "config=" + $(this).children("option:selected").val(),
success: function (res){
$(".modal.show .ip_dropdown").html("");
$(".modal.show .ip_dropdown").append('<option value="none" selected="selected" disabled>Choose an IP');
$(".modal.show .ip_dropdown").append(res);
}
});
});
// Ping Tools
$(".send_ping").on("click", function (){
$(this).attr("disabled","disabled");
$(this).html("Pinging...");
$("#ping_modal .form-control").attr("disabled","disabled");
$.ajax({
method:"POST",
data: "ip="+ $(':selected', $("#ping_modal .ip_dropdown")).val() +
"&count=" + $("#ping_modal .ping_count").val(),
url: "/ping_ip",
success: function (res){
$(".ping_result tbody").html("");
let html = '<tr><th scope="row">Address</th><td>'+res.address+'</td></tr>' +
'<tr><th scope="row">Is Alive</th><td>'+res.is_alive+'</td></tr>' +
'<tr><th scope="row">Min RTT</th><td>'+res.min_rtt+'ms</td></tr>' +
'<tr><th scope="row">Average RTT </th><td>'+res.avg_rtt+'ms</td></tr>' +
'<tr><th scope="row">Max RTT</th><td>'+res.max_rtt+'ms</td></tr>' +
'<tr><th scope="row">Package Sent</th><td>'+res.package_sent+'</td></tr>' +
'<tr><th scope="row">Package Received</th><td>'+res.package_received+'</td></tr>' +
'<tr><th scope="row">Package Loss</th><td>'+res.package_loss+'</td></tr>';
$(".ping_result tbody").html(html);
$(".send_ping").removeAttr("disabled");
$(".send_ping").html("Ping");
$("#ping_modal .form-control").removeAttr("disabled");
}
});
});
// Traceroute Tools
$(".send_traceroute").on("click", function (){
$(this).attr("disabled","disabled");
$(this).html("Tracing...");
$("#traceroute_modal .form-control").attr("disabled","disabled");
$.ajax({
url: "/traceroute_ip",
method: "POST",
data: "ip=" + $(':selected', $("#traceroute_modal .ip_dropdown")).val(),
success: function (res){
$(".traceroute_result tbody").html("");
res.forEach((ele) =>
$(".traceroute_result tbody").append('<tr><th scope="row">'+ele.hop+'</th><td>'+ele.ip+'</td><td>'+ele.avg_rtt+'</td><td>'+ele.min_rtt+'</td><td>'+ele.max_rtt+'</td></tr>'));
$(".send_traceroute").removeAttr("disabled").html("Traceroute");
$("#traceroute_modal .form-control").removeAttr("disabled");
}
});
});
let numberToast = 0;
function showToast(msg, isDanger = false) {
$(".toastContainer").append(
`<div id="${numberToast}-toast" class="toast hide animate__animated animate__fadeInUp" role="alert" data-delay="5000">
<div class="toast-header">
<strong class="mr-auto">WGDashboard</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body ${isDanger ? 'text-danger':''}">${msg}</div>
<div class="toast-progressbar ${isDanger ? 'bg-danger':''}"></div>
</div>` )
$(`#${numberToast}-toast`).toast('show');
$(`#${numberToast}-toast .toast-body`).html(msg);
$(`#${numberToast}-toast .toast-progressbar`).css("transition", `width ${$(`#${numberToast}-toast .toast-progressbar`).parent().data('delay')}ms cubic-bezier(0, 0, 0, 0)`);
$(`#${numberToast}-toast .toast-progressbar`).css("width", "0px");
let i = numberToast;
setTimeout(function(){
$(`#${i}-toast`).removeClass("animate__fadeInUp").addClass("animate__fadeOutRight")
}, 4500)
numberToast++;
}
-10
View File
@@ -1,10 +0,0 @@
$(".ip_dropdown").on("change",function(){$(".modal.show .btn").removeAttr("disabled")});$(".conf_dropdown").on("change",function(){$(".modal.show .ip_dropdown").html('<option value="none" selected="selected" disabled>Loading...');$.ajax({url:"/get_ping_ip",method:"POST",data:"config="+$(this).children("option:selected").val(),success:function(res){$(".modal.show .ip_dropdown").html("");$(".modal.show .ip_dropdown").append('<option value="none" selected="selected" disabled>Choose an IP');$(".modal.show .ip_dropdown").append(res)}})});$(".send_ping").on("click",function(){$(this).attr("disabled","disabled");$(this).html("Pinging...");$("#ping_modal .form-control").attr("disabled","disabled");$.ajax({method:"POST",data:"ip="+$(":selected",$("#ping_modal .ip_dropdown")).val()+"&count="+$("#ping_modal .ping_count").val(),url:"/ping_ip",success:function(res){$(".ping_result tbody").html("");let html='<tr><th scope="row">Address</th><td>'+res.address+"</td></tr>"+'<tr><th scope="row">Is Alive</th><td>'+res.is_alive+"</td></tr>"+'<tr><th scope="row">Min RTT</th><td>'+res.min_rtt+"ms</td></tr>"+'<tr><th scope="row">Average RTT </th><td>'+res.avg_rtt+"ms</td></tr>"+'<tr><th scope="row">Max RTT</th><td>'+res.max_rtt+"ms</td></tr>"+'<tr><th scope="row">Package Sent</th><td>'+res.package_sent+"</td></tr>"+'<tr><th scope="row">Package Received</th><td>'+res.package_received+"</td></tr>"+'<tr><th scope="row">Package Loss</th><td>'+res.package_loss+"</td></tr>";$(".ping_result tbody").html(html);$(".send_ping").removeAttr("disabled");$(".send_ping").html("Ping");$("#ping_modal .form-control").removeAttr("disabled")}})});$(".send_traceroute").on("click",function(){$(this).attr("disabled","disabled");$(this).html("Tracing...");$("#traceroute_modal .form-control").attr("disabled","disabled");$.ajax({url:"/traceroute_ip",method:"POST",data:"ip="+$(":selected",$("#traceroute_modal .ip_dropdown")).val(),success:function(res){$(".traceroute_result tbody").html("");res.forEach(ele=>$(".traceroute_result tbody").append('<tr><th scope="row">'+ele.hop+"</th><td>"+ele.ip+"</td><td>"+ele.avg_rtt+"</td><td>"+ele.min_rtt+"</td><td>"+ele.max_rtt+"</td></tr>"));$(".send_traceroute").removeAttr("disabled").html("Traceroute");$("#traceroute_modal .form-control").removeAttr("disabled")}})});let numberToast=0;function showToast(msg,isDanger=false){$(".toastContainer").append(`<div id="${numberToast}-toast" class="toast hide animate__animated animate__fadeInUp" role="alert" data-delay="5000">
<div class="toast-header">
<strong class="mr-auto">WGDashboard</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body ${isDanger?"text-danger":""}">${msg}</div>
<div class="toast-progressbar ${isDanger?"bg-danger":""}"></div>
</div>`);$(`#${numberToast}-toast`).toast("show");$(`#${numberToast}-toast .toast-body`).html(msg);$(`#${numberToast}-toast .toast-progressbar`).css("transition",`width ${$(`#${numberToast}-toast .toast-progressbar`).parent().data("delay")}ms cubic-bezier(0, 0, 0, 0)`);$(`#${numberToast}-toast .toast-progressbar`).css("width","0px");let i=numberToast;setTimeout(function(){$(`#${i}-toast`).removeClass("animate__fadeInUp").addClass("animate__fadeOutRight")},4500);numberToast++}
-313
View File
@@ -1,313 +0,0 @@
/*! SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
(function() {
function gf(init) {
var r = new Float64Array(16);
if (init) {
for (var i = 0; i < init.length; ++i)
r[i] = init[i];
}
return r;
}
function pack(o, n) {
var b, m = gf(), t = gf();
for (var i = 0; i < 16; ++i)
t[i] = n[i];
carry(t);
carry(t);
carry(t);
for (var j = 0; j < 2; ++j) {
m[0] = t[0] - 0xffed;
for (var i = 1; i < 15; ++i) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
m[i - 1] &= 0xffff;
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
b = (m[15] >> 16) & 1;
m[14] &= 0xffff;
cswap(t, m, 1 - b);
}
for (var i = 0; i < 16; ++i) {
o[2 * i] = t[i] & 0xff;
o[2 * i + 1] = t[i] >> 8;
}
}
function carry(o) {
var c;
for (var i = 0; i < 16; ++i) {
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
o[i] &= 0xffff;
}
}
function cswap(p, q, b) {
var t, c = ~(b - 1);
for (var i = 0; i < 16; ++i) {
t = c & (p[i] ^ q[i]);
p[i] ^= t;
q[i] ^= t;
}
}
function add(o, a, b) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] + b[i]) | 0;
}
function subtract(o, a, b) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] - b[i]) | 0;
}
function multmod(o, a, b) {
var t = new Float64Array(31);
for (var i = 0; i < 16; ++i) {
for (var j = 0; j < 16; ++j)
t[i + j] += a[i] * b[j];
}
for (var i = 0; i < 15; ++i)
t[i] += 38 * t[i + 16];
for (var i = 0; i < 16; ++i)
o[i] = t[i];
carry(o);
carry(o);
}
function invert(o, i) {
var c = gf();
for (var a = 0; a < 16; ++a)
c[a] = i[a];
for (var a = 253; a >= 0; --a) {
multmod(c, c, c);
if (a !== 2 && a !== 4)
multmod(c, c, i);
}
for (var a = 0; a < 16; ++a)
o[a] = c[a];
}
function clamp(z) {
z[31] = (z[31] & 127) | 64;
z[0] &= 248;
}
function generatePublicKey(privateKey) {
var r, z = new Uint8Array(32);
var a = gf([1]),
b = gf([9]),
c = gf(),
d = gf([1]),
e = gf(),
f = gf(),
_121665 = gf([0xdb41, 1]),
_9 = gf([9]);
for (var i = 0; i < 32; ++i)
z[i] = privateKey[i];
clamp(z);
for (var i = 254; i >= 0; --i) {
r = (z[i >>> 3] >>> (i & 7)) & 1;
cswap(a, b, r);
cswap(c, d, r);
add(e, a, c);
subtract(a, a, c);
add(c, b, d);
subtract(b, b, d);
multmod(d, e, e);
multmod(f, a, a);
multmod(a, c, a);
multmod(c, b, e);
add(e, a, c);
subtract(a, a, c);
multmod(b, a, a);
subtract(c, d, f);
multmod(a, c, _121665);
add(a, a, d);
multmod(c, c, a);
multmod(a, d, f);
multmod(d, b, _9);
multmod(b, e, e);
cswap(a, b, r);
cswap(c, d, r);
}
invert(c, c);
multmod(a, a, c);
pack(z, a);
return z;
}
function generatePresharedKey() {
var privateKey = new Uint8Array(32);
window.crypto.getRandomValues(privateKey);
return privateKey;
}
function generatePrivateKey() {
var privateKey = generatePresharedKey();
clamp(privateKey);
return privateKey;
}
function encodeBase64(dest, src) {
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
for (var i = 0; i < 4; ++i)
dest[i] = input[i] + 65 +
(((25 - input[i]) >> 8) & 6) -
(((51 - input[i]) >> 8) & 75) -
(((61 - input[i]) >> 8) & 15) +
(((62 - input[i]) >> 8) & 3);
}
function keyToBase64(key) {
var i, base64 = new Uint8Array(44);
for (i = 0; i < 32 / 3; ++i)
encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
base64[43] = 61;
return String.fromCharCode.apply(null, base64);
}
function base64ToKey(base64) {
let binary_string = window.atob(base64);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
let uint8 = new Uint8Array(bytes.buffer);
return uint8;
}
function putU32(b, n)
{
b.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff);
}
function putU16(b, n)
{
b.push(n & 0xff, (n >>> 8) & 0xff);
}
function putBytes(b, a)
{
for (var i = 0; i < a.length; ++i)
b.push(a[i] & 0xff);
}
function encodeString(s)
{
var utf8 = unescape(encodeURIComponent(s));
var b = new Uint8Array(utf8.length);
for (var i = 0; i < utf8.length; ++i)
b[i] = utf8.charCodeAt(i);
return b;
}
function crc32(b)
{
if (!crc32.table) {
crc32.table = [];
for (var c = 0, n = 0; n < 256; c = ++n) {
for (var k = 0; k < 8; ++k)
c = ((c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1));
crc32.table[n] = c;
}
}
var crc = -1;
for (var i = 0; i < b.length; ++i)
crc = (crc >>> 8) ^ crc32.table[(crc ^ b[i]) & 0xff];
return (crc ^ (-1)) >>> 0;
}
function createZipFile(files)
{
var b = [];
var cd = [];
var offset = 0;
for (var i = 0; i < files.length; ++i) {
var name = encodeString(files[i].filename);
var contents = encodeString(files[i].content);
var crc = crc32(contents);
putU32(b, 0x04034b50); /* signature */
putU16(b, 20); /* version needed */
putU16(b, 0); /* flags */
putU16(b, 0); /* compression method */
putU16(b, 0); /* mtime */
putU16(b, 0); /* mdate */
putU32(b, crc); /* crc32 */
putU32(b, contents.length); /* compressed size */
putU32(b, contents.length); /* uncompressed size */
putU16(b, name.length); /* file name length */
putU16(b, 0); /* extra field length */
putBytes(b, name);
putBytes(b, contents);
putU32(cd, 0x02014b50); /* signature */
putU16(cd, 0); /* version made */
putU16(cd, 20); /* version needed */
putU16(cd, 0); /* flags */
putU16(cd, 0); /* compression method */
putU16(cd, 0); /* mtime */
putU16(cd, 0); /* mdate */
putU32(cd, crc); /* crc32 */
putU32(cd, contents.length); /* compressed size */
putU32(cd, contents.length); /* uncompressed size */
putU16(cd, name.length); /* file name length */
putU16(cd, 0); /* extra field length */
putU16(cd, 0); /* file comment length */
putU16(cd, 0); /* disk number start */
putU16(cd, 0); /* internal file attributes */
putU32(cd, 32); /* external file attributes - 'archive' bit set (32) */
putU32(cd, offset); /* relative offset of local header */
putBytes(cd, name); /* file name */
offset += 30 + contents.length + name.length
}
putBytes(b, cd); /* central directory */
putU32(b, 0x06054b50); /* end of central directory signature */
putU16(b, 0); /* number of this disk */
putU16(b, 0); /* number of disk with central directory start */
putU16(b, files.length); /* number of entries on disk */
putU16(b, files.length); /* number of entries */
putU32(b, cd.length); /* length of central directory */
putU32(b, offset); /* offset to start of central directory */
putU16(b, 0); /* zip comment size */
return Uint8Array.from(b);
}
window.wireguard = {
generateKeypair: function() {
var privateKey = generatePrivateKey();
var publicKey = generatePublicKey(privateKey);
var presharedKey = generatePresharedKey();
return {
publicKey: keyToBase64(publicKey),
privateKey: keyToBase64(privateKey),
presharedKey: keyToBase64(presharedKey)
};
},
generatePublicKey: function (privateKey){
privateKey = base64ToKey(privateKey);
return keyToBase64(generatePublicKey(privateKey));
},
generateZipFiles: function(res){
var files = res.peers;
var zipFile = createZipFile(files);
var blob = new Blob([zipFile], { type: "application/zip" });
var a = document.createElement("a");
a.download = res.filename;
a.href = URL.createObjectURL(blob);
a.style.display = "none";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
};
})();
+2 -2
View File
@@ -13,8 +13,8 @@
<link rel="apple-touch-icon" sizes="192x192" href="{{ url_for('static',filename='img/192x192ios.png') }}"> <link rel="apple-touch-icon" sizes="192x192" href="{{ url_for('static',filename='img/192x192ios.png') }}">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="{{ url_for('static',filename='img/logo.png') }}"/> <link rel="icon" href="{{ url_for('static',filename='img/logo.png') }}"/>
<link rel="stylesheet" href="../static/app/dist/assets/index.css"> <link rel="stylesheet" href="{{ url_for('static',filename='app/dist/assets/index.css') }}">
<script src="../static/app/dist/assets/index.js" type="module"></script> <script src="{{ url_for('static',filename='app/dist/assets/index.js') }}" type="module"></script>
</head> </head>
<body> <body>
<div id="app" class="w-100 vh-100"></div> <div id="app" class="w-100 vh-100"></div>
+8 -4
View File
@@ -1,14 +1,18 @@
[Unit] [Unit]
After=syslog.target network-online.target After=syslog.target network-online.target
Wants=wg-quick.target
ConditionPathIsDirectory=/etc/wireguard ConditionPathIsDirectory=/etc/wireguard
[Service] [Service]
Environment="VIRTUAL_ENV={{VIRTUAL_ENV}}" Type=forking
WorkingDirectory={{APP_ROOT}} PIDFile=<absolute_path_of_wgdashboard_src>/gunicorn.pid
ExecStart={{VIRTUAL_ENV}}/bin/python3 {{APP_ROOT}}/dashboard.py WorkingDirectory=<absolute_path_of_wgdashboard_src>
ExecStart=<absolute_path_of_wgdashboard_src>/wgd.sh start
ExecStop=<absolute_path_of_wgdashboard_src>/wgd.sh stop
ExecReload=<absolute_path_of_wgdashboard_src>/wgd.sh restart
TimeoutSec=120
PrivateTmp=yes PrivateTmp=yes
Restart=always Restart=always
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
+118 -51
View File
@@ -2,10 +2,17 @@
# wgd.sh - Copyright(C) 2024 Donald Zou [https://github.com/donaldzou] # wgd.sh - Copyright(C) 2024 Donald Zou [https://github.com/donaldzou]
# Under Apache-2.0 License # Under Apache-2.0 License
trap "kill $TOP_PID"
export TOP_PID=$$
app_name="dashboard.py" app_name="dashboard.py"
app_official_name="WGDashboard" app_official_name="WGDashboard"
venv_python="./venv/bin/python3" venv_python="./venv/bin/python3"
venv_gunicorn="./venv/bin/gunicorn" venv_gunicorn="./venv/bin/gunicorn"
pythonExecutable="python3"
heavy_checkmark=$(printf "\xE2\x9C\x94")
heavy_crossmark=$(printf "\xE2\x9C\x97")
PID_FILE=./gunicorn.pid PID_FILE=./gunicorn.pid
environment=$(if [[ $ENVIRONMENT ]]; then echo $ENVIRONMENT; else echo 'develop'; fi) environment=$(if [[ $ENVIRONMENT ]]; then echo $ENVIRONMENT; else echo 'develop'; fi)
@@ -40,9 +47,15 @@ _check_and_set_venv(){
VIRTUAL_ENV="./venv" VIRTUAL_ENV="./venv"
if [ ! -d $VIRTUAL_ENV ]; then if [ ! -d $VIRTUAL_ENV ]; then
printf "[WGDashboard] Creating Python Virtual Environment under ./venv\n" printf "[WGDashboard] Creating Python Virtual Environment under ./venv\n"
{ python3 -m venv $VIRTUAL_ENV; } >> ./log/install.txt { $pythonExecutable -m venv $VIRTUAL_ENV; } >> ./log/install.txt
fi fi
printf "[WGDashboard] Activate Python Virtual Environment under ./venv\n"
if ! $venv_python --version > /dev/null 2>&1
then
printf "[WGDashboard] %s Python Virtual Environment under ./venv failed to create. Halting now.\n" "$heavy_crossmark"
kill $TOP_PID
fi
. ${VIRTUAL_ENV}/bin/activate . ${VIRTUAL_ENV}/bin/activate
} }
@@ -55,50 +68,44 @@ _determineOS(){
# elif [ -f /etc/arch-release ]; then # elif [ -f /etc/arch-release ]; then
# OS="arch" # OS="arch"
else else
printf "[WGDashboard] Sorry, your OS is not supported. Currently the install script only support Debian-based, Red Hat-based OS." printf "[WGDashboard] %s Sorry, your OS is not supported. Currently the install script only support Debian-based, Red Hat-based OS." "$heavy_crossmark"
printf "%s\n" "$helpMsg" printf "%s\n" "$helpMsg"
exit 1 kill $TOP_PID
fi fi
printf "[WGDashboard] OS: %s\n" "$OS"
} }
_installPython(){ _installPython(){
case "$OS" in case "$OS" in
ubuntu|debian) ubuntu|debian)
{ sudo apt update ; sudo apt-get install -y python3; printf "\n\n"; } &>> ./log/install.txt { sudo apt update ; sudo apt-get install -y python3 net-tools; printf "\n\n"; } &>> ./log/install.txt
;; ;;
centos|fedora|redhat) centos|fedora|redhat|rehl)
if command -v dnf &> /dev/null; then if command -v dnf &> /dev/null; then
{ sudo dnf install -y python3; printf "\n\n"; } >> ./log/install.txt { sudo dnf install -y python3 net-tools; printf "\n\n"; } >> ./log/install.txt
else else
{ sudo yum install -y python3; printf "\n\n"; } >> ./log/install.txt { sudo yum install -y python3 net-tools ; printf "\n\n"; } >> ./log/install.txt
fi fi
;; ;;
# arch)
# { sudo pacman -Sy python python-pip; printf "\n\n"; } >> ./log/install.txt
# ;;
# *)
# printf "[WGDashboard] Sorry, your OS is not supported. Currently the install script only support Debian-based, Red Hat-based and Arch-based OS."
# printf "%s" "$helpMsg"
# exit 1
# ;;
esac esac
if ! python3 --version > /dev/null 2>&1 if ! python3 --version > /dev/null 2>&1
then then
printf "[WGDashboard] Python is still not installed, halting script now.\n" printf "[WGDashboard] %s Python is still not installed, halting script now.\n" "$heavy_crossmark"
printf "%s\n" "$helpMsg" printf "%s\n" "$helpMsg"
exit 1 kill $TOP_PID
else else
printf "[WGDashboard] \xE2\x9C\x94 Python is installed\n" printf "[WGDashboard] %s Python is installed\n" "$heavy_checkmark"
fi fi
} }
_installPythonVenv(){ _installPythonVenv(){
if [ "$pythonExecutable" = "python3" ]; then
case "$OS" in case "$OS" in
ubuntu|debian) ubuntu|debian)
{ sudo apt update ; sudo apt-get install -y python3-venv; printf "\n\n"; } &>> ./log/install.txt { sudo apt update ; sudo apt-get install -y python3-venv; printf "\n\n"; } &>> ./log/install.txt
;; ;;
centos|fedora|redhat) centos|fedora|redhat|rhel)
if command -v dnf &> /dev/null; then if command -v dnf &> /dev/null; then
{ sudo dnf install -y python3-virtualenv; printf "\n\n"; } >> ./log/install.txt { sudo dnf install -y python3-virtualenv; printf "\n\n"; } >> ./log/install.txt
else else
@@ -106,65 +113,121 @@ _installPythonVenv(){
fi fi
;; ;;
*) *)
printf "[WGDashboard] Sorry, your OS is not supported. Currently the install script only support Debian-based, Red Hat-based OS." printf "[WGDashboard] %s Sorry, your OS is not supported. Currently the install script only support Debian-based, Red Hat-based OS.\n" "$heavy_crossmark"
printf "%s\n" "$helpMsg" printf "%s\n" "$helpMsg"
exit 1 kill $TOP_PID
;; ;;
esac esac
else
case "$OS" in
ubuntu|debian)
{ sudo apt-get update; sudo apt-get install ${pythonExecutable}-venv; } &>> ./log/install.txt
;;
# centos|fedora|redhat|rhel)
# if command -v dnf &> /dev/null; then
# { sudo dnf install -y ${pythonExecutable}-virtualenv; printf "\n\n"; } >> ./log/install.txt
# else
# { sudo yum install -y ${pythonExecutable}-virtualenv; printf "\n\n"; } >> ./log/install.txt
# fi
# ;;
# *)
# printf "[WGDashboard] %s Sorry, your OS is not supported. Currently the install script only support Debian-based, Red Hat-based OS.\n" "$heavy_crossmark"
# printf "%s\n" "$helpMsg"
# kill $TOP_PID
# ;;
esac
fi
if ! python3 -m venv -h > /dev/null 2>&1 if ! $pythonExecutable -m venv -h > /dev/null 2>&1
then then
printf "[WGDashboard] Python Virtual Environment is still not installed, halting script now.\n" printf "[WGDashboard] %s Python Virtual Environment is still not installed, halting script now.\n" "$heavy_crossmark"
printf "%s\n" "$helpMsg" printf "%s\n" "$helpMsg"
else else
printf "[WGDashboard] \xE2\x9C\x94 Python Virtual Environment is installed\n" printf "[WGDashboard] %s Python Virtual Environment is installed\n" "$heavy_checkmark"
fi fi
} }
_installPythonPip(){ _installPythonPip(){
if ! $pythonExecutable -m pip -h > /dev/null 2>&1
then
case "$OS" in case "$OS" in
ubuntu|debian) ubuntu|debian)
if [ "$pythonExecutable" = "python3" ]; then
{ sudo apt update ; sudo apt-get install -y python3-pip; printf "\n\n"; } &>> ./log/install.txt { sudo apt update ; sudo apt-get install -y python3-pip; printf "\n\n"; } &>> ./log/install.txt
else
{ sudo apt update ; sudo apt-get install -y ${pythonExecutable}-distutil python3-pip; printf "\n\n"; } &>> ./log/install.txt
fi
;; ;;
centos|fedora|redhat) centos|fedora|redhat|rhel)
if command -v dnf &> /dev/null; then if [ "$pythonExecutable" = "python3" ]; then
{ sudo dnf install -y python3-pip; printf "\n\n"; } >> ./log/install.txt { sudo dnf install -y python3-pip; printf "\n\n"; } >> ./log/install.txt
else else
{ sudo yum install -y python3-pip printf "\n\n"; } >> ./log/install.txt { sudo dnf install -y ${pythonExecutable}-pip; printf "\n\n"; } >> ./log/install.txt
fi fi
;; ;;
*) *)
printf "[WGDashboard] Sorry, your OS is not support auto install. Currently the install script only support Debian-based, Red Hat-based OS." printf "[WGDashboard] %s Sorry, your OS is not supported. Currently the install script only support Debian-based, Red Hat-based OS.\n" "$heavy_crossmark"
printf "%s\n" "$helpMsg" printf "%s\n" "$helpMsg"
exit 1 kill $TOP_PID
;; ;;
esac esac
fi
if ! python3 -m pip -h > /dev/null 2>&1 if ! $pythonExecutable -m pip -h > /dev/null 2>&1
then then
printf "[WGDashboard] Python Package Manager (PIP) is still not installed, halting script now.\n" printf "[WGDashboard] %s Python Package Manager (PIP) is still not installed, halting script now.\n" "$heavy_crossmark"
printf "%s\n" "$helpMsg" printf "%s\n" "$helpMsg"
kill $TOP_PID
else else
printf "[WGDashboard] \xE2\x9C\x94 Python Package Manager (PIP) is installed\n" printf "[WGDashboard] %s Python Package Manager (PIP) is installed\n" "$heavy_checkmark"
fi fi
} }
_checkWireguard(){ _checkWireguard(){
if ! wg -h > /dev/null 2>&1 if ! wg -h > /dev/null 2>&1
then then
printf "[WGDashboard] WireGuard is not installed. Please follow instruction on https://www.wireguard.com/install/ to install. \n" printf "[WGDashboard] %s WireGuard is not installed. Please follow instruction on https://www.wireguard.com/install/ to install. \n" "$heavy_crossmark"
exit 1 kill $TOP_PID
fi fi
if ! wg-quick -h > /dev/null 2>&1 if ! wg-quick -h > /dev/null 2>&1
then then
printf "[WGDashboard] WireGuard is not installed. Please follow instruction on https://www.wireguard.com/install/ to install. \n" printf "[WGDashboard] %s WireGuard is not installed. Please follow instruction on https://www.wireguard.com/install/ to install. \n" "$heavy_crossmark"
exit 1 kill $TOP_PID
fi
}
_checkPythonVersion(){
version_pass=$($pythonExecutable -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 10) else print("0");')
version=$($pythonExecutable --version)
if [ $version_pass == "1" ]
then
return;
elif python3.10 --version > /dev/null 2>&1
then
printf "[WGDashboard] %s Found Python 3.10. Will be using [python3.10] to install WGDashboard.\n" "$heavy_checkmark"
pythonExecutable="python3.10"
elif python3.11 --version > /dev/null 2>&1
then
printf "[WGDashboard] %s Found Python 3.11. Will be using [python3.11] to install WGDashboard.\n" "$heavy_checkmark"
pythonExecutable="python3.11"
elif python3.12 --version > /dev/null 2>&1
then
printf "[WGDashboard] %s Found Python 3.12. Will be using [python3.12] to install WGDashboard.\n" "$heavy_checkmark"
pythonExecutable="python3.12"
else
printf "[WGDashboard] %s Could not find a compatible version of Python. Current Python is %s.\n" "$heavy_crossmark" "$version"
printf "[WGDashboard] WGDashboard required Python 3.10, 3.11 or 3.12. Halting install now.\n"
kill $TOP_PID
fi fi
} }
install_wgd(){ install_wgd(){
printf "[WGDashboard] Starting to install WGDashboard\n" printf "[WGDashboard] Starting to install WGDashboard\n"
_checkWireguard _checkWireguard
sudo chmod -R 755 /etc/wireguard/
if [ ! -d "log" ] if [ ! -d "log" ]
then then
@@ -177,18 +240,15 @@ install_wgd(){
printf "[WGDashboard] Python is not installed, trying to install now\n" printf "[WGDashboard] Python is not installed, trying to install now\n"
_installPython _installPython
else else
printf "[WGDashboard] \xE2\x9C\x94 Python is installed\n" printf "[WGDashboard] %s Python is installed\n" "$heavy_checkmark"
fi fi
_checkPythonVersion
_installPythonVenv _installPythonVenv
_installPythonPip _installPythonPip
version_pass=$(python3 -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 7) else print("0");')
if [ $version_pass == "0" ]
then
printf "[WGDashboard] WGDashboard required Python 3.7 or above\n"
exit 1
fi
if [ ! -d "db" ] if [ ! -d "db" ]
then then
@@ -197,7 +257,7 @@ install_wgd(){
fi fi
_check_and_set_venv _check_and_set_venv
printf "[WGDashboard] Upgrading Python Package Manage (PIP)\n" printf "[WGDashboard] Upgrading Python Package Manage (PIP)\n"
{ date; python3 -m pip install pip; printf "\n\n"; } >> ./log/install.txt { date; python3 -m pip install --upgrade pip; printf "\n\n"; } >> ./log/install.txt
printf "[WGDashboard] Installing latest Python dependencies\n" printf "[WGDashboard] Installing latest Python dependencies\n"
{ date; python3 -m pip install -r requirements.txt ; printf "\n\n"; } >> ./log/install.txt { date; python3 -m pip install -r requirements.txt ; printf "\n\n"; } >> ./log/install.txt
printf "[WGDashboard] WGDashboard installed successfully!\n" printf "[WGDashboard] WGDashboard installed successfully!\n"
@@ -236,10 +296,18 @@ gunicorn_start () {
export PATH=$PATH:/usr/local/bin:$HOME/.local/bin export PATH=$PATH:/usr/local/bin:$HOME/.local/bin
fi fi
_check_and_set_venv _check_and_set_venv
sudo "$venv_gunicorn" --access-logfile log/access_"$d".log \ sudo "$venv_gunicorn" --config ./gunicorn.conf.py
--log-level 'debug' --capture-output \ sleep 5
--error-logfile log/error_"$d".log 'dashboard:app' checkPIDExist=0
printf "[WGDashboard] Log files is under ./log\n" while [ $checkPIDExist -eq 0 ]
do
if test -f './gunicorn.pid'; then
checkPIDExist=1
printf "[WGDashboard] Checking if WGDashboard w/ Gunicorn started successfully\n"
fi
sleep 2
done
printf "[WGDashboard] WGDashboard w/ Gunicorn started successfully\n"
printf "%s\n" "$dashes" printf "%s\n" "$dashes"
} }
@@ -269,7 +337,7 @@ start_wgd_debug() {
} }
update_wgd() { update_wgd() {
new_ver=$(python3 -c "import json; import urllib.request; data = urllib.request.urlopen('https://api.github.com/repos/donaldzou/WGDashboard/releases/latest').read(); output = json.loads(data);print(output['tag_name'])") new_ver=$(venv_python -c "import json; import urllib.request; data = urllib.request.urlopen('https://api.github.com/repos/donaldzou/WGDashboard/releases/latest').read(); output = json.loads(data);print(output['tag_name'])")
printf "%s\n" "$dashes" printf "%s\n" "$dashes"
printf "[WGDashboard] Are you sure you want to update to the %s? (Y/N): " "$new_ver" printf "[WGDashboard] Are you sure you want to update to the %s? (Y/N): " "$new_ver"
read up read up
@@ -293,7 +361,6 @@ update_wgd() {
fi fi
} }
if [ "$#" != 1 ]; if [ "$#" != 1 ];
then then
help help