Compare commits

...

389 Commits

Author SHA1 Message Date
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
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
61473877a4 Update wgd.sh 2024-08-03 14:01:04 -04:00
Donald Zou
52989c8f5c Fixed clearInterval is not working within configuration 2024-08-03 13:26:16 -04:00
Donald Zou
b64ba2ef16 Fixed Gunicorn global variable issue 2024-08-03 13:25:57 -04:00
Donald Zou
461ae99dd8 Testing again... 2024-08-02 21:48:42 -04:00
Donald Zou
8681df6f02 Seems like Gunicorn need global 2024-08-02 21:45:55 -04:00
Donald Zou
ba081ee442 Update wgd.sh 2024-08-02 21:21:19 -04:00
Donald Zou
cf90d05115 Update wgd.sh 2024-08-02 18:30:16 -04:00
Donald Zou
658c6554af Update wgd.sh 2024-08-02 18:18:31 -04:00
Donald Zou
94d9d608f7 Update wgd.sh 2024-08-02 18:16:34 -04:00
Donald Zou
015b50be5f Doing some testing across distros 2024-08-02 17:27:28 -04:00
Donald Zou
85970f8c96 New build 2024-07-31 02:27:44 -04:00
Donald Zou
881d62d69d The UI and backend of API keys is done! 2024-07-30 18:45:05 -04:00
Donald Zou
935129f0a5 Finished the log system, now move on to something else... 2024-07-29 22:17:51 -04:00
Donald Zou
63e8553a09 Finished job logs :) 2024-07-29 18:40:07 -04:00
Donald Zou
b65828416f Job logs api are done, still need to build the UI to view logs 2024-07-27 18:51:43 -04:00
Donald Zou
48dc8033f5 Schedule system is finally running, still need to more testing :) 2024-07-01 00:58:02 +08:00
Donald Zou
2d838b69fd Peer schedule style is almost done
But I don't feel it quite right..
2024-06-25 23:02:13 +08:00
Donald Zou
6c529a6908 Fixed Gunicorn issue, continue on Peer Schedule Job 2024-06-19 17:09:58 +08:00
Donald Zou
9baefec541 Figuring out with Gunicorn... 2024-06-18 03:40:25 +08:00
Donald Zou
327d66bb80 Removed a tons of files :) 2024-06-18 03:16:42 +08:00
Donald Zou
760a4dfcb9 Still working on schedule.. 2024-06-16 23:32:32 +08:00
Donald Zou
8ed75d1d21 Working on Job Scheduling 2024-06-16 15:40:10 +08:00
Donald Zou
54710b8221 Update .gitignore 2024-06-15 23:14:48 +08:00
Donald Zou
ff794033e1 Finished Traceroute 2024-06-13 16:56:06 +08:00
Donald Zou
f0f486da9e Adjusted some UI and finished the Ping function 2024-06-12 17:54:29 +08:00
Donald Zou
eb18857ecc Some UI adjustment 2024-06-03 02:16:09 +08:00
Donald Zou
9a280e99ad Finished restrict & allow access of peers 2024-06-02 23:22:43 +08:00
Donald Zou
c7ca20b45a Made some progress ;) 2024-05-20 22:28:52 +08:00
Donald Zou
41e05ddf9c Update dashboard_new.py
Yay adding peers in bulk is finally done ;)
2024-05-14 00:58:01 +08:00
Donald Zou
5a34f16dcf Update dashboard_new.py
Typo in `updatePeer`
2024-05-13 22:17:00 +08:00
Donald Zou
769ca4e26d Kind of finished revamping add peers
Still need to clean some of the codes but overall is good :)
2024-05-12 00:39:17 +08:00
Donald Zou
57c2e89f00 Fixing some of the issue from users ;) 2024-04-26 00:03:42 +08:00
Donald Zou
914a0bf514 Minor updates... 2024-04-03 01:16:56 -04:00
Donald Zou
75fbdb653a Update build 2024-03-24 22:55:19 -04:00
Donald Zou
bdfe75cff3 Finished download & QR Code 2024-03-24 22:46:32 -04:00
Donald Zou
bcd845fd59 Finished revamping peer edit 2024-03-24 18:24:01 -04:00
Donald Zou
f1e71ecb78 20240323 Commit
Finished implementing peer settings dropdown and planned how peer settings will be link
2024-03-21 02:03:08 -04:00
Donald Zou
0aa4c8af6f Continue to work on v4 ;0 2024-03-11 00:11:07 -04:00
Donald Zou
a950b80d5a v4 branch first commit! 2024-02-27 18:37:49 -05:00
Donald Zou
ed3bb6429b Finished some building blocks on the new version 2024-02-26 10:22:33 -05:00
Donald Zou
1e88491ca1 I am giving up on using ORM...
Lets go back to the good old sql query days ;)
2024-02-11 23:53:51 -05:00
Donald Zou
6b6ad05e3a The UI for New Configuration is done 2024-01-31 12:06:44 -05:00
Donald Zou
5f4a364095 Huge update
A welcome session
Added Time based One-Time-Passcode (TOTP)
UI Update
2024-01-23 15:09:44 -05:00
Donald Zou
95a8867527 Update .gitignore 2024-01-11 13:47:35 -05:00
Donald Zou
7cb1301e80 Continue to refactor the UI and APIs :) 2024-01-11 13:46:08 -05:00
Donald Zou
e6e070d89e Continue to refactor the UI and APIs :) 2024-01-11 01:10:26 -05:00
Donald Zou
ba2bcaba07 Finally figured out SQLAlchemy and started to re-write some of the APIs. The UI will completely handle by JS with Vue. There will be no more templating from flask to minimize the resource usage ;) 2024-01-10 01:42:19 -05:00
Donald Zou
864f82ba11 Started to refactor dashboard.py with dashboard_new.py and trying really hard to figure out sqlalchemy lol 2024-01-09 00:25:47 -05:00
Donald Zou
f671c992e1 testing something... 2024-01-08 12:23:57 -05:00
Donald Zou
0c0bce9755 Combining Vue.js!!! How exciting!
Adding Vue.js to handle frontend changes, leaving the server only need to response json data. Ditching flask template and hope it can reduce the memory and cpu usage :)
2023-11-30 09:42:02 -05:00
Donald Zou
f07508073f A new update to the dev branch 2023-11-28 16:37:16 -05:00
Donald Cheng Hong Zou
e06cc1bd2d Finally finished theme switching!!!! 2022-04-23 00:34:11 -04:00
Donald Cheng Hong Zou
36e33a4c10 Adjust the dark mode theme for PWA 2022-04-22 16:21:16 -04:00
Donald Cheng Hong Zou
7f668c1653 Some changes to dark mode css 2022-04-22 00:12:22 -04:00
Donald Zou
b464fa98df Merge pull request #173 from LeoEricson/v3.1-dev
Add dark theme
2022-04-21 22:33:40 -04:00
LeoEricson
23491f1e8c Add dark theme 2022-04-21 21:30:55 +00:00
Donald Cheng Hong Zou
2b90a2eed2 Update header.html 2022-04-21 15:11:35 -04:00
Donald Cheng Hong Zou
13b9d15d8f New darkmode theme 2022-04-21 15:11:01 -04:00
Donald Cheng Hong Zou
a053504bb8 Make the dashboard more mobile friendly 2022-04-11 20:01:29 -04:00
Donald Cheng Hong Zou
dcdd4aec85 Update Peer Data Usage Graph 2022-04-10 09:26:54 -04:00
Donald Cheng Hong Zou
179da2ac05 Finished peer data usage chart 2022-04-06 20:59:23 -04:00
Donald Cheng Hong Zou
4848739b6e Added dockerfile! Thanks @ikidd! 2022-04-05 21:39:47 -04:00
Donald Cheng Hong Zou
46da285831 Adjusted js 2022-03-30 14:20:08 -04:00
Donald Cheng Hong Zou
71a6a36a54 Update dashboard.css 2022-03-30 14:06:05 -04:00
Donald Cheng Hong Zou
c8ca9ef7ab Minimized some js code 2022-03-30 00:54:11 -04:00
Donald Cheng Hong Zou
5af2fff9ca IPv6 configuration IP should be working now 2022-03-29 15:11:50 -04:00
Donald Cheng Hong Zou
337c9bc01e Update dashboard.py 2022-03-28 15:33:26 -04:00
Donald Cheng Hong Zou
a196dce1fa Removed flask-socketio 2022-03-28 08:09:28 -04:00
Donald Cheng Hong Zou
b9633bbcd6 Finished implementing add/delete config 2022-03-24 20:43:56 -04:00
Donald Cheng Hong Zou
46efe2b8dd Finished developing add config 2022-03-24 02:10:52 -04:00
Donald Cheng Hong Zou
cc1dd682e8 Add configuration and adjusted redirect functionality 2022-03-22 16:13:57 -04:00
Donald Cheng Hong Zou
bdd984a887 Brand new switch button and toast UI 2022-03-21 22:33:19 -04:00
Donald Cheng Hong Zou
2d3dffe5fc Moved refresh interval and display mode to localStorage 2022-03-07 09:29:47 -05:00
Donald Cheng Hong Zou
65f31a0b38 Gave up using WebSocket
Flask-SocketIO does not fully compatible with Gunicorn, and it limited to 1 worker, which it will takes forever to finish loading the webpage. Switched back to  using ajax.
2022-03-04 22:09:01 -05:00
Donald Cheng Hong Zou
4a1a6c5933 Testing 2022-03-04 10:06:14 -05:00
Donald Cheng Hong Zou
7e1fd99c37 Fixed chart and updated requirement.txt 2022-03-04 08:28:54 -05:00
Donald Cheng Hong Zou
8fe8209580 Added line graph using chart.js & Improving websocket.
Added line graph to show total receive & total sent changes per refresh interval, using chart.js line chart to show the changes. Switching configuration don't need to refresh anymore, by using websocket.
2022-03-03 08:46:23 -05:00
Donald Cheng Hong Zou
264a050360 Temp fix 2022-02-28 13:34:46 -05:00
Donald Cheng Hong Zou
3623104e3b Merge branch 'main' into hmm-what-about-socket 2022-02-28 13:31:06 -05:00
Donald Cheng Hong Zou
191ff1abec Merge branch 'main' of https://github.com/donaldzou/WGDashboard 2022-02-28 13:29:25 -05:00
Donald Cheng Hong Zou
3bb86493cc Minor changes 2022-02-28 13:29:17 -05:00
Donald Zou
d1d3151e1e Update README.md 2022-02-27 15:50:55 -05:00
Donald Cheng Hong Zou
f9dc9ebdb3 Removed some unnecessary comments 2022-02-11 09:35:58 -05:00
Donald Cheng Hong Zou
3b478bcc2d OneDrive gone again.... 2022-02-04 21:36:46 -05:00
Donald Cheng Hong Zou
77bb78c381 OneDrive caused my whole project folder gone.... 2022-02-04 11:44:50 -05:00
Donald Cheng Hong Zou
cafa97f502 Changed version number 2022-01-31 16:09:50 -05:00
Donald Cheng Hong Zou
0d858493d5 Update README.md 2022-01-31 09:56:38 -05:00
Donald Cheng Hong Zou
99fb07c6b3 Fixed public key error with existed private key 2022-01-31 09:38:05 -05:00
Donald Zou
eaad971c0a Update README.md 2022-01-24 22:06:38 -05:00
Donald Cheng Hong Zou
377abd87fd Commit 2022-01-24 20:11:35 -05:00
Donald Zou
af71176296 Merge pull request #133 from donaldzou/v3.0.3-pr
v3.0.3
2022-01-23 19:34:06 -05:00
Donald Cheng Hong Zou
69737177ef Changed version number to v3.0.3 2022-01-23 19:30:43 -05:00
Donald Cheng Hong Zou
efae1222c1 Update dashboard.py 2022-01-23 19:25:12 -05:00
Donald Cheng Hong Zou
3caea1a903 Update README.md 2022-01-23 19:21:43 -05:00
Donald Cheng Hong Zou
a43478d627 Fixed no init_dashboard() for debug mode the first time 2022-01-23 19:19:38 -05:00
Donald Cheng Hong Zou
ed2c3f43c7 Added tracking loading time 2022-01-19 10:43:39 -05:00
Donald Cheng Hong Zou
84c9846f7b Hmmmm... What about SocketIO? 2022-01-19 10:27:17 -05:00
Donald Cheng Hong Zou
fcfd816cec Remove pylint 2022-01-19 09:25:27 -05:00
Donald Cheng Hong Zou
2e3977e59c commit 2022-01-19 09:18:03 -05:00
Donald Cheng Hong Zou
3c68430336 Update pylint.yml 2022-01-19 09:12:57 -05:00
Donald Cheng Hong Zou
43afb86fa8 Update pylint.yml 2022-01-19 09:10:59 -05:00
Donald Cheng Hong Zou
194ccbdbb4 Update pylint.yml 2022-01-19 09:10:07 -05:00
Donald Cheng Hong Zou
7139e230cf Commit 2022-01-19 09:08:27 -05:00
Donald Zou
120d3b9f54 Update pylint.yml 2022-01-19 09:05:39 -05:00
Donald Zou
712460a040 Update pylint.yml 2022-01-19 09:03:35 -05:00
Donald Zou
be5594f1c9 Create pylint.yml 2022-01-19 09:01:35 -05:00
Donald Cheng Hong Zou
0a885117db Update README.md 2022-01-18 13:04:00 -05:00
Donald Zou
5b73654544 Update README.md 2022-01-18 12:43:37 -05:00
Donald Cheng Hong Zou
1485b78b7b Merge branch 'main' of https://github.com/donaldzou/WGDashboard 2022-01-18 12:43:20 -05:00
Donald Cheng Hong Zou
bfec57172a Update README.md 2022-01-18 12:43:11 -05:00
Donald Zou
9f8559c12d Update README.md 2022-01-18 12:34:40 -05:00
Donald Zou
83dde12ca9 Merge pull request #126 from donaldzou/Migrate-to-SQLite
Production branch for v3.0
2022-01-18 12:33:34 -05:00
Donald Cheng Hong Zou
d17888db4e Update README.md 2022-01-18 12:33:21 -05:00
Donald Cheng Hong Zou
096529af96 Experimental Functions 2022-01-18 12:16:10 -05:00
Donald Cheng Hong Zou
8a56b22635 Added manifest.json 2022-01-18 11:02:19 -05:00
Donald Cheng Hong Zou
dcdd353981 Removed unnecessary files 2022-01-18 10:49:41 -05:00
Donald Cheng Hong Zou
258564208b Finalizing everything. 2022-01-18 10:42:23 -05:00
Donald Cheng Hong Zou
e2e8222005 Upload manifest.json 2022-01-16 20:35:24 -05:00
Donald Cheng Hong Zou
75f2826805 Merge branch 'pr/93' into Migrate-to-SQLite 2022-01-13 09:44:47 -05:00
Donald Cheng Hong Zou
730af4f2ee Finishing documentation 2022-01-13 09:37:23 -05:00
Donald Zou
e5737ebd9d Merge branch 'main' into Migrate-to-SQLite 2022-01-12 21:41:41 -05:00
Donald Cheng Hong Zou
7f24904164 Bugs 2022-01-12 20:28:12 -05:00
Donald Cheng Hong Zou
9ff7198602 Still finalizing everything 2022-01-12 19:53:36 -05:00
Donald Cheng Hong Zou
6d56967a0f Getting ready for v3.0 2022-01-08 15:26:17 -05:00
Donald Cheng Hong Zou
584118805a Feature Added
Moved key generating to front-end, by using wireguard.js from WireGuard's official repository. Added "Add Peers by bulk" feature.
2022-01-06 15:17:43 -05:00
Donald Zou
68b3f01082 Merge pull request #127 from pgalonza/refactoring
Fixed misprint
2022-01-04 16:41:26 -05:00
Donald Cheng Hong Zou
77a82cb84b Finished adding available IP and related UI adjustment 2022-01-04 16:32:23 -05:00
Galonza Peter
844b19495d fixed misprint 2022-01-03 00:51:22 +03:00
Donald Cheng Hong Zou
b8b721f2bd Delete wgdashboard.db 2022-01-02 14:45:24 -05:00
Donald Cheng Hong Zou
4f93d48ac6 Successfully migrate to SQlite 2022-01-02 14:44:27 -05:00
Donald Zou
02d1bf5a2c Merge pull request #125 from pgalonza/refactoring
Refactoring
2022-01-02 14:41:24 -05:00
Galonza Peter
fb3606c2b7 Refactored
Added docstrings like sphinx style, small refactoring and added TODO
2022-01-02 16:35:39 +03:00
Galonza Peter
a5bc2cd48f wg_conf_path is global variable 2022-01-01 03:20:30 +03:00
Galonza Peter
72151ac6a3 db path is global variable 2022-01-01 02:57:59 +03:00
Donald Cheng Hong Zou
e2c8b3bf02 Removed Semaphore for configuration file 2021-12-31 13:29:37 -05:00
Donald Cheng Hong Zou
06dd641274 Minified all CSS and JS script 2021-12-30 15:21:25 -05:00
Donald Cheng Hong Zou
7d9bf01d27 Cleaned configuration.js
Removed unused functions and duplicated jQuery selector
2021-12-29 23:26:15 -05:00
Donald Cheng Hong Zou
b9553e0a05 Update dashboard.py
Finished refactoring some variable name
2021-12-29 16:15:00 -05:00
Donald Cheng Hong Zou
993dcaf235 Merge branch 'main' of https://github.com/donaldzou/WGDashboard 2021-12-29 15:30:52 -05:00
Donald Cheng Hong Zou
8d7c39bac4 Update dashboard.py
Wrapped all reading and writing to dashboard configuration into functions
2021-12-29 15:29:29 -05:00
Donald Zou
80933ab571 Merge pull request #124 from pgalonza/fix-variables
Fixed variables name
2021-12-29 14:57:53 -05:00
Donald Cheng Hong Zou
b5b30c8119 Update dashboard.py 2021-12-29 14:57:44 -05:00
Galonza Peter
bca06129d9 fixed variables name 2021-12-29 22:56:19 +03:00
Donald Zou
a136b86d81 Merge pull request #122 from pgalonza/change-path
Fixed variable name
2021-12-29 14:20:52 -05:00
Donald Zou
8d555d8ae7 Merge pull request #121 from pgalonza/refactoring
Refactoring
2021-12-29 13:57:21 -05:00
Donald Zou
c22e61add2 Merge branch 'main' into refactoring 2021-12-29 13:57:11 -05:00
Donald Zou
2c83f1ce57 Merge pull request #120 from pgalonza/fix-download
Fixed download configuration file for peer
2021-12-29 12:21:29 -05:00
Donald Cheng Hong Zou
f2750de153 Feature adding and UI adjustment
Added Preshared-Key to all configuration
2021-12-29 12:17:44 -05:00
Galonza Peter
e0934f11d6 Merge branch 'main' into fix-download 2021-12-29 02:00:36 +03:00
Galonza Peter
422924026a fixed variable name 2021-12-29 02:00:18 +03:00
Galonza Peter
332e22e534 Merge branch 'main' into refactoring
# Conflicts:
#	src/dashboard.py
2021-12-29 01:51:41 +03:00
Galonza Peter
c7bf30e18e pylint warnings 2021-12-29 00:59:10 +03:00
Galonza Peter
5ade5176c0 fixed download configuration file for peer 2021-12-28 22:55:52 +03:00
Donald Zou
f9e4fafe9f Merge pull request #96 from pgalonza/change-path
Added the ability to change the path for files to be saved
2021-12-27 21:05:34 -05:00
Donald Zou
201f2ef1f0 Merge branch 'main' into change-path 2021-12-27 21:05:25 -05:00
Donald Zou
32af4fdcca Merge pull request #119 from donaldzou/v3.0-beta
Finishing v3.0
2021-12-27 21:01:24 -05:00
Donald Cheng Hong Zou
d254b4cc39 Finishing v3.0 2021-12-27 21:01:02 -05:00
Galonza Peter
d0ad4e05bf general refactoring 2021-12-28 00:29:26 +03:00
Galonza Peter
c75854cb7b fixed after merge 2021-12-27 00:35:10 +03:00
Galonza Peter
8cc71913b3 fixed read configuration after merge 2021-12-26 23:17:19 +03:00
Galonza Peter
99abe6bcd2 Merge branch 'main' into change-path 2021-12-26 12:58:44 +03:00
Galonza Peter
f5ad7a49ce general refactoring 2021-12-26 12:57:57 +03:00
Donald Cheng Hong Zou
e42ecf3c99 Update wgd.sh 2021-12-25 17:45:47 -05:00
Donald Cheng Hong Zou
b58bf9dc17 Update wgd.sh 2021-12-25 17:40:53 -05:00
Donald Cheng Hong Zou
745c8aaf3e Update wgd.sh 2021-12-25 17:40:08 -05:00
Donald Cheng Hong Zou
0ca73bf80b Update wgd.sh 2021-12-25 17:38:13 -05:00
Donald Cheng Hong Zou
fd76cabd01 Update wgd.sh 2021-12-25 17:37:43 -05:00
Donald Cheng Hong Zou
fcf9300a49 Update wgd.sh 2021-12-25 17:35:36 -05:00
Donald Cheng Hong Zou
649b66ba1a Update wgd.sh 2021-12-25 17:20:52 -05:00
Donald Cheng Hong Zou
b5d73a0861 Delete hi.txt 2021-12-25 17:20:31 -05:00
Donald Cheng Hong Zou
1453996ccb Update wgd.sh 2021-12-25 17:16:50 -05:00
Donald Cheng Hong Zou
5bf9b97d70 Update wgd.sh 2021-12-25 17:15:33 -05:00
Donald Cheng Hong Zou
5c1e46f5a6 Update wgd.sh 2021-12-25 17:13:21 -05:00
Donald Cheng Hong Zou
c2665cbae8 Update wgd.sh 2021-12-25 17:13:08 -05:00
Donald Cheng Hong Zou
3934248a6b Update wgd.sh 2021-12-25 17:08:59 -05:00
Donald Cheng Hong Zou
7ae3bb5601 Testing update function 2021-12-25 17:07:33 -05:00
Donald Cheng Hong Zou
9890d4f3bc Removed using venv for now 2021-12-25 16:29:33 -05:00
Donald Cheng Hong Zou
5b47fc16d9 Update wgd.sh 2021-12-25 16:08:30 -05:00
Galonza Peter
344770a0a4 Merge branch 'main' into change-path
# Conflicts:
#	src/dashboard.py
2021-12-25 23:42:10 +03:00
Donald Cheng Hong Zou
fc11cacd24 Update wgd.sh 2021-12-25 15:20:56 -05:00
Donald Cheng Hong Zou
737ebdd0c6 Update wgd.sh 2021-12-25 15:20:04 -05:00
Galonza Peter
d06dddfa24 Merge branch 'main' into change-path 2021-12-25 23:17:40 +03:00
Donald Cheng Hong Zou
710d631ffe Update wgd.sh 2021-12-25 15:17:07 -05:00
Donald Cheng Hong Zou
7d9a845ca0 Update wgd.sh 2021-12-25 14:48:38 -05:00
Donald Cheng Hong Zou
cc6e5ffb97 Update wgd.sh 2021-12-25 14:46:47 -05:00
Donald Cheng Hong Zou
8d29cb7488 Testing Python venv 2021-12-25 14:44:14 -05:00
Donald Cheng Hong Zou
5c588ea01a Style adjustment
Adjusted style and the status light: Added box-shadow and border for connected peer
2021-12-24 00:20:44 -05:00
Donald Zou
dade9935da Merge pull request #117 from donaldzou/v3.0-Slow-Load-Fix
Slow Performance Fixed
2021-12-24 00:19:26 -05:00
Donald Cheng Hong Zou
34cd3aaaca Slow Performance Fixed 2021-12-23 21:26:24 -05:00
Donald Cheng Hong Zou
78d3f338fb Update dashboard.py 2021-12-22 11:36:29 -05:00
Donald Zou
d801c5911e Merge pull request #91 from pgalonza/todo-ipv6
Add IPv6
2021-12-22 11:35:17 -05:00
Galonza Peter
003743c074 Merge branch 'main' into gunicorn 2021-12-21 22:41:03 +03:00
Galonza Peter
4cef39d3b1 Merge branch 'main' into gunicorn-tmp
# Conflicts:
#	src/wgd.sh
2021-12-21 22:38:31 +03:00
Galonza Peter
83f0df32d7 added thread 2021-12-21 22:23:26 +03:00
Galonza Peter
24f269191c added Let’s Encrypt via certbot 2021-12-21 22:23:13 +03:00
Galonza Peter
94a0d5a0a4 refactored 2021-12-21 22:23:05 +03:00
Galonza Peter
c0fbf4dd0c optimized performance
# Conflicts:
#	src/gunicorn.conf.py
2021-12-21 22:22:37 +03:00
Galonza Peter
37b6161075 fixed check gunicorn process 2021-12-21 22:20:43 +03:00
Galonza Peter
c9b792c370 fixed update variable 2021-12-21 22:20:31 +03:00
Galonza Peter
2103d547a1 fixed initiation for gunicorn 2021-12-21 22:18:36 +03:00
Galonza Peter
3918e220d2 style 2021-12-21 22:17:32 +03:00
Galonza Peter
c3eaaed43b fixed stop 2021-12-21 22:17:07 +03:00
Galonza Peter
e0bf648076 added paths when command as root 2021-12-21 22:16:40 +03:00
Galonza Peter
bb298164e4 added gunicorn in requirements 2021-12-21 22:15:55 +03:00
Galonza Peter
898694b9be fixed gunicorn start and added stop 2021-12-21 22:15:41 +03:00
Galonza Peter
f0f9ac92e6 added gunicorn start 2021-12-21 22:15:23 +03:00
Galonza Peter
ea0229a8ab added wsgi for gunicorn 2021-12-21 22:15:08 +03:00
Donald Zou
66fc7cd91d Merge pull request #112 from reafian/main
Added MTU and PersistentKeepalive to QR code and download files
2021-12-21 14:12:42 -05:00
Donald Zou
c11d4f01b1 Merge pull request #114 from donaldzou/all-contributors/add-marneu
docs: add marneu as a contributor for code
2021-12-21 13:54:03 -05:00
Donald Zou
a8f498f292 Merge pull request #113 from donaldzou/marneu-main
Marneu main
2021-12-21 13:53:51 -05:00
allcontributors[bot]
6854e2275f docs: update .all-contributorsrc [skip ci] 2021-12-21 18:53:40 +00:00
allcontributors[bot]
6662e79ac5 docs: update README.md [skip ci] 2021-12-21 18:53:39 +00:00
Donald Cheng Hong Zou
9e77430634 Added Slash 2021-12-21 13:52:08 -05:00
Richard Newton
b8e070824a Update get_conf.html
Added MTU and PersistentKeepalive back into QR Code.

Update dashboard.py

Added MTU and PersistentKeepalive to downloaded file.
2021-12-21 16:48:11 +00:00
Donald Cheng Hong Zou
304d4293f3 Update wgd.sh 2021-12-20 19:54:53 -05:00
Donald Zou
a03e7f5b53 Merge pull request #111 from donaldzou/marneu-main
Marneu main
2021-12-19 18:18:50 -05:00
Donald Zou
ea57c8cad4 Merge branch 'main' into marneu-main 2021-12-19 18:18:45 -05:00
Donald Cheng Hong Zou
280802cf74 Update .gitignore 2021-12-19 18:16:45 -05:00
Donald Cheng Hong Zou
fbce01d846 Update .gitignore 2021-12-19 18:15:51 -05:00
Markus Neubauer
591b60c336 Update wgd.sh
a more automated approach to systemctl
2021-12-19 18:13:15 -05:00
Markus Neubauer
78ef870d91 Update wg-dashboard.service
A more automated approach
2021-12-19 18:13:15 -05:00
Donald Zou
bc19d82aed Merge pull request #105 from marneu/patch-1
An automated way to a systemctl unit
2021-12-19 18:07:35 -05:00
Donald Zou
79b41e1056 Update README.md 2021-12-06 14:15:22 -05:00
Markus Neubauer
805f03d231 Update .gitignore 2021-11-25 16:03:09 +01:00
Donald Zou
62acc1081f Update README.md 2021-11-19 14:48:46 -05:00
Donald Zou
257a2c2d9f Update README.md 2021-11-19 14:45:37 -05:00
Donald Zou
ed9b05cdf9 Update README.md 2021-11-19 14:44:40 -05:00
Donald Zou
2cc6fbd643 Update README.md 2021-11-10 11:40:46 -05:00
Galonza Peter
bdfc260dd2 fixed 2021-10-24 13:32:21 +03:00
Galonza Peter
7ad65956c9 fixed, if dir is exists 2021-10-24 12:45:08 +03:00
Galonza Peter
b3dc60b7ca fixed check ipv6 dns 2021-10-24 12:05:14 +03:00
Galonza Peter
bed08109fa not needed 2021-10-24 02:10:20 +03:00
Galonza Peter
06cedec090 mistake 2021-10-24 01:58:59 +03:00
Galonza Peter
1d2c468d03 fixed, when database does not create, if no directory 2021-10-24 01:51:04 +03:00
Galonza Peter
e84616f5f5 added the ability to change the path for files to be saved 2021-10-24 00:56:34 +03:00
Galonza Peter
caa9a90476 added dual-stack 2021-10-17 22:55:08 +03:00
Galonza Peter
62be683dd8 fixed search ipv6 in the string with comma 2021-10-17 21:13:18 +03:00
Galonza Peter
ffa63b3f82 completed todo. Added IPv6 2021-10-17 20:22:27 +03:00
Galonza Peter
c5a7788394 optimized pattern 2021-10-17 20:01:16 +03:00
Donald Zou
99c42ff2c1 Merge pull request #90 from pike00/pike00-update-readme
Updated README to include username/password
2021-10-14 10:14:38 -04:00
Will Pike
fd2b7bb917 Updated README to include username/password
Updated Installation instructions to include default username/password in step 6 on login. It is located in the 'Dashboard Configuration' section, however for readability/ease of setting up the server, I added the default login to the install instructions.
2021-10-14 10:10:17 -04:00
Donald Cheng Hong Zou
922a494df0 Finished 2021-09-09 12:50:38 -04:00
Donald Cheng Hong Zou
c478ec0095 Update README.md 2021-09-09 12:45:09 -04:00
Donald Cheng Hong Zou
3abbe17910 Naming changed 2021-09-09 12:43:49 -04:00
Donald Zou
349076599f Merge pull request #80 from ikidd/patch-1
Fixed service unit file spelling error
2021-09-09 12:35:44 -04:00
ikidd
92d299507f Fixed service unit file spelling error 2021-09-08 20:36:53 -06:00
Donald Cheng Hong Zou
6f2d61e918 Update README.md 2021-09-08 22:02:33 -04:00
Donald Cheng Hong Zou
ed13babf6a Merge branch 'main' of https://github.com/donaldzou/Wireguard-Dashboard into main 2021-09-08 22:01:01 -04:00
Donald Cheng Hong Zou
80db82eb93 Update dashboard.py 2021-09-08 22:00:53 -04:00
Donald Zou
8d20334d6e Update README.md 2021-09-08 21:59:15 -04:00
Donald Zou
03a95ad018 Merge pull request #79 from donaldzou/v2.3.1
Update README.md
2021-09-08 21:58:02 -04:00
Donald Cheng Hong Zou
aa59cc8af0 Update README.md 2021-09-08 21:57:50 -04:00
Donald Zou
80d63c2c89 Merge pull request #78 from donaldzou/v2.3.1
Changed dashboard naming to WGDashboard
2021-09-08 21:56:48 -04:00
Donald Cheng Hong Zou
03a4acd72b Changed dashboard naming to WGDashboard 2021-09-08 21:56:31 -04:00
Donald Cheng Hong Zou
53118cf660 Merge branch 'main' of https://github.com/donaldzou/Wireguard-Dashboard into main 2021-09-08 16:58:40 -04:00
Donald Cheng Hong Zou
16b1baf81e Update README.md 2021-09-08 16:58:36 -04:00
Donald Zou
960e0d0965 Update README.md 2021-09-08 14:50:26 -04:00
Donald Zou
8f41222e43 Merge pull request #77 from donaldzou/v2.3
Update README.md
2021-09-08 12:43:09 -04:00
Donald Cheng Hong Zou
e7bc447166 Update README.md 2021-09-08 12:42:50 -04:00
Donald Zou
1736359615 Merge pull request #76 from donaldzou/v2.3
V2.3
2021-09-08 12:42:11 -04:00
Donald Cheng Hong Zou
58784b1e22 v2.3 Final Commit 2021-09-08 12:39:25 -04:00
Donald Zou
07f2bfd279 Merge pull request #75 from donaldzou/all-contributors/add-davejlong
docs: add davejlong as a contributor for code
2021-09-08 11:24:04 -04:00
allcontributors[bot]
6d62db2890 docs: update .all-contributorsrc [skip ci] 2021-09-08 15:23:38 +00:00
allcontributors[bot]
a1b23c4aab docs: update README.md [skip ci] 2021-09-08 15:23:37 +00:00
Donald Zou
e272a096da Merge pull request #74 from donaldzou/all-contributors/add-reafian
docs: add reafian as a contributor for code
2021-09-05 16:38:57 -04:00
allcontributors[bot]
b8ca24c5bf docs: update .all-contributorsrc [skip ci] 2021-09-05 20:38:34 +00:00
allcontributors[bot]
a05d09f0de docs: update README.md [skip ci] 2021-09-05 20:38:33 +00:00
Donald Zou
1807a79c4f Create FUNDING.yml 2021-09-04 17:16:31 -04:00
Donald Cheng Hong Zou
77f6826af9 Adjusted UI and edit readme 2021-09-03 17:32:51 -04:00
Donald Cheng Hong Zou
7e39eedf35 Update wgd.sh 2021-09-03 14:40:57 -04:00
Donald Cheng Hong Zou
0b4b48ceae Update wgd.sh 2021-09-03 14:30:13 -04:00
Donald Cheng Hong Zou
dad931e898 Update wgd.sh 2021-09-03 14:26:27 -04:00
Donald Cheng Hong Zou
2e387730cb Update wgd.sh 2021-09-03 14:26:05 -04:00
Donald Cheng Hong Zou
c9b6775793 Update wgd.sh 2021-09-03 14:24:38 -04:00
Donald Cheng Hong Zou
a6eaf30c2b Update wgd.sh 2021-09-03 14:24:14 -04:00
Donald Cheng Hong Zou
6f91d199d8 Update wgd.sh 2021-09-03 14:23:08 -04:00
Donald Cheng Hong Zou
7ea486307a Update wgd.sh 2021-09-03 14:22:14 -04:00
Donald Cheng Hong Zou
37d254c827 Update wgd.sh 2021-09-03 13:55:41 -04:00
Donald Cheng Hong Zou
b4014eb317 Update wgd.sh 2021-09-03 13:55:03 -04:00
Donald Cheng Hong Zou
c7824fa184 Update wgd.sh 2021-09-03 13:52:23 -04:00
Donald Cheng Hong Zou
13c5bd45f9 Update wgd.sh 2021-09-02 22:52:22 -04:00
Donald Cheng Hong Zou
465d102bd0 Update wgd.sh 2021-09-02 22:52:01 -04:00
Donald Cheng Hong Zou
93007cd56c Update wgd.sh 2021-09-02 22:27:05 -04:00
Donald Cheng Hong Zou
8367576dc8 Update wgd.sh 2021-09-02 22:24:40 -04:00
Donald Cheng Hong Zou
dbc06145b3 Update wgd.sh 2021-09-02 22:24:07 -04:00
Donald Cheng Hong Zou
4efcf0e7c8 Update wgd.sh 2021-09-02 22:13:00 -04:00
Donald Cheng Hong Zou
0a4bbc1106 Update wgd.sh 2021-09-02 22:09:06 -04:00
Donald Cheng Hong Zou
b08c1f6699 Updated requirement.txt and wgd.sh 2021-09-02 21:56:50 -04:00
Donald Cheng Hong Zou
6828b905ef Added checking if peers removed directly from config file by not from dashboard, and fixed DNS did not get check from editing peers 2021-08-24 21:04:01 -04:00
Donald Zou
f72b405f6d Merge pull request #64 from Kofl/patch-1
Update typo3 fix
2021-08-24 20:19:22 -04:00
Donald Zou
27ae99864c Merge pull request #71 from donaldzou/v2.2.2
v2.2.2 Merge
2021-08-24 20:16:21 -04:00
Donald Cheng Hong Zou
82ba15ba5a Small fix on HTML files 2021-08-24 20:15:28 -04:00
Kofl
af01db7bc9 Update typo3 fix
TOTAL RECIEVED replaced with TOTAL RECEIVED
2021-08-18 21:46:06 +02:00
Donald Cheng Hong Zou
6ec0a65d0c Update README.md 2021-08-16 15:32:14 -04:00
Donald Zou
c32ccfc9ed Merge pull request #51 from donaldzou/v2.2.1
V2.2.1
2021-08-16 15:29:49 -04:00
Donald Cheng Hong Zou
ef818388c8 Update dashboard.py 2021-08-16 15:28:43 -04:00
Donald Cheng Hong Zou
f5aa45a3e1 Update README.md 2021-08-16 15:27:10 -04:00
Donald Cheng Hong Zou
b7a047a743 Bug fixed 2021-08-16 15:26:15 -04:00
Donald Zou
05ec985453 Update README.md 2021-08-16 12:23:56 -04:00
Donald Cheng Hong Zou
88ed9c3e79 Update README.md 2021-08-15 02:23:35 -04:00
Donald Zou
3fb03c8dfe Update README.md 2021-08-15 00:16:36 -04:00
Donald Zou
ff304027cd Merge pull request #46 from donaldzou/v2.2-beta
V2.2 pull request
2021-08-14 23:57:07 -04:00
Donald Cheng Hong Zou
3140d46c94 Finalizing the documentation 2021-08-14 23:43:30 -04:00
Donald Cheng Hong Zou
9d476af384 v2.2beta almost done!!!!! 2021-08-14 23:30:05 -04:00
Donald Cheng Hong Zou
0d380672f3 v2.2 beta 4 2021-08-14 17:13:16 -04:00
Donald Cheng Hong Zou
e2fb8dca5b Update README.md 2021-08-06 00:08:30 -04:00
Donald Cheng Hong Zou
eaba4c90c5 Edit Readme 2021-08-06 00:00:32 -04:00
Donald Cheng Hong Zou
b4ca7373f3 Update Readme 2021-08-05 23:47:18 -04:00
Donald Cheng Hong Zou
905c068ed5 v2.2-beta4
Finished testing the QR code function and finish debugging the peer file download function.
2021-08-05 23:15:50 -04:00
Donald Cheng Hong Zou
02886dd420 Remove public and private key txt 2021-08-05 01:39:32 -04:00
Donald Cheng Hong Zou
5bf7754d6c Update README.md 2021-08-05 01:21:44 -04:00
Donald Cheng Hong Zou
7a0a3055c4 Update README.md 2021-08-05 01:16:22 -04:00
Donald Cheng Hong Zou
bf73efb3b4 Update README.md 2021-08-05 01:15:17 -04:00
Donald Cheng Hong Zou
b4da212158 Update README.md 2021-08-05 01:07:10 -04:00
Donald Cheng Hong Zou
4a04d42600 v2.2-beta3
Finished QR code function and starting to test.
2021-08-05 00:45:15 -04:00
Donald Cheng Hong Zou
42bbfbe538 v2.2-beta update 2021-08-03 18:45:40 -04:00
Donald Cheng Hong Zou
31ec2430c4 Update README.md 2021-08-03 18:06:43 -04:00
Donald Cheng Hong Zou
f0b56c9f1d Update README.md 2021-08-03 18:06:22 -04:00
Donald Zou
6f9f01c991 Update README.md 2021-07-12 16:35:07 -04:00
Donald Cheng Hong Zou
b822ccb19d Update README.md 2021-07-11 13:07:27 -04:00
Donald Zou
dcb11671f3 Update README.md 2021-07-11 12:49:58 -04:00
Donald Zou
282599d36f Update README.md 2021-07-05 16:09:02 -04:00
Donald Zou
f11acdc68f Update README.md 2021-07-05 15:35:23 -04:00
Donald Zou
971e127cfd Update README.md 2021-07-03 10:30:54 -04:00
Donald Zou
9b2c59d529 Delete wg0.conf 2021-07-02 14:35:34 -04:00
Donald Zou
14dc049a66 Update README.md 2021-07-02 14:31:33 -04:00
Donald Zou
d3d0e7c615 Merge pull request #24 from donaldzou/v2.1-beta
v2.1 Merge
2021-07-02 13:28:36 -04:00
Donald Zou
89423ccb8b Merge branch 'main' into v2.1-beta 2021-07-02 13:28:29 -04:00
Donald Cheng Hong Zou
746f31f70c Update README.md 2021-07-02 13:25:35 -04:00
Donald Cheng Hong Zou
3aa9eab2bd Commit June 2 2021-07-02 13:23:04 -04:00
Donald Zou
1bcc8fed5e Update README.md 2021-07-02 12:00:12 -04:00
Donald Zou
1213a030e3 Update README.md 2021-05-13 19:54:37 -04:00
Donald Zou
3b2344b477 Delete .github/workflows directory 2021-05-13 19:51:54 -04:00
Donald Zou
c53b0fe2d4 Create main.yml 2021-05-13 19:38:22 -04:00
Donald Cheng Hong Zou
0a92269456 v2.0-beta-6 Commit 2021-05-13 18:47:59 -04:00
Donald Cheng Hong Zou
1cb8906893 v2.0-beta-6 Commit 2021-05-13 18:21:10 -04:00
Donald Cheng Hong Zou
f76579fe5e v2.0-beta-6 Commit 2021-05-13 18:00:40 -04:00
Donald Zou
db05ff5e98 Update README.md 2021-05-08 12:35:22 -04:00
Donald Zou
a05d6979ec Update README.md 2021-05-07 15:27:37 -04:00
Donald Zou
be40d17172 Update README.md 2021-05-07 15:25:23 -04:00
Donald Zou
9e11339979 Update README.md 2021-05-06 11:20:28 -04:00
Donald Zou
a56129afe0 Update README.md 2021-05-05 17:25:23 -04:00
Donald Zou
8933ea999c Update README.md 2021-05-05 15:52:13 -04:00
151 changed files with 17895 additions and 1468 deletions

View File

@@ -23,10 +23,37 @@
"contributions": [
"code"
]
},
{
"login": "reafian",
"name": "Richard Newton",
"avatar_url": "https://avatars.githubusercontent.com/u/11992416?v=4",
"profile": "https://github.com/reafian",
"contributions": [
"code"
]
},
{
"login": "davejlong",
"name": "David Long",
"avatar_url": "https://avatars.githubusercontent.com/u/175317?v=4",
"profile": "http://www.davejlong.com",
"contributions": [
"code"
]
},
{
"login": "marneu",
"name": "Markus Neubauer",
"avatar_url": "https://avatars.githubusercontent.com/u/5978293?v=4",
"profile": "http://www.std-soft.com",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"projectName": "wireguard-dashboard",
"projectName": "WGDashboard",
"projectOwner": "donaldzou",
"repoType": "github",
"repoHost": "https://github.com",

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [donaldzou]

54
.gitignore vendored
View File

@@ -1,12 +1,52 @@
.vscode/sftp.json
src/.vscode/sftp.json
.vscode
.DS_Store
wg.db
*.json
.idea
src/test.py
tmp
src/db
__pycache__
src/wg-dashboard.ini
src/test.py
*.db
src/wg-dashboard.ini
src/static/pic.xd
*.conf
private_key.txt
public_key.txt
venv/**
log/**
release/*
src/db/wgdashboard.db
.jshintrc
node_modules/**
*/proxy.js
src/static/app/proxy.js
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
proxy.js
.vite/*

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
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

568
README.md
View File

@@ -1,7 +1,7 @@
<p align="center">
<img src="https://raw.githubusercontent.com/donaldzou/wireguard-dashboard/main/img/Group%202.png" width="128">
<img alt="WGDashboard" src="img/logo.png" width="128">
</p>
<h1 align="center"> Wireguard Dashboard</h1>
<h1 align="center">WGDashboard</h1>
<p align="center">
@@ -9,166 +9,534 @@
</p>
<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://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://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 align="center">Monitoring Wireguard is not convinient, need to login into server and type <code>wg show</code>. That's why this platform is being created, to view all configurations and manage them in a easier way.</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"><small><i>This project is not affiliate to the official WireGuard Project</i></small></p>
## 📣 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**
- **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.
- **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.
- **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.
- **Designs**
- **Real-time Graphs**: You can view real-time data changes with graphs in each configuration.
- **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.
- **🧐 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.**
- Rewrote the backend into a REST API structure
- Improved SQL query efficient
- Removed all templates, except for `index.html` where it will load the Vue.js app.
## 📣 What's New: Version 2.0
### ⚠️ **Update from v1.x.x**
1. Stop the dashboard if it is running.
2. You can use `git pull https://github.com/donaldzou/Wireguard-Dashboard.git v2.0` to get the new update inside `Wireguard-Dashboard` directory.
3. Proceed **Step 2 & 3** in the Install step down below.
**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**
<hr>
- Added login function to dashboard
## Table of Content
- ***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!
- [💡 Features](#-features)
- [📝 Requirement](#-requirement)
- [🛠 Install](#-install)
- [🪜 Usage](#-usage)
- [Start/Stop/Restart WGDashboard](#startstoprestart-wgdashboard)
- [Autostart WGDashboard on boot](#autostart-wgdashboard-on-boot--v22)
- [✂️ Dashboard Configuration](#%EF%B8%8F-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)
- [🥘 Experimental Functions](#-experimental-functions)
- [🔍 Screenshot](#-screenshot)
- [⏰ Changelog](#--changelog)
- [🛒 Dependencies](#-dependencies)
- [✨ Contributors](#-contributors)
## 💡 Features
- Add peers for each WireGuard configuration
- Automatically look for existing WireGuard configuration under `/etc/wireguard`
- Easy to use interface, provided credential and TOTP protection to the dashboard
- Manage peers and configuration
- Add Peers or by bulk with auto-generated information
- Edit peer information
- Delete peers with ease
- Restrict peers
- Generate QR Code and `.conf` file for peers
- Schedule jobs to delete / restrict peer when conditions are met
- View real time peer status
- Testing tool: Ping and Traceroute to your peer
- Manage peer
- Delete peers
- And many more coming up! Welcome to contribute to this project!
## 📝 Requirement
- Ubuntu or Debian based OS, other might work, but haven't test yet. Tested on the following OS:
- [x] Ubuntu 18.04.1 LTS
- [ ] If you have tested on other OS and it works perfectly please provide it to me!
- Recommend the following OS, tested by our beloved users:
- [x] Ubuntu 18.04.1 LTS, 20.04.1 LTS, 22.04.4 LTS [@Me]
- [x] Debian GNU/Linux 10 (buster) [❤️ @[robchez](https://github.com/robchez)]
- [x] AlmaLinux 8.4 (Electric Cheetah) [❤️ @[barry-smithjr](https://github.com/)]
- [x] CentOS 7 [❤️ @[PrzemekSkw](https://github.com/PrzemekSkw)]
- ‼️ Make sure you have **Wireguard** and **Wireguard-Tools (`wg-quick`)** installed.‼️ <a href="https://www.wireguard.com/install/">How to install?</a>
- Configuration files under **/etc/wireguard**
> **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!**
- **Note: For peers, `PublicKey` & `AllowedIPs` is required.**
- Python 3.7+ & Pip3
```
$ sudo apt-get install python3 python3-pip
- **WireGuard** and **WireGuard-Tools (`wg-quick`)** are installed.
> Don't know how? Check this <a href="https://www.wireguard.com/install/">official documentation</a>
- Configuration files under **`/etc/wireguard`**, but please note the following sample
```ini
[Interface]
...
SaveConfig = true
# Need to include this line to allow WireGuard Tool to save your configuration,
# or if you just want it to monitor your WireGuard Interface and don't need to
# make any changes with the dashboard, you can set it to false.
[Peer]
PublicKey = abcd1234
AllowedIPs = 1.2.3.4/32
# Must have for each peer
```
- **Python 3.10** for v4.0+, **Python 3.7 - 3.9** for v2.0 - v3.0.6.2
## 🛠 Install
1. Download WGDashboard
1. Download Wireguard Dashboard
```shell
git clone -b v4.0 https://github.com/donaldzou/WGDashboard.git wgdashboard
2. Open the WGDashboard folder
```
$ git clone -b v2.0 https://github.com/donaldzou/Wireguard-Dashboard.git
```
```shell
cd wgdashboard/src
```
3. Install WGDashboard
**2. Install Python Dependencies**
```shell
sudo chmod u+x wgd.sh
sudo ./wgd.sh install
```
```
$ cd Wireguard-Dashboard/src
$ python3 -m pip install -r requirements.txt
```
4. Give read and execute permission to root of the WireGuard configuration folder, you can change the path if your configuration files are not stored in `/etc/wireguard`
**3. Install & run Wireguard Dashboard**
```shell
sudo chmod -R 755 /etc/wireguard
```
```
$ sudo sh wgd.sh start
```
5. Run WGDashboard
Access your server with port `10086` ! e.g (http://your_server_ip:10086), continue to read to on how to change port and ip that dashboard is running with.
```shell
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
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
**1. Start/Stop/Restart Wireguard Dashboard**
#### Start/Stop/Restart WGDashboard
```
$ cd Wireguard-Dashboard/src
$ sudo sh wgd.sh start # Start the dashboard in background
$ sudo sh wgd.sh debug # Start the dashboard in foreground (debug mode)
$ sudo sh wgd.sh stop # Stop the dashboard
$ sudo sh wgd.sh restart # Restart the dasboard
$ sudo sh wgd.sh update # Update the dashboard
```shell
cd wgdashboard/src
-----------------------------
./wgd.sh start # Start the dashboard in background
-----------------------------
./wgd.sh debug # Start the dashboard in foreground (debug mode)
-----------------------------
./wgd.sh stop # Stop the dashboard
-----------------------------
./wgd.sh restart # Restart the dasboard
```
⚠️ **For first time user please also read the next section.**
#### Autostart WGDashboard on boot (>= v2.2)
In the `src` folder, it contained a file called `wg-dashboard.service`, we can use this file to let our system to autostart the dashboard after reboot. The following guide has tested on **Ubuntu**, most **Debian** based OS might be the same, but some might not. Please don't hesitate to provide your system if you have tested the autostart on another system.
1. Changing the directory to the dashboard's directory
```shell
cd wgdashboard/src
```
2. Get the full path of the dashboard's directory
```shell
pwd
#Output: /root/wgdashboard/src
```
For this example, the output is `/root/wireguard-dashboard/src`, your path might be different since it depends on where you downloaded the dashboard in the first place. **Copy the the output to somewhere, we will need this in the next step.**
3. Edit the service file, the service file is located in `wireguard-dashboard/src`, you can use other editor you like, here will be using `nano`
```shell
nano wg-dashboard.service
```
You will see something like this:
```ini
[Unit]
After=network.service
[Service]
WorkingDirectory=<your dashboard directory full path here>
ExecStart=/usr/bin/python3 <your dashboard directory full path here>/dashboard.py
Restart=always
[Install]
WantedBy=default.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:
```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
4. Copy the service file to systemd folder
```bash
$ 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.
5. Enable the service
```bash
$ sudo chmod 664 /etc/systemd/system/wg-dashboard.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable wg-dashboard.service
$ sudo systemctl start wg-dashboard.service # <-- To start the service
```
6. Check if the service run correctly
```bash
$ sudo systemctl status wg-dashboard.service
```
And you should see something like this
```shell
● wg-dashboard.service
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
Main PID: 6602 (python3)
Tasks: 1 (limit: 453)
Memory: 26.1M
CGroup: /system.slice/wg-dashboard.service
└─6602 /usr/bin/python3 /root/wgdashboard/src/dashboard.py
Aug 03 22:31:26 ubuntu-wg systemd[1]: Started wg-dashboard.service.
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Serving Flask app1 "WGDashboard" (lazy loading)
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Environment: production
Aug 03 22:31:27 ubuntu-wg python3[6602]: WARNING: This is a development server. Do not use it in a production deployment.
Aug 03 22:31:27 ubuntu-wg python3[6602]: Use a production WSGI server instead.
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Debug mode: off
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Running on all addresses.
Aug 03 22:31:27 ubuntu-wg python3[6602]: WARNING: This is a development server. Do not use it in a production deployment.
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Running on http://0.0.0.0:10086/ (Press CTRL+C to quit)
```
If you see `Active:` followed by `active (running) since...` then it means it run correctly.
7. Stop/Start/Restart the service
```bash
sudo systemctl stop wg-dashboard.service # <-- To stop the service
sudo systemctl start wg-dashboard.service # <-- To start the service
sudo systemctl restart wg-dashboard.service # <-- To restart the service
```
8. **And now you can reboot your system, and use the command at step 6 to see if it will auto start after the reboot, or just simply access the dashboard through your browser. If you have any questions or problem, please report it in the issue page.**
## ✂️ Dashboard Configuration
Since version 2.0, Wireguard Dashboard 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 config:
#### Dashboard Configuration file
### `[Account]`
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:
`username` - Username (Default: `admin`)
| | Description | Default | Edit Available |
| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | -------------- |
| **`[Account]`** | *Configuration on account* | | |
| `username` | Dashboard login username | `admin` | Yes |
| `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes |
| | | | |
| **`[Server]`** | *Configuration on dashboard* | | |
| `wg_conf_path` | The path of all the Wireguard configurations | `/etc/wireguard` | 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 |
| `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** |
| `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes |
| `dashboard_sort` | How configuration is sorting | `status` | Yes |
| | | | |
| **`[Peers]`** | *Default Settings on a new peer* | | |
| `peer_global_dns` | DNS Server | `1.1.1.1` | Yes |
| `peer_endpoint_allowed_ip` | Endpoint Allowed IP | `0.0.0.0/0` | Yes |
| `peer_display_mode` | How peer will display | `grid` | Yes |
| `remote_endpoint` | Remote Endpoint (i.e where your peers will connect to) | *depends on your server's default network interface* | Yes |
| `peer_mtu` | Maximum Transmit Unit | `1420` | |
| `peer_keep_alive` | Keep Alive | `21` | Yes |
`password` - Password, will be hash with SHA256 (Default: `admin`).
#### Generating QR code and peer configuration file (.conf)
### `[Server]`
Starting version 2.2, dashboard can now generate QR code and configuration file for each peer. Here is a template of what each QR code encoded with and the same content will be inside the file:
`wg_conf_path` - The path of all the Wireguard configurations (Default: `/etc/wireguard`)
`app_ip` - IP address the flask will run with (Default: `0.0.0.0`)
`app_port` - Port the flask will run with (Default: `10086`)
`auth_req` - Does the dashboard need authentication (Default: `true`)
- If `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only change the file directly in system**.
`version` - Dashboard Version
All these settings will be able to configure within the dashboard in **Settings** on the sidebar, without changing the actual file. **Except `version` and `auth_req` due to security consideration.**
```ini
[Interface]
PrivateKey = QWERTYUIOPO234567890YUSDAKFH10E1B12JE129U21=
Address = 0.0.0.0/32
DNS = 1.1.1.1
[Peer]
PublicKey = QWERTYUIOPO234567890YUSDAKFH10E1B12JE129U21=
AllowedIPs = 0.0.0.0/0
Endpoint = 0.0.0.0:51820
```
| | Description | Default Value | Available in Peer setting |
| ----------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------- |
| **`[Interface]`** | | | |
| `PrivateKey` | The private key of this peer | Private key generated by WireGuard (`wg genkey`) or provided by user | Yes |
| `Address` | The `allowed_ips` of your peer | N/A | Yes |
| `DNS` | The DNS server your peer will use | `1.1.1.1` - Cloud flare DNS, you can change it when you adding the peer or in the peer setting. | Yes |
| **`[Peer]`** | | | |
| `PublicKey` | The public key of your server | N/A | No |
| `AllowedIPs` | IP ranges for which a peer will route traffic | `0.0.0.0/0` - Indicated a default route to send all internet and VPN traffic through that peer. | Yes |
| `Endpoint` | Your wireguard server ip and port, the dashboard will search for your server's default interface's ip. | `<your server default interface ip>:<listen port>` | Yes |
## ❓ How to update the dashboard?
```
$ cd wireguard-dashboard
$ sudo sh wgd.sh update # Perform update
$ sudo sh wgd.sh start # Start dashboard
```
#### **Please note for user who is using `v2.3.1` or below**
- 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:.
<hr>
#### 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`
```shell
cd wgdashboard/src
```
2. Update the dashboard
```shell
git pull https://github.com/donaldzou/WGDashboard.git v3.0.5 --force
```
3. Install
```shell
./wgd.sh install
```
Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol)
## 🥘 Experimental Functions
#### Progressive Web App (PWA) for WGDashboard
- 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.
<img src="img/PWA.gif"/>
## 🔍 Screenshot
![Index Image](https://github.com/donaldzou/Wireguard-Dashboard/raw/main/src/static/index.png)
![Sign In Page](img/SignIn.png)
<p align=center>Index Page</p>
![Index Image](img/HomePage.png)
![Signin Image](https://github.com/donaldzou/Wireguard-Dashboard/raw/main/src/static/signin.png)
![Configuration](img/Configuration.png)
<p align=center>Signin Page</p>
![Add Peer](img/AddPeer.png)
![Configuration Image](https://github.com/donaldzou/Wireguard-Dashboard/raw/main/src/static/configuration.png)
![Edit Peer](img/EditPeer.png)
<p align=center>Configuration Page</p>
![Delete Peer](img/DeleteBulk.png)
![Settings Image](https://github.com/donaldzou/Wireguard-Dashboard/raw/main/src/static/settings.png)
![Dashboard Setting](img/DashboardSetting.png)
<p align=center>Settings Page</p>
![Ping](img/Ping.png)
## Contributors ✨
![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-2-orange.svg?style=flat-square)](#contributors-)
[![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)):
@@ -178,8 +546,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- 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/wireguard-dashboard/commits?author=antonioag95" title="Tests">⚠️</a> <a href="https://github.com/donaldzou/wireguard-dashboard/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/wireguard-dashboard/commits?author=tonjo" title="Code">💻</a></td>
<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>
@@ -188,5 +559,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

19
docker-compose.yml Normal file
View File

@@ -0,0 +1,19 @@
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
docker/Docker-explain.md Normal file
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
docker/Dockerfile Normal file
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
docker/compose.yaml Normal file
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
docker/entrypoint.sh Normal file
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

8
docs/api-documents.md Normal file
View File

@@ -0,0 +1,8 @@
# API Documents for WGDashboard
**Version: v4.0**
**Created by: Donald Zou**
<hr>

0
docs/changelogs.md Normal file
View File

16
entrypoint.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/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
img/AddPeer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
img/Configuration.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
img/DashboardSetting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
img/DeleteBulk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
img/DeletePeer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

BIN
img/EditPeer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

BIN
img/HomePage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
img/PWA.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

BIN
img/Ping.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

BIN
img/QRCode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

BIN
img/SearchIP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
img/SignIn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
img/Traceroute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

View File

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

17
package-lock.json generated Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "Wireguard-Dashboard",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dayjs": "^1.11.12"
}
},
"node_modules/dayjs": {
"version": "1.11.12",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz",
"integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg=="
}
}
}

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"dayjs": "^1.11.12"
}
}

244
src/api.py Normal file
View File

@@ -0,0 +1,244 @@
import ipaddress, subprocess, datetime, os, util
from datetime import datetime, timedelta
from flask import jsonify
from util import *
import configparser
notEnoughParameter = {"status": False, "reason": "Please provide all required parameters."}
good = {"status": True, "reason": ""}
def ret(status=True, reason="", data=""):
return {"status": status, "reason": reason, "data": data}
def togglePeerAccess(data, g):
checkUnlock = g.cur.execute(f"SELECT * FROM {data['config']} WHERE id='{data['peerID']}'").fetchone()
if checkUnlock:
moveUnlockToLock = g.cur.execute(
f"INSERT INTO {data['config']}_restrict_access SELECT * FROM {data['config']} WHERE id = '{data['peerID']}'")
if g.cur.rowcount == 1:
print(g.cur.rowcount)
print(util.deletePeers(data['config'], [data['peerID']], g.cur, g.db))
else:
moveLockToUnlock = g.cur.execute(
f"SELECT * FROM {data['config']}_restrict_access WHERE id = '{data['peerID']}'").fetchone()
try:
if len(moveLockToUnlock[-1]) == 0:
status = subprocess.check_output(
f"wg set {data['config']} peer {moveLockToUnlock[0]} allowed-ips {moveLockToUnlock[11]}",
shell=True, stderr=subprocess.STDOUT)
else:
now = str(datetime.datetime.now().strftime("%m%d%Y%H%M%S"))
f_name = now + "_tmp_psk.txt"
f = open(f_name, "w+")
f.write(moveLockToUnlock[-1])
f.close()
subprocess.check_output(
f"wg set {data['config']} peer {moveLockToUnlock[0]} allowed-ips {moveLockToUnlock[11]} preshared-key {f_name}",
shell=True, stderr=subprocess.STDOUT)
os.remove(f_name)
status = subprocess.check_output(f"wg-quick save {data['config']}", shell=True, stderr=subprocess.STDOUT)
g.cur.execute(
f"INSERT INTO {data['config']} SELECT * FROM {data['config']}_restrict_access WHERE id = '{data['peerID']}'")
if g.cur.rowcount == 1:
g.cur.execute(f"DELETE FROM {data['config']}_restrict_access WHERE id = '{data['peerID']}'")
except subprocess.CalledProcessError as exc:
return {"status": False, "reason": str(exc.output.strip())}
return good
class managePeer:
def getPeerDataUsage(self, data, cur):
now = datetime.now()
now_string = now.strftime("%d/%m/%Y %H:%M:%S")
interval = {
"30min": now - timedelta(hours=0, minutes=30),
"1h": now - timedelta(hours=1, minutes=0),
"6h": now - timedelta(hours=6, minutes=0),
"24h": now - timedelta(hours=24, minutes=0),
"all": ""
}
if data['interval'] not in interval.keys():
return {"status": False, "reason": "Invalid interval."}
intv = ""
if data['interval'] != "all":
t = interval[data['interval']].strftime("%d/%m/%Y %H:%M:%S")
intv = f" AND time >= '{t}'"
timeData = cur.execute(f"SELECT total_receive, total_sent, time FROM wg0_transfer WHERE id='{data['peerID']}' {intv} ORDER BY time DESC;")
chartData = []
for i in timeData:
chartData.append({
"total_receive": i[0],
"total_sent": i[1],
"time": i[2]
})
return {"status": True, "reason": "", "data": chartData}
class manageConfiguration:
def AddressCheck(self, data):
address = data['address']
address = address.replace(" ", "")
address = address.split(',')
amount = 0
for i in address:
try:
ips = ipaddress.ip_network(i, False)
amount += ips.num_addresses
except ValueError as e:
return {"status": False, "reason": str(e)}
if amount >= 1:
return {"status": True, "reason": "", "data": f"Total of {amount} IPs"}
else:
return {"status": True, "reason": "", "data": f"0 available IPs"}
def PortCheck(self, data, configs):
port = data['port']
if (not port.isdigit()) or int(port) < 1 or int(port) > 65535:
return {"status": False, "reason": f"Invalid port."}
for i in configs:
if i['port'] == port:
return {"status": False, "reason": f"{port} used by {i['conf']}."}
checkSystem = subprocess.run(f'ss -tulpn | grep :{port} > /dev/null', shell=True)
if checkSystem.returncode != 1:
return {"status": False, "reason": f"Port {port} used by other process in your system."}
return good
def NameCheck(self, data, configs):
name = data['name']
name = name.replace(" ", "")
for i in configs:
if name == i['conf']:
return {"status": False, "reason": f"{name} already existed."}
illegal_filename = ["(Space)", " ", ".", ",", "/", "?", "<", ">", "\\", ":", "*", '|' '\"', "com1", "com2",
"com3",
"com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4",
"lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "con", "nul", "prn"]
for i in illegal_filename:
name = name.replace(i, "")
if len(name) == 0:
return {"status": False, "reason": "Invalid name."}
return good
def addConfiguration(self, data, configs, WG_CONF_PATH):
output = ["[Interface]", "SaveConfig = true"]
required = ['addConfigurationPrivateKey', 'addConfigurationListenPort',
'addConfigurationAddress', 'addConfigurationPreUp', 'addConfigurationPreDown',
'addConfigurationPostUp', 'addConfigurationPostDown']
for i in required:
e = data[i]
if len(e) != 0:
key = i.replace("addConfiguration", "")
o = f"{key} = {e}"
output.append(o)
name = data['addConfigurationName']
illegal_filename = ["(Space)", " ", ".", ",", "/", "?", "<", ">", "\\", ":", "*", '|' '\"', "com1", "com2",
"com3",
"com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4",
"lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "con", "nul", "prn"]
for i in illegal_filename:
name = name.replace(i, "")
try:
newFile = open(f"{WG_CONF_PATH}/{name}.conf", "w+")
newFile.write("\n".join(output))
except Exception as e:
return {"status": False, "reason": str(e)}
return {"status": True, "reason": "", "data": name}
def deleteConfiguration(self, data, config, g, WG_CONF_PATH):
confs = []
for i in config:
confs.append(i['conf'])
print(confs)
if data['name'] not in confs:
return {"status": False, "reason": "Configuration does not exist", "data": ""}
for i in config:
if i['conf'] == data['name']:
if i['status'] == "running":
try:
subprocess.check_output("wg-quick down " + data['name'], shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
return {"status": False, "reason": "Can't stop peer", "data": str(exc.output.strip().decode("utf-8"))}
g.cur.execute(f'DROP TABLE {data["name"]}')
g.cur.execute(f'DROP TABLE {data["name"]}_restrict_access')
g.db.commit()
try:
os.remove(f'{WG_CONF_PATH}/{data["name"]}.conf')
except Exception as e:
return {"status": False, "reason": "Can't delete peer", "data": str(e)}
return good
def getConfigurationInfo(self, configName, WG_CONF_PATH):
conf = configparser.RawConfigParser(strict=False)
conf.optionxform = str
try:
with open(f'{WG_CONF_PATH}/{configName}.conf', 'r'):
conf.read(f'{WG_CONF_PATH}/{configName}.conf')
if not conf.has_section("Interface"):
return ret(status=False, reason="No [Interface] in configuration file")
return ret(data=dict(conf['Interface']))
except FileNotFoundError as err:
return ret(status=False, reason=str(err))
def saveConfiguration(self, data, WG_CONF_PATH, configs):
conf = configparser.RawConfigParser(strict=False)
conf.optionxform = str
configName = data['configurationName']
pc = manageConfiguration.PortCheck(self, {'port': data['ListenPort']}, configs)
if pc['status']:
try:
newData = []
with open(f'{WG_CONF_PATH}/{configName}.conf', 'r') as f:
conf.read(f'{WG_CONF_PATH}/{configName}.conf')
if not conf.has_section("Interface"):
return ret(status=False, reason="No [Interface] in configuration file")
l = ['ListenPort', 'PostUp', 'PostDown', 'PreUp', 'PreDown']
for i in l:
conf.set("Interface", i, data[i])
conf.remove_section("Peer")
newData = list(map(lambda x : f"{x[0]} = {x[1]}\n", list(conf.items("Interface"))))
originalData = f.readlines()
for i in range(len(originalData)):
if originalData[i] == "[Peer]\n":
originalData = originalData[i:]
break
newData.insert(0, "[Interface]\n")
newData.append("\n")
newData = newData + originalData
conf.clear()
try:
check = subprocess.check_output("wg-quick down " + configName,
shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
pass
with open(f'{WG_CONF_PATH}/{configName}.conf', 'w') as f:
for i in newData:
f.write(i)
try:
check = subprocess.check_output("wg-quick up " + configName,
shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
pass
return ret()
except FileNotFoundError as err:
return ret(status=False, reason=str(err))
else:
return pc
class settings:
def setTheme(self, theme, config, setConfig):
themes = ['light', 'dark']
if theme not in themes:
return ret(status=False, reason="Theme does not exist")
config['Server']['dashboard_theme'] = theme
setConfig(config)
return ret()

7
src/bulkAddBash.sh Normal file
View File

@@ -0,0 +1,7 @@
for ((i = 0 ; i<$1 ; i++ ))
do
privateKey=$(wg genkey)
presharedKey=$(wg genkey)
publicKey=$(wg pubkey <<< "$privateKey")
echo "$privateKey,$publicKey,$presharedKey"
done

4
src/certbot.ini Normal file
View File

@@ -0,0 +1,4 @@
authenticator = standalone
noninteractive = true
agree-tos = true
rsa-key-size = 2048

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
You can delete this later ;)

26
src/gunicorn.conf.py Normal file
View File

@@ -0,0 +1,26 @@
import dashboard
from datetime import datetime
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger
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'
workers = 1
threads = 1
bind = f"{app_host}:{app_port}"
daemon = True
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)

View File

@@ -1,3 +1,8 @@
Flask==1.1.2
tinydb==4.3.0
ifcfg==0.21
bcrypt
ifcfg
psutil
pyotp
Flask
flask-cors
icmplib
gunicorn

BIN
src/static/.DS_Store vendored

Binary file not shown.

30
src/static/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.vite/*

Binary file not shown.

Binary file not shown.

15
src/static/app/dist/assets/index.css vendored Normal file

File diff suppressed because one or more lines are too long

68
src/static/app/dist/assets/index.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
src/static/app/dist/favicon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

14
src/static/app/dist/index.html vendored Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/static/app/dist/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WGDashboard</title>
<script type="module" crossorigin src="/static/app/dist/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/static/app/dist/assets/index.css">
</head>
<body>
<div id="app" class="w-100 vh-100"></div>
</body>
</html>

13
src/static/app/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WGDashboard</title>
</head>
<body>
<div id="app" class="w-100 vh-100"></div>
<script type="module" src="./src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

4124
src/static/app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"name": "app",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vuepic/vue-datepicker": "^9.0.1",
"@vueuse/core": "^10.9.0",
"@vueuse/shared": "^10.9.0",
"animate.css": "^4.1.1",
"bootstrap": "^5.3.2",
"bootstrap-icons": "^1.11.2",
"cidr-tools": "^7.0.4",
"dayjs": "^1.11.12",
"fuse.js": "^7.0.0",
"i": "^0.3.7",
"is-cidr": "^5.0.3",
"npm": "^10.5.0",
"pinia": "^2.1.7",
"qrcode": "^1.5.3",
"qrcodejs": "^1.0.0",
"uuid": "^9.0.1",
"vue": "^3.4.29",
"vue-chartjs": "^5.3.0",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.10"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@@ -0,0 +1,37 @@
<script setup >
import { RouterView } from 'vue-router'
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
const store = DashboardConfigurationStore();
</script>
<template>
<nav class="navbar bg-dark sticky-top" data-bs-theme="dark">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">WGDashboard</span>
</div>
</nav>
<Suspense>
<RouterView v-slot="{ Component }">
<Transition name="app" mode="out-in">
<Component :is="Component"></Component>
</Transition>
</RouterView>
</Suspense>
</template>
<style scoped>
.app-enter-active,
.app-leave-active {
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.app-enter-from{
transform: translateY(20px);
opacity: 0;
}
.app-leave-to {
transform: translateY(-20px);
opacity: 0;
}
</style>

View File

@@ -0,0 +1,142 @@
<script>
import {fetchGet} from "@/utilities/fetch.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "allowedIPsInput",
props: {
data: Object,
saving: Boolean,
bulk: Boolean,
availableIp: undefined,
},
data(){
return {
allowedIp: [],
availableIpSearchString: "",
customAvailableIp: "",
allowedIpFormatError: false
}
},
setup(){
const store = WireguardConfigurationsStore();
const dashboardStore = DashboardConfigurationStore();
return {store, dashboardStore}
},
computed: {
searchAvailableIps(){
return this.availableIpSearchString ?
this.availableIp.filter(x =>
x.includes(this.availableIpSearchString) && !this.data.allowed_ips.includes(x)) :
this.availableIp.filter(x => !this.data.allowed_ips.includes(x))
}
},
methods: {
addAllowedIp(ip){
if(this.store.checkCIDR(ip)){
this.data.allowed_ips.push(ip);
return true;
}
return false;
}
},
watch: {
customAvailableIp(){
this.allowedIpFormatError = false;
},
availableIp(){
if (this.availableIp !== undefined && this.availableIp.length > 0){
this.addAllowedIp(this.availableIp[0])
}
}
},
mounted() {
}
}
</script>
<template>
<div :class="{inactiveField: this.bulk}">
<label for="peer_allowed_ip_textbox" class="form-label">
<small class="text-muted">Allowed IPs <code>(Required)</code></small>
</label>
<div class="d-flex gap-2 flex-wrap" :class="{'mb-2': this.data.allowed_ips.length > 0}">
<TransitionGroup name="list">
<span class="badge rounded-pill text-bg-success" v-for="(ip, index) in this.data.allowed_ips" :key="ip">
{{ip}}
<a role="button" @click="this.data.allowed_ips.splice(index, 1)">
<i class="bi bi-x-circle-fill ms-1"></i></a>
</span>
</TransitionGroup>
</div>
<div class="d-flex gap-2 align-items-center">
<div class="input-group">
<input type="text" class="form-control form-control-sm rounded-start-3"
placeholder="Enter IP Address/CIDR"
:class="{'is-invalid': this.allowedIpFormatError}"
v-model="customAvailableIp"
:disabled="bulk">
<button class="btn btn-outline-success btn-sm rounded-end-3"
:disabled="bulk || !this.customAvailableIp"
@click="this.addAllowedIp(this.customAvailableIp)
? this.customAvailableIp = '' :
this.allowedIpFormatError = true;
this.dashboardStore.newMessage('WGDashboard', 'Allowed IP is invalid', 'danger')"
type="button" id="button-addon2">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<small class="text-muted">or</small>
<div class="dropdown flex-grow-1">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle rounded-3 w-100"
:disabled="!availableIp || bulk"
data-bs-auto-close="outside"
type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-filter-circle me-2"></i>
Pick Available IP
</button>
<ul class="dropdown-menu mt-2 shadow w-100 dropdown-menu-end rounded-3"
v-if="this.availableIp"
style="overflow-y: scroll; max-height: 270px; width: 300px !important;">
<li>
<div class="px-3 pb-2 pt-1">
<input class="form-control form-control-sm rounded-3"
v-model="this.availableIpSearchString"
placeholder="Search...">
</div>
</li>
<li v-for="ip in this.searchAvailableIps" >
<a class="dropdown-item d-flex" role="button" @click="this.addAllowedIp(ip)">
<span class="me-auto"><small>{{ip}}</small></span>
</a>
</li>
<li v-if="this.searchAvailableIps.length === 0">
<small class="px-3 text-muted">No available IP containing "{{this.availableIpSearchString}}"</small>
</li>
</ul>
</div>
</div>
</div>
</template>
<style scoped>
.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(10px);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.list-leave-active {
position: absolute;
}
</style>

View File

@@ -0,0 +1,41 @@
<script>
export default {
name: "bulkAdd",
props: {
saving: Boolean,
data: Object,
availableIp: undefined
}
}
</script>
<template>
<div>
<div class="form-check form-switch ">
<input class="form-check-input"
type="checkbox" role="switch"
:disabled="!this.availableIp"
id="bulk_add" v-model="this.data.bulkAdd">
<label class="form-check-label me-2" for="bulk_add">
<small><strong>Bulk Add</strong></small>
</label>
</div>
<p :class="{'mb-0': !this.data.bulkAdd}"><small class="text-muted d-block">
By adding peers by bulk, each peer's name will be auto generated, and Allowed IP will be assign to the next available IP.
</small></p>
<div class="form-group" v-if="this.data.bulkAdd">
<input class="form-control form-control-sm rounded-3 mb-1" type="number" min="1"
:max="this.availableIp.length"
v-model="this.data.bulkAddAmount"
placeholder="How many peers you want to add?">
<small class="text-muted">
You can add up to <strong>{{this.availableIp.length}}</strong> peers
</small>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,66 @@
<script>
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "dnsInput",
props: {
data: Object,
saving: Boolean,
},
data(){
return {
error: false,
dns: JSON.parse(JSON.stringify(this.data.DNS)),
}
},
setup(){
const store = WireguardConfigurationsStore();
const dashboardStore = DashboardConfigurationStore();
return {store, dashboardStore}
},
methods:{
checkDNS(){
if(this.dns){
let i = this.dns.split(',').map(x => x.replaceAll(' ', ''));
for(let ip in i){
if (!this.store.regexCheckIP(i[ip])){
if (!this.error){
this.dashboardStore.newMessage("WGDashboard", "DNS is invalid", "danger");
}
this.error = true;
this.data.DNS = "";
return;
}
}
this.error = false;
this.data.DNS = this.dns;
}
}
},
watch: {
'dns'(){
this.checkDNS();
}
}
}
</script>
<template>
<div>
<label for="peer_DNS_textbox" class="form-label">
<small class="text-muted">DNS</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:class="{'is-invalid': this.error}"
:disabled="this.saving"
v-model="this.dns"
id="peer_DNS_textbox">
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,63 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
export default {
name: "endpointAllowedIps",
props: {
data: Object,
saving: Boolean
},
setup(){
const store = WireguardConfigurationsStore();
const dashboardStore = DashboardConfigurationStore()
return {store, dashboardStore}
},
data(){
return {
endpointAllowedIps: JSON.parse(JSON.stringify(this.data.endpoint_allowed_ip)),
error: false
}
},
methods: {
checkAllowedIP(){
let i = this.endpointAllowedIps.split(",").map(x => x.replaceAll(' ', ''));
for (let ip in i){
if (!this.store.checkCIDR(i[ip])){
if (!this.error){
this.dashboardStore.newMessage("WGDashboard", "Endpoint Allowed IP is invalid.", "danger")
}
this.data.endpoint_allowed_ip = "";
this.error = true;
return;
}
}
this.error = false;
this.data.endpoint_allowed_ip = this.endpointAllowedIps;
}
},
watch: {
'endpointAllowedIps'(){
this.checkAllowedIP();
}
}
}
</script>
<template>
<div>
<label for="peer_endpoint_allowed_ips" class="form-label">
<small class="text-muted">Endpoint Allowed IPs <code>(Required)</code></small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:class="{'is-invalid': error}"
:disabled="this.saving"
v-model="this.endpointAllowedIps"
@blur="this.checkAllowedIP()"
id="peer_endpoint_allowed_ips">
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,23 @@
<script>
export default {
name: "mtuInput",
props: {
data: Object,
saving: Boolean
}
}
</script>
<template>
<div>
<label for="peer_mtu" class="form-label"><small class="text-muted">MTU</small></label>
<input type="number" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.mtu"
id="peer_mtu">
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,26 @@
<script>
export default {
name: "nameInput",
props: {
bulk: Boolean,
data: Object,
saving: Boolean
}
}
</script>
<template>
<div :class="{inactiveField: this.bulk}">
<label for="peer_name_textbox" class="form-label">
<small class="text-muted">Name</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving || this.bulk"
v-model="this.data.name"
id="peer_name_textbox" placeholder="">
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,25 @@
<script>
export default {
name: "persistentKeepAliveInput",
props: {
data: Object,
saving: Boolean
}
}
</script>
<template>
<div>
<label for="peer_keep_alive" class="form-label">
<small class="text-muted">Persistent Keepalive</small>
</label>
<input type="number" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.keepalive"
id="peer_keep_alive">
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,25 @@
<script>
export default {
name: "presharedKeyInput",
props: {
data: Object,
saving: Boolean
}
}
</script>
<template>
<div>
<label for="peer_preshared_key_textbox" class="form-label">
<small class="text-muted">Pre-Shared Key</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.preshared_key"
id="peer_preshared_key_textbox">
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,108 @@
<script>
import "@/utilities/wireguard.js"
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "privatePublicKeyInput",
props: {
data: Object,
saving: Boolean,
bulk: Boolean
},
setup(){
const dashboardStore = DashboardConfigurationStore();
return {dashboardStore}
},
data(){
return {
keypair: {
publicKey: "",
privateKey: "",
presharedKey: ""
},
editKey: false,
error: false
}
},
methods: {
genKeyPair(){
this.editKey = false;
this.keypair = window.wireguard.generateKeypair();
this.data.private_key = this.keypair.privateKey;
this.data.public_key = this.keypair.publicKey;
},
checkMatching(){
try{
if (window.wireguard.generatePublicKey(this.keypair.privateKey)
!== this.keypair.publicKey){
this.error = true;
this.dashboardStore.newMessage("WGDashboard", "Private Key and Public Key does not match.", "danger");
}
}catch (e){
this.error = true;
this.data.private_key = "";
this.data.public_key = "";
}
}
},
mounted() {
this.genKeyPair();
},
watch: {
keypair: {
deep: true,
handler(){
this.error = false;
this.checkMatching();
}
}
}
}
</script>
<template>
<div class="d-flex gap-2 flex-column" :class="{inactiveField: this.bulk}">
<div>
<label for="peer_private_key_textbox" class="form-label">
<small class="text-muted">Private Key <code>(Required for QR Code and Download)</code></small>
</label>
<div class="input-group">
<input type="text" class="form-control form-control-sm rounded-start-3"
v-model="this.keypair.privateKey"
:disabled="!this.editKey || this.bulk"
:class="{'is-invalid': this.error}"
@blur="this.checkMatching()"
id="peer_private_key_textbox">
<button class="btn btn-outline-info btn-sm rounded-end-3"
@click="this.genKeyPair()"
:disabled="this.bulk"
type="button" id="button-addon2">
<i class="bi bi-arrow-repeat"></i> </button>
</div>
</div>
<div>
<div class="d-flex">
<label for="public_key" class="form-label">
<small class="text-muted">Public Key <code>(Required)</code></small>
</label>
<div class="form-check form-switch ms-auto">
<input class="form-check-input" type="checkbox" role="switch"
:disabled="this.bulk"
id="enablePublicKeyEdit" v-model="this.editKey">
<label class="form-check-label" for="enablePublicKeyEdit">
<small>Edit</small>
</label>
</div>
</div>
<input class="form-control-sm form-control rounded-3"
:class="{'is-invalid': this.error}"
v-model="this.keypair.publicKey"
@blur="this.checkMatching()"
:disabled="!this.editKey || this.bulk"
type="text" id="public_key">
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,125 @@
<script>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'
import "animate.css"
import PeerSettingsDropdown from "@/components/configurationComponents/peerSettingsDropdown.vue";
export default {
name: "peer",
components: {PeerSettingsDropdown},
props: {
Peer: Object
},
data(){
return {
}
},
setup(){
const target = ref(null);
const subMenuOpened = ref(false)
onClickOutside(target, event => {
subMenuOpened.value = false;
});
return {target, subMenuOpened}
},
computed: {
getLatestHandshake(){
if (this.Peer.latest_handshake.includes(",")){
return this.Peer.latest_handshake.split(",")[0]
}
return this.Peer.latest_handshake;
}
}
}
</script>
<template>
<div class="card shadow-sm rounded-3 peerCard bg-transparent"
:class="{'border-warning': Peer.restricted}">
<div>
<div v-if="!Peer.restricted" class="card-header bg-transparent d-flex align-items-center gap-2 border-0">
<div class="dot ms-0" :class="{active: Peer.status === 'running'}"></div>
<div style="font-size: 0.8rem" class="ms-auto d-flex gap-2">
<span class="text-primary">
<i class="bi bi-arrow-down"></i><strong>
{{(Peer.cumu_receive + Peer.total_receive).toFixed(4)}}</strong> GB
</span>
<span class="text-success">
<i class="bi bi-arrow-up"></i><strong>
{{(Peer.cumu_sent + Peer.total_sent).toFixed(4)}}</strong> GB
</span>
<span class="text-secondary" v-if="Peer.latest_handshake !== 'No Handshake'">
<i class="bi bi-arrows-angle-contract"></i>
{{getLatestHandshake}} ago
</span>
</div>
</div>
<div v-else class="border-0 card-header bg-transparent text-warning fw-bold"
style="font-size: 0.8rem">
<i class="bi-lock-fill me-2"></i>
Access Restricted
</div>
</div>
<div class="card-body pt-1" style="font-size: 0.9rem">
<h6>
{{Peer.name ? Peer.name : 'Untitled Peer'}}
</h6>
<div class="mb-2">
<small class="text-muted">Public Key</small>
<p class="mb-0"><samp>{{Peer.id}}</samp></p>
</div>
<div class="d-flex align-items-end">
<div>
<small class="text-muted">Allowed IP</small>
<p class="mb-0"><samp>{{Peer.allowed_ip}}</samp></p>
</div>
<div class="ms-auto px-2 rounded-3 subMenuBtn"
:class="{active: this.subMenuOpened}"
>
<a role="button" class="text-body"
@click="this.subMenuOpened = true">
<h5 class="mb-0"><i class="bi bi-three-dots"></i></h5>
</a>
<Transition name="slide-fade">
<PeerSettingsDropdown
@qrcode="(file) => this.$emit('qrcode', file)"
@setting="this.$emit('setting')"
@jobs="this.$emit('jobs')"
@refresh="this.$emit('refresh')"
@share="this.$emit('share')"
:Peer="Peer"
v-if="this.subMenuOpened"
ref="target"
></PeerSettingsDropdown>
</Transition>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.slide-fade-leave-active, .slide-fade-enter-active{
transition: all 0.2s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(20px);
opacity: 0;
filter: blur(3px);
}
.subMenuBtn.active{
background-color: #ffffff20;
}
.peerCard{
transition: box-shadow 0.1s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.peerCard:hover{
box-shadow: var(--bs-box-shadow) !important;
}
</style>

View File

@@ -0,0 +1,164 @@
<script>
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import NameInput from "@/components/configurationComponents/newPeersComponents/nameInput.vue";
import PrivatePublicKeyInput from "@/components/configurationComponents/newPeersComponents/privatePublicKeyInput.vue";
import AllowedIPsInput from "@/components/configurationComponents/newPeersComponents/allowedIPsInput.vue";
import DnsInput from "@/components/configurationComponents/newPeersComponents/dnsInput.vue";
import EndpointAllowedIps from "@/components/configurationComponents/newPeersComponents/endpointAllowedIps.vue";
import PresharedKeyInput from "@/components/configurationComponents/newPeersComponents/presharedKeyInput.vue";
import MtuInput from "@/components/configurationComponents/newPeersComponents/mtuInput.vue";
import PersistentKeepAliveInput
from "@/components/configurationComponents/newPeersComponents/persistentKeepAliveInput.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import BulkAdd from "@/components/configurationComponents/newPeersComponents/bulkAdd.vue";
export default {
name: "peerCreate",
components: {
BulkAdd,
PersistentKeepAliveInput,
MtuInput,
PresharedKeyInput, EndpointAllowedIps, DnsInput, AllowedIPsInput, PrivatePublicKeyInput, NameInput},
data(){
return{
data: {
bulkAdd: false,
bulkAddAmount: "",
name: "",
allowed_ips: [],
private_key: "",
public_key: "",
DNS: this.dashboardStore.Configuration.Peers.peer_global_dns,
endpoint_allowed_ip: this.dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip,
keepalive: parseInt(this.dashboardStore.Configuration.Peers.peer_keep_alive),
mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu),
preshared_key: ""
},
availableIp: undefined,
availableIpSearchString: "",
saving: false,
allowedIpDropdown: undefined
}
},
mounted() {
fetchGet("/api/getAvailableIPs/" + this.$route.params.id, {}, (res) => {
if (res.status){
this.availableIp = res.data;
}
})
},
setup(){
const store = WireguardConfigurationsStore();
const dashboardStore = DashboardConfigurationStore();
return {store, dashboardStore}
},
methods: {
peerCreate(){
this.saving = true
fetchPost("/api/addPeers/" + this.$route.params.id, this.data, (res) => {
if (res.status){
this.$router.push(`/configuration/${this.$route.params.id}/peers`)
this.dashboardStore.newMessage("Server", "Peer create successfully", "success")
}else{
this.dashboardStore.newMessage("Server", res.message, "danger")
}
this.saving = false;
})
}
},
computed:{
allRequireFieldsFilled(){
let status = true;
if (this.data.bulkAdd){
if(this.data.bulkAddAmount.length === 0 || this.data.bulkAddAmount > this.availableIp.length){
status = false;
}
}else{
let requireFields =
["allowed_ips", "private_key", "public_key", "endpoint_allowed_ip", "keepalive", "mtu"]
requireFields.forEach(x => {
if (this.data[x].length === 0) status = false;
});
}
return status;
}
},
watch: {
bulkAdd(newVal){
if(!newVal){
this.data.bulkAddAmount = "";
}
},
'data.bulkAddAmount'(){
if (this.data.bulkAddAmount > this.availableIp.length){
this.data.bulkAddAmount = this.availableIp.length;
}
}
}
}
</script>
<template>
<div class="container">
<div class="mb-4">
<RouterLink to="peers" is="div" class="d-flex align-items-center gap-4 text-decoration-none">
<h3 class="mb-0 text-body">
<i class="bi bi-chevron-left"></i>
</h3>
<h3 class="text-body mb-0">Add Peers</h3>
</RouterLink>
</div>
<div class="d-flex flex-column gap-2">
<BulkAdd :saving="saving" :data="this.data" :availableIp="this.availableIp"></BulkAdd>
<hr class="mb-0 mt-2">
<NameInput :saving="saving" :data="this.data" v-if="!this.data.bulkAdd"></NameInput>
<PrivatePublicKeyInput :saving="saving" :data="data" v-if="!this.data.bulkAdd"></PrivatePublicKeyInput>
<AllowedIPsInput :availableIp="this.availableIp" :saving="saving" :data="data" v-if="!this.data.bulkAdd"></AllowedIPsInput>
<EndpointAllowedIps :saving="saving" :data="data"></EndpointAllowedIps>
<DnsInput :saving="saving" :data="data"></DnsInput>
<hr class="mb-0 mt-2">
<div class="row">
<div class="col-sm" v-if="!this.data.bulkAdd">
<PresharedKeyInput :saving="saving" :data="data" :bulk="this.data.bulkAdd"></PresharedKeyInput>
</div>
<div class="col-sm">
<MtuInput :saving="saving" :data="data"></MtuInput>
</div>
<div class="col-sm">
<PersistentKeepAliveInput :saving="saving" :data="data"></PersistentKeepAliveInput>
</div>
</div>
<div class="d-flex mt-2">
<button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow"
:disabled="!this.allRequireFieldsFilled || this.saving"
@click="this.peerCreate()"
>
<i class="bi bi-plus-circle-fill me-2" v-if="!this.saving"></i>
{{this.saving ? 'Saving...': 'Add'}}
</button>
</div>
</div>
</div>
</template>
<style scoped>
.peerSettingContainer {
background-color: #00000060;
z-index: 9998;
}
div{
transition: 0.2s ease-in-out;
}
.inactiveField{
opacity: 0.4;
}
.card{
max-height: 100%;
}
</style>

View File

@@ -0,0 +1,109 @@
<script>
import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue";
import SchedulePeerJob from "@/components/configurationComponents/peerScheduleJobsComponents/schedulePeerJob.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {v4} from "uuid";
export default {
name: "peerJobs",
setup(){
const store = WireguardConfigurationsStore();
return {store}
},
props:{
selectedPeer: Object
},
components:{
SchedulePeerJob,
ScheduleDropdown,
},
data(){
return {
}
},
methods:{
deleteJob(j){
this.selectedPeer.jobs = this.selectedPeer.jobs.filter(x => x.JobID !== j.JobID);
},
addJob(){
this.selectedPeer.jobs.unshift(JSON.parse(JSON.stringify({
JobID: v4().toString(),
Configuration: this.selectedPeer.configuration.Name,
Peer: this.selectedPeer.id,
Field: this.store.PeerScheduleJobs.dropdowns.Field[0].value,
Operator: this.store.PeerScheduleJobs.dropdowns.Operator[0].value,
Value: "",
CreationDate: "",
ExpireDate: "",
Action: this.store.PeerScheduleJobs.dropdowns.Action[0].value
}))
)
}
},
}
</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">
<div class="card rounded-3 shadow" style="width: 700px">
<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">Schedule Jobs
<strong></strong>
</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div>
<div class="card-body px-4 pb-4 pt-2 position-relative">
<div class="d-flex align-items-center mb-3">
<button class="btn bg-primary-subtle border-1 border-primary-subtle text-primary-emphasis rounded-3 shadow"
@click="this.addJob()">
<i class="bi bi-plus-lg me-2"></i> Job
</button>
</div>
<TransitionGroup name="schedulePeerJobTransition" tag="div" class="position-relative">
<SchedulePeerJob
@refresh="this.$emit('refresh')"
@delete="this.deleteJob(job)"
:dropdowns="this.store.PeerScheduleJobs.dropdowns"
:key="job.JobID"
:pjob="job" v-for="(job, index) in this.selectedPeer.jobs">
</SchedulePeerJob>
<div class="card shadow-sm" key="none"
style="height: 153px"
v-if="this.selectedPeer.jobs.length === 0">
<div class="card-body text-muted text-center d-flex">
<h6 class="m-auto">This peer does not have any job yet.</h6>
</div>
</div>
</TransitionGroup>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.schedulePeerJobTransition-move, /* apply transition to moving elements */
.schedulePeerJobTransition-enter-active,
.schedulePeerJobTransition-leave-active {
transition: all 0.4s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.schedulePeerJobTransition-enter-from,
.schedulePeerJobTransition-leave-to {
opacity: 0;
transform: scale(0.9);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.schedulePeerJobTransition-leave-active {
position: absolute;
width: 100%;
}
</style>

View File

@@ -0,0 +1,86 @@
<script>
import SchedulePeerJob from "@/components/configurationComponents/peerScheduleJobsComponents/schedulePeerJob.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {v4} from "uuid";
export default {
name: "peerJobsAllModal",
setup(){
const store = WireguardConfigurationsStore();
return {store}
},
components: {SchedulePeerJob},
props: {
configurationPeers: Array[Object]
},
methods:{
getuuid(){
return v4();
}
},
computed:{
getAllJobs(){
return this.configurationPeers.filter(x => x.jobs.length > 0)
}
}
}
</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">
<div class="card rounded-3 shadow" style="width: 700px">
<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">All Active Jobs
</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div>
<div class="card-body px-4 pb-4 pt-2 ">
<div class="accordion" id="peerJobsLogsModalAccordion" v-if="this.getAllJobs.length > 0">
<div class="accordion-item" v-for="(p, index) in this.getAllJobs" :key="p.id">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
:data-bs-target="'#collapse_' + index">
<small>
<strong>
<span v-if="p.name">
{{p.name}} &#x2022;
</span>
<samp class="text-muted">{{p.id}}</samp>
</strong>
</small>
</button>
</h2>
<div :id="'collapse_' + index" class="accordion-collapse collapse"
data-bs-parent="#peerJobsLogsModalAccordion">
<div class="accordion-body">
<SchedulePeerJob
@delete="this.$emit('refresh')"
@refresh="this.$emit('refresh')"
:dropdowns="this.store.PeerScheduleJobs.dropdowns"
:viewOnly="true"
:key="job.JobID"
:pjob="job" v-for="job in p.jobs">
</SchedulePeerJob>
</div>
</div>
</div>
</div>
<div class="card shadow-sm"
style="height: 153px"
v-else>
<div class="card-body text-muted text-center d-flex">
<h6 class="m-auto">No active job at the moment.</h6>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,162 @@
<script>
import dayjs from "dayjs";
import {fetchGet} from "@/utilities/fetch.js";
export default {
name: "peerJobsLogsModal",
props: {
configurationInfo: Object
},
data(){
return {
dataLoading: true,
data: [],
logFetchTime: undefined,
showLogID: false,
showJobID: true,
showSuccessJob: true,
showFailedJob: true,
showLogAmount: 10
}
},
async mounted(){
await this.fetchLog();
},
methods: {
async fetchLog(){
this.dataLoading = true;
await fetchGet(`/api/getPeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => {
this.data = res.data;
this.logFetchTime = dayjs().format("YYYY-MM-DD HH:mm:ss")
this.dataLoading = false;
});
}
},
computed: {
getLogs(){
return this.data
.filter(x => {
return (this.showSuccessJob && x.Status === "1") || (this.showFailedJob && x.Status === "0")
})
},
showLogs(){
return this.getLogs.slice(0, this.showLogAmount);
}
}
}
</script>
<template>
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
<div class="container-fluid d-flex h-100 w-100">
<div class="m-auto mt-0 modal-dialog-centered dashboardModal" style="width: 100%">
<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-0">
<h4 class="mb-0">Jobs Logs</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div>
<div class="card-body px-4 pb-4 pt-2">
<div v-if="!this.dataLoading">
<p>Updated at: {{this.logFetchTime}}</p>
<div class="mb-2 d-flex gap-3">
<button @click="this.fetchLog()"
class="btn btn-sm rounded-3 shadow-sm
text-info-emphasis bg-info-subtle border-1 border-info-subtle me-1">
<i class="bi bi-arrow-clockwise me-2"></i>
Refresh
</button>
<div class="d-flex gap-3 align-items-center">
<span class="text-muted">Filter</span>
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="this.showSuccessJob"
id="jobLogsShowSuccessCheck">
<label class="form-check-label" for="jobLogsShowSuccessCheck">
<span class="badge text-success-emphasis bg-success-subtle">Success</span>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="this.showFailedJob"
id="jobLogsShowFailedCheck">
<label class="form-check-label" for="jobLogsShowFailedCheck">
<span class="badge text-danger-emphasis bg-danger-subtle">Failed</span>
</label>
</div>
</div>
<div class="d-flex gap-3 align-items-center ms-auto">
<span class="text-muted">Display</span>
<div class="form-check">
<input class="form-check-input" type="checkbox"
v-model="showJobID"
id="jobLogsShowJobIDCheck">
<label class="form-check-label" for="jobLogsShowJobIDCheck">
Job ID
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox"
v-model="showLogID"
id="jobLogsShowLogIDCheck">
<label class="form-check-label" for="jobLogsShowLogIDCheck">
Log ID
</label>
</div>
</div>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col" v-if="showLogID">Log ID</th>
<th scope="col" v-if="showJobID">Job ID</th>
<th scope="col">Status</th>
<th scope="col">Message</th>
</tr>
</thead>
<tbody>
<tr v-for="log in this.showLogs" style="font-size: 0.875rem">
<th scope="row">{{log.LogDate}}</th>
<td v-if="showLogID"><samp class="text-muted">{{log.LogID}}</samp></td>
<td v-if="showJobID"><samp class="text-muted">{{log.JobID}}</samp></td>
<td>
<span class="badge" :class="[log.Status === '1' ? 'text-success-emphasis bg-success-subtle':'text-danger-emphasis bg-danger-subtle']">
{{log.Status === "1" ? 'Success': 'Failed'}}
</span>
</td>
<td>{{log.Message}}</td>
</tr>
</tbody>
</table>
<div class="d-flex gap-2">
<button v-if="this.getLogs.length > this.showLogAmount"
@click="this.showLogAmount += 20"
class="btn btn-sm rounded-3 shadow-sm
text-primary-emphasis bg-primary-subtle border-1 border-primary-subtle">
<i class="bi bi-chevron-down me-2"></i>
Show More
</button>
<button v-if="this.showLogAmount > 20"
@click="this.showLogAmount = 20"
class="btn btn-sm rounded-3 shadow-sm
text-primary-emphasis bg-primary-subtle border-1 border-primary-subtle">
<i class="bi bi-chevron-up me-2"></i>
Collapse
</button>
</div>
</div>
<div class="d-flex align-items-center flex-column" v-else>
<div class="spinner-border text-body" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,627 @@
<script>
import PeerSearch from "@/components/configurationComponents/peerSearch.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchGet} from "@/utilities/fetch.js";
import Peer from "@/components/configurationComponents/peer.vue";
import { Line, Bar } from 'vue-chartjs'
import Fuse from "fuse.js";
import {
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
BubbleController,
DoughnutController,
LineController,
PieController,
PolarAreaController,
RadarController,
ScatterController,
CategoryScale,
LinearScale,
LogarithmicScale,
RadialLinearScale,
TimeScale,
TimeSeriesScale,
Decimation,
Filler,
Legend,
Title,
Tooltip
} from 'chart.js';
import dayjs from "dayjs";
import PeerSettings from "@/components/configurationComponents/peerSettings.vue";
import PeerQRCode from "@/components/configurationComponents/peerQRCode.vue";
import PeerCreate from "@/components/configurationComponents/peerCreate.vue";
import PeerJobs from "@/components/configurationComponents/peerJobs.vue";
import PeerJobsAllModal from "@/components/configurationComponents/peerJobsAllModal.vue";
import PeerJobsLogsModal from "@/components/configurationComponents/peerJobsLogsModal.vue";
import {ref} from "vue";
import PeerShareLinkModal from "@/components/configurationComponents/peerShareLinkModal.vue";
Chart.register(
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
BubbleController,
DoughnutController,
LineController,
PieController,
PolarAreaController,
RadarController,
ScatterController,
CategoryScale,
LinearScale,
LogarithmicScale,
RadialLinearScale,
TimeScale,
TimeSeriesScale,
Decimation,
Filler,
Legend,
Title,
Tooltip
);
export default {
name: "peerList",
components: {
PeerShareLinkModal,
PeerJobsLogsModal,
PeerJobsAllModal, PeerJobs, PeerCreate, PeerQRCode, PeerSettings, PeerSearch, Peer, Line, Bar},
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
const wireguardConfigurationStore = WireguardConfigurationsStore();
const interval = ref(undefined)
return {dashboardConfigurationStore, wireguardConfigurationStore, interval}
},
data(){
return {
configurationToggling: false,
loading: false,
error: null,
configurationInfo: [],
configurationPeers: [],
historyDataSentDifference: [],
historyDataReceivedDifference: [],
historySentData: {
labels: [],
datasets: [
{
label: 'Data Sent',
data: [],
fill: false,
borderColor: '#198754',
tension: 0
},
],
},
historyReceiveData: {
labels: [],
datasets: [
{
label: 'Data Received',
data: [],
fill: false,
borderColor: '#0d6efd',
tension: 0
},
],
},
peerSetting: {
modalOpen: false,
selectedPeer: undefined
},
peerScheduleJobs:{
modalOpen: false,
selectedPeer: undefined
},
peerQRCode: {
modalOpen: false,
peerConfigData: undefined
},
peerCreate: {
modalOpen: false
},
peerScheduleJobsAll: {
modalOpen: false
},
peerScheduleJobsLogs: {
modalOpen: false
},
peerShare:{
modalOpen: false,
selectedPeer: undefined
}
}
},
mounted() {
},
watch: {
'$route': {
immediate: true,
handler(){
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
this.loading = true;
let id = this.$route.params.id;
this.configurationInfo = [];
this.configurationPeers = [];
if (id){
this.getPeers(id)
this.setPeerInterval();
}
}
},
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
this.setPeerInterval();
}
},
beforeRouteLeave(){
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
},
methods:{
toggle(){
this.configurationToggling = true;
fetchGet("/api/toggleWireguardConfiguration/", {
configurationName: this.configurationInfo.Name
}, (res) => {
if (res.status){
this.dashboardConfigurationStore.newMessage("Server",
`${this.configurationInfo.Name} is
${res.data ? 'is on':'is off'}`, "Success")
}else{
this.dashboardConfigurationStore.newMessage("Server",
res.message, 'danger')
}
this.configurationInfo.Status = res.data
this.configurationToggling = false;
})
},
getPeers(id = this.$route.params.id){
fetchGet("/api/getWireguardConfigurationInfo",
{
configurationName: id
}, (res) => {
this.configurationInfo = res.data.configurationInfo;
this.configurationPeers = res.data.configurationPeers;
this.configurationPeers.forEach(x => {
x.restricted = false;
})
res.data.configurationRestrictedPeers.forEach(x => {
x.restricted = true;
this.configurationPeers.push(x)
})
this.loading = false;
if (this.configurationPeers.length > 0){
const sent = this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((x,y) => x + y).toFixed(4);
const receive = this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((x,y) => x + y).toFixed(4);
if (
this.historyDataSentDifference[this.historyDataSentDifference.length - 1] !== sent
){
if (this.historyDataSentDifference.length > 0){
this.historySentData = {
labels: [...this.historySentData.labels, dayjs().format("HH:mm:ss A")],
datasets: [
{
label: 'Data Sent',
data: [...this.historySentData.datasets[0].data,
((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000)
.toFixed(4)],
fill: false,
borderColor: '#198754',
tension: 0
}
],
}
}
this.historyDataSentDifference.push(sent)
}
if (
this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1] !== receive
){
if (this.historyDataReceivedDifference.length > 0){
this.historyReceiveData = {
labels: [...this.historyReceiveData.labels, dayjs().format("HH:mm:ss A")],
datasets: [
{
label: 'Data Received',
data: [...this.historyReceiveData.datasets[0].data,
((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000)
.toFixed(4)],
fill: false,
borderColor: '#0d6efd',
tension: 0
}
],
}
}
this.historyDataReceivedDifference.push(receive)
}
}
});
},
setPeerInterval(){
this.dashboardConfigurationStore.Peers.RefreshInterval = setInterval(() => {
this.getPeers()
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
},
},
computed: {
configurationSummary(){
const k = {
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length,
totalUsage: this.configurationPeers.length > 0 ?
this.configurationPeers.filter(x => !x.restricted)
.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(){
return this.historyReceiveData
},
sentData(){
return this.historySentData
},
individualDataUsage(){
return {
labels: this.configurationPeers.map(x => {
if (x.name) return x.name
return `Untitled Peer - ${x.id}`
}),
datasets: [{
label: 'Total Data Usage',
data: this.configurationPeers.map(x => x.cumu_data + x.total_data),
backgroundColor: this.configurationPeers.map(x => `#0dcaf0`),
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.formattedValue} GB`
}
}
}
}]
}
},
individualDataUsageChartOption(){
return {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val, index) => {
return `${val} GB`
}
},
grid: {
display: false
},
}
}
}
},
chartOptions() {
return {
responsive: true,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.formattedValue} MB/s`
}
}
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val, index) => {
return `${val} MB/s`
}
},
grid: {
display: false
},
}
}
}
},
searchPeers(){
const fuse = new Fuse(this.configurationPeers, {
keys: ["name", "id", "allowed_ip"]
});
const result = this.wireguardConfigurationStore.searchString ?
fuse.search(this.wireguardConfigurationStore.searchString).map(x => x.item) : this.configurationPeers;
if (this.dashboardConfigurationStore.Configuration.Server.dashboard_sort === "restricted"){
return result.slice().sort((a, b) => {
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
< b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort] ){
return 1;
}
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
> b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]){
return -1;
}
return 0;
});
}
return result.slice().sort((a, b) => {
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
< b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort] ){
return -1;
}
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
> b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]){
return 1;
}
return 0;
});
}
}
}
</script>
<template>
<div v-if="!this.loading">
<div class="d-flex align-items-center">
<div>
<small CLASS="text-muted">CONFIGURATION</small>
<div class="d-flex align-items-center gap-3">
<h1 class="mb-0"><samp>{{this.configurationInfo.Name}}</samp></h1>
</div>
</div>
<div class="card rounded-3 bg-transparent shadow-sm ms-auto">
<div class="card-body py-2 d-flex align-items-center">
<div>
<p class="mb-0 text-muted"><small>Status</small></p>
<div class="form-check form-switch ms-auto">
<label class="form-check-label" style="cursor: pointer" :for="'switch' + this.configurationInfo.id">
{{this.configurationToggling ? 'Turning ':''}}
{{this.configurationInfo.Status ? "On":"Off"}}
<span v-if="this.configurationToggling"
class="spinner-border spinner-border-sm" aria-hidden="true"></span>
</label>
<input class="form-check-input"
style="cursor: pointer"
:disabled="this.configurationToggling"
type="checkbox" role="switch" :id="'switch' + this.configurationInfo.id"
@change="this.toggle()"
v-model="this.configurationInfo.Status"
>
</div>
</div>
<div class="dot ms-5" :class="{active: this.configurationInfo.Status}"></div>
</div>
</div>
</div>
<div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Address</small></p>
{{this.configurationInfo.Address}}
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Listen Port</small></p>
{{this.configurationInfo.ListenPort}}
</div>
</div>
</div>
<div style="word-break: break-all" class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Public Key</small></p>
<samp>{{this.configurationInfo.PublicKey}}</samp>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-2">
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Connected Peers</small></p>
<strong class="h4">{{configurationSummary.connectedPeers}}</strong>
</div>
<i class="bi bi-ethernet ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Usage</small></p>
<strong class="h4">{{configurationSummary.totalUsage}} GB</strong>
</div>
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Received</small></p>
<strong class="h4 text-primary">{{configurationSummary.totalReceive}} GB</strong>
</div>
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Sent</small></p>
<strong class="h4 text-success">{{configurationSummary.totalSent}} GB</strong>
</div>
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-3">
<div class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent shadow-sm" style="height: 270px">
<div class="card-header bg-transparent border-0">
<small class="text-muted">Peers Total Data Usage</small></div>
<div class="card-body pt-1">
<Bar
:data="individualDataUsage"
:options="individualDataUsageChartOption"
style="width: 100%; height: 200px; max-height: 200px"></Bar>
</div>
</div>
</div>
<div class="col-sm col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm" style="height: 270px">
<div class="card-header bg-transparent border-0"><small class="text-muted">Real Time Received Data Usage</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="receiveData"
style="width: 100%; height: 200px; max-height: 200px"
></Line>
</div>
</div>
</div>
<div class="col-sm col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm" style="height: 270px">
<div class="card-header bg-transparent border-0"><small class="text-muted">Real Time Sent Data Usage</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="sentData"
style="width: 100%; height: 200px; max-height: 200px"
></Line>
</div>
</div>
</div>
</div>
<div class="mb-4">
<!-- <div class="d-flex align-items-center gap-3 mb-2">-->
<!-- <h3>Peers</h3>-->
<!-- </div>-->
<PeerSearch
@jobsAll="this.peerScheduleJobsAll.modalOpen = true"
@jobLogs="this.peerScheduleJobsLogs.modalOpen = true"
:configuration="this.configurationInfo"></PeerSearch>
<TransitionGroup name="list" tag="div" class="row gx-2 gy-2 z-0">
<div class="col-12 col-lg-6 col-xl-4"
:key="peer.id"
v-for="peer in this.searchPeers">
<Peer :Peer="peer"
@share="this.peerShare.selectedPeer = peer.id; this.peerShare.modalOpen = true;"
@refresh="this.getPeers()"
@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)"
@qrcode="(file) => {this.peerQRCode.peerConfigData = file; this.peerQRCode.modalOpen = true;}"
></Peer>
</div>
</TransitionGroup>
</div>
<Transition name="zoom">
<PeerSettings v-if="this.peerSetting.modalOpen"
key="settings"
:selectedPeer="this.peerSetting.selectedPeer"
@refresh="this.getPeers()"
@close="this.peerSetting.modalOpen = false">
</PeerSettings>
</Transition>
<Transition name="zoom">
<PeerQRCode :peerConfigData="this.peerQRCode.peerConfigData"
key="qrcode"
@close="this.peerQRCode.modalOpen = false"
v-if="peerQRCode.modalOpen"></PeerQRCode>
</Transition>
<Transition name="zoom">
<PeerJobs
@refresh="this.getPeers()"
v-if="this.peerScheduleJobs.modalOpen"
:selectedPeer="this.peerScheduleJobs.selectedPeer"
@close="this.peerScheduleJobs.modalOpen = false">
</PeerJobs>
</Transition>
<Transition name="zoom">
<PeerJobsAllModal
v-if="this.peerScheduleJobsAll.modalOpen"
@refresh="this.getPeers()"
@close="this.peerScheduleJobsAll.modalOpen = false"
:configurationPeers="this.configurationPeers"
>
</PeerJobsAllModal>
</Transition>
<Transition name="zoom">
<PeerJobsLogsModal v-if="this.peerScheduleJobsLogs.modalOpen"
@close="this.peerScheduleJobsLogs.modalOpen = false"
:configurationInfo="this.configurationInfo"
>
</PeerJobsLogsModal>
</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>
</template>
<style scoped>
.peerNav .nav-link{
&.active{
//background: linear-gradient(var(--degree), var(--brandColor1) var(--distance2), var(--brandColor2) 100%);
//color: white;
background-color: #efefef;
}
}
</style>

View File

@@ -0,0 +1,35 @@
<script>
import QRCode from "qrcode";
export default {
name: "peerQRCode",
props: {
peerConfigData: String
},
mounted() {
QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfigData , (error) => {
if (error) console.error(error)
})
}
}
</script>
<template>
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0">
<div class="container d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal justify-content-center">
<div class="card rounded-3 shadow">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
<h4 class="mb-0">QR Code</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div>
<div class="card-body">
<canvas id="qrcode" class="rounded-3 shadow" ref="qrcode"></canvas>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,48 @@
<script>
export default {
name: "scheduleDropdown",
props: {
options: Array,
data: String,
edit: false
},
setup(props) {
if (props.data === undefined){
this.$emit('update', this.options[0].value)
}
},
computed:{
currentSelection(){
return this.options.find(x => x.value === this.data)
}
}
}
</script>
<template>
<div class="dropdown scheduleDropdown">
<button class="btn btn-sm btn-outline-primary rounded-3"
:class="{'disabled border-transparent': !edit}" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<samp>{{this.currentSelection.display}}</samp>
</button>
<ul class="dropdown-menu rounded-3 shadow" style="font-size: 0.875rem; width: 200px">
<li v-for="x in this.options" v-if="edit">
<a class="dropdown-item d-flex align-items-center" role="button" @click="$emit('update', x.value)">
<samp>{{x.display}}</samp>
<i class="bi bi-check ms-auto" v-if="x.value === this.currentSelection.value"></i>
</a>
</li>
</ul>
</div>
</template>
<style scoped>
.btn.disabled{
opacity: 1;
background-color: rgba(13, 110, 253, 0.09);
border-color: transparent;
}
</style>

View File

@@ -0,0 +1,220 @@
<script>
import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue";
import {ref} from "vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchPost} from "@/utilities/fetch.js";
import VueDatePicker from "@vuepic/vue-datepicker";
import dayjs from "dayjs";
export default {
name: "schedulePeerJob",
components: {VueDatePicker, ScheduleDropdown},
props: {
dropdowns: Array[Object],
pjob: Object,
viewOnly: false
},
setup(props){
const job = ref({})
const edit = ref(false)
const newJob = ref(false)
job.value = JSON.parse(JSON.stringify(props.pjob))
if (!job.value.CreationDate){
edit.value = true
newJob.value = true
}
const store = DashboardConfigurationStore()
return {job, edit, newJob, store}
},
data(){
return {
inputType: undefined,
}
},
watch:{
pjob: {
deep: true,
immediate: true,
handler(newValue){
if (!this.edit){
this.job = JSON.parse(JSON.stringify(newValue))
}
}
}
},
methods: {
save(){
if (this.job.Field && this.job.Operator && this.job.Action && this.job.Value){
fetchPost(`/api/savePeerScheduleJob/`, {
Job: this.job
}, (res) => {
if (res.status){
this.edit = false;
this.store.newMessage("Server", "Job Saved!", "success")
console.log(res.data)
this.$emit("refresh", res.data[0])
this.newJob = false;
}else{
this.store.newMessage("Server", res.message, "danger")
}
})
}else{
this.alert();
}
},
alert(){
let animation = "animate__flash";
let dropdowns = this.$el.querySelectorAll(".scheduleDropdown");
let inputs = this.$el.querySelectorAll("input");
dropdowns.forEach(x => x.classList.add("animate__animated", animation))
inputs.forEach(x => x.classList.add("animate__animated", animation))
setTimeout(() => {
dropdowns.forEach(x => x.classList.remove("animate__animated", animation))
inputs.forEach(x => x.classList.remove("animate__animated", animation))
}, 2000)
},
reset(){
if(this.job.CreationDate){
this.job = JSON.parse(JSON.stringify(this.pjob));
this.edit = false;
}else{
this.$emit('delete')
}
},
delete(){
if(this.job.CreationDate){
fetchPost(`/api/deletePeerScheduleJob/`, {
Job: this.job
}, (res) => {
if (!res.status){
this.store.newMessage("Server", res.message, "danger")
this.$emit('delete')
}else{
this.store.newMessage("Server", "Job Deleted!", "success")
}
})
}
this.$emit('delete')
},
parseTime(modelData){
if(modelData){
this.job.Value = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
}
}
},
}
</script>
<template>
<div class="card shadow-sm rounded-3 mb-2" :class="{'border-warning-subtle': this.newJob}">
<div class="card-header bg-transparent text-muted border-0">
<small class="d-flex" v-if="!this.newJob">
<strong class="me-auto">Job ID</strong>
<samp>{{this.job.JobID}}</samp>
</small>
<small v-else><span class="badge text-bg-warning">Unsaved Job</span></small>
</div>
<div class="card-body pt-1" style="font-family: var(--bs-font-monospace)">
<div class="d-flex gap-2 align-items-center mb-2">
<samp>
if
</samp>
<ScheduleDropdown
:edit="edit"
:options="this.dropdowns.Field"
:data="this.job.Field"
@update="(value) => {this.job.Field = value}"
></ScheduleDropdown>
<samp>
is
</samp>
<ScheduleDropdown
:edit="edit"
:options="this.dropdowns.Operator"
:data="this.job.Operator"
@update="(value) => this.job.Operator = value"
></ScheduleDropdown>
<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"
v-if="this.job.Field === 'date'"
:dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
/>
<!-- <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"
:disabled="!edit"
v-else
v-model="this.job.Value"
style="width: auto">
<samp>
{{this.dropdowns.Field.find(x => x.value === this.job.Field)?.unit}} {
</samp>
</div>
<div class="px-5 d-flex gap-2 align-items-center">
<samp>then</samp>
<ScheduleDropdown
:edit="edit"
:options="this.dropdowns.Action"
:data="this.job.Action"
@update="(value) => this.job.Action = value"
></ScheduleDropdown>
</div>
<div class="d-flex gap-3">
<samp>}</samp>
<div class="ms-auto d-flex gap-3" v-if="!this.edit">
<a role="button"
class="ms-auto text-decoration-none"
@click="this.edit = true">[E] Edit</a>
<a role="button"
@click="this.delete()"
class=" text-danger text-decoration-none">[D] Delete</a>
</div>
<div class="ms-auto d-flex gap-3" v-else>
<a role="button"
class="text-secondary text-decoration-none"
@click="this.reset()">[C] Cancel</a>
<a role="button"
class="text-primary ms-auto text-decoration-none"
@click="this.save()">[S] Save</a>
</div>
</div>
</div>
</div>
</template>
<style scoped>
*{
font-size: 0.875rem;
}
input{
padding: 0.1rem 0.4rem;
}
input:disabled{
border-color: transparent;
background-color: rgba(13, 110, 253, 0.09);
color: #0d6efd;
}
.dp__main{
width: auto;
flex-grow: 1;
--dp-input-padding: 2.5px 30px 2.5px 12px;
--dp-border-radius: 0.5rem;
}
</style>

View File

@@ -0,0 +1,179 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
export default {
name: "peerSearch",
setup(){
const store = DashboardConfigurationStore();
const wireguardConfigurationStore = WireguardConfigurationsStore()
return {store, wireguardConfigurationStore}
},
props: {
configuration: Object
},
data(){
return {
sort: {
status: "Status",
name: "Name",
allowed_ip: "Allowed IP",
restricted: "Restricted"
},
interval: {
'5000': '5 Seconds',
'10000': '10 Seconds',
'30000': '30 Seconds',
'60000': '1 Minutes'
},
searchString: "",
searchStringTimeout: undefined
}
},
methods: {
debounce(){
if (!this.searchStringTimeout){
this.searchStringTimeout = setTimeout(() => {
this.wireguardConfigurationStore.searchString = this.searchString;
}, 300)
}else{
clearTimeout(this.searchStringTimeout)
this.searchStringTimeout = setTimeout(() => {
this.wireguardConfigurationStore.searchString = this.searchString;
}, 300)
}
},
updateSort(sort){
fetchPost("/api/updateDashboardConfigurationItem", {
section: "Server",
key: "dashboard_sort",
value: sort
}, (res) => {
if (res.status){
this.store.getConfiguration();
}
})
},
updateRefreshInterval(refreshInterval){
fetchPost("/api/updateDashboardConfigurationItem", {
section: "Server",
key: "dashboard_refresh_interval",
value: refreshInterval
}, (res) => {
if (res.status){
this.store.getConfiguration();
}
})
},
downloadAllPeer(){
fetchGet(`/api/downloadAllPeers/${this.configuration.Name}`, {}, (res) => {
console.log(res);
window.wireguard.generateZipFiles(res, this.configuration.Name)
})
}
},
mounted() {
}
}
</script>
<template>
<div class="mb-3">
<div class="d-flex gap-2 z-3">
<RouterLink
to="create"
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
</RouterLink>
<button class="btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm"
@click="this.downloadAllPeer()">
<i class="bi bi-download me-2"></i> Download All
</button>
<div class="flex-grow-1">
<input class="form-control rounded-3 bg-secondary-subtle border-1 border-secondary-subtle shadow-sm w-100"
placeholder="Search..."
id="searchPeers"
@keyup="this.debounce()"
v-model="this.searchString">
</div>
<div class="dropdown dropup">
<button class="btn 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-filter-circle me-2"></i>
Display
</button>
<ul class="dropdown-menu mt-2 shadow rounded-3 animate__animated animation__fadeInDropdown dropdown-menu-end">
<li>
<small class="dropdown-header">Sort by</small>
</li>
<li v-for="(value, key) in this.sort">
<a class="dropdown-item d-flex align-items-center" role="button" @click="this.updateSort(key)">
<small class="me-auto">{{value}}</small>
<i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_sort === key"></i>
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<small class="dropdown-header">Refresh Interval</small>
</li>
<li v-for="(value, key) in this.interval">
<a class="dropdown-item d-flex" role="button" @click="updateRefreshInterval(key)">
<small class="me-auto">{{value}}</small>
<i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i>
</a>
</li>
</ul>
</div>
<div class="dropdown dropup">
<button class="btn 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"></i>
</button>
<ul class="dropdown-menu shadow mt-2 rounded-3 animate__animated animation__fadeInDropdown">
<li>
<h6 class="dropdown-header">Peer Jobs</h6>
</li>
<li>
<a role="button" class="dropdown-item" @click="this.$emit('jobsAll')">
Active Jobs
</a>
</li>
<li>
<a role="button" class="dropdown-item" @click="this.$emit('jobLogs')">
Logs
</a>
</li>
</ul>
</div>
</div>
</div>
</template>
<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);
}
}
</style>

View File

@@ -0,0 +1,233 @@
<script>
import {fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "peerSettings",
props: {
selectedPeer: Object
},
data(){
return {
data: undefined,
dataChanged: false,
showKey: false,
saving: false
}
},
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
return {dashboardConfigurationStore}
},
methods: {
reset(){
if (this.selectedPeer){
this.data = JSON.parse(JSON.stringify(this.selectedPeer))
this.dataChanged = false;
}
},
savePeer(){
this.saving = true;
fetchPost(`/api/updatePeerSettings/${this.$route.params.id}`, this.data, (res) => {
this.saving = false;
if (res.status){
this.dashboardConfigurationStore.newMessage("Server", "Peer Updated!", "success")
}else{
this.dashboardConfigurationStore.newMessage("Server", res.message, "danger")
}
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() {
this.reset();
},
mounted() {
this.$el.querySelectorAll("input").forEach(x => {
x.addEventListener("keyup", () => {
this.dataChanged = true;
});
})
}
}
</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">
<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 pb-2">
<h4 class="mb-0">Peer Settings</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.data">
<div class="d-flex flex-column gap-2 mb-4">
<div class="d-flex align-items-center">
<small class="text-muted">Public Key</small>
<small class="ms-auto"><samp>{{this.data.id}}</samp></small>
</div>
<div>
<label for="peer_name_textbox" class="form-label">
<small class="text-muted">Name</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.name"
id="peer_name_textbox" placeholder="">
</div>
<div>
<div class="d-flex position-relative">
<label for="peer_private_key_textbox" class="form-label">
<small class="text-muted">Private Key <code>(Required for QR Code and Download)</code></small>
</label>
<a role="button" class="ms-auto text-decoration-none toggleShowKey"
@click="this.showKey = !this.showKey"
>
<i class="bi" :class="[this.showKey ? 'bi-eye-slash-fill':'bi-eye-fill']"></i>
</a>
</div>
<input :type="[this.showKey ? 'text':'password']" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.private_key"
id="peer_private_key_textbox"
style="padding-right: 40px">
</div>
<div>
<label for="peer_allowed_ip_textbox" class="form-label">
<small class="text-muted">Allowed IPs <code>(Required)</code></small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.allowed_ip"
id="peer_allowed_ip_textbox">
</div>
<div>
<label for="peer_endpoint_allowed_ips" class="form-label">
<small class="text-muted">Endpoint Allowed IPs <code>(Required)</code></small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.endpoint_allowed_ip"
id="peer_endpoint_allowed_ips">
</div>
<div>
<label for="peer_DNS_textbox" class="form-label">
<small class="text-muted">DNS</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.DNS"
id="peer_DNS_textbox">
</div>
<div class="accordion mt-3" id="peerSettingsAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button rounded-3 collapsed" type="button"
data-bs-toggle="collapse" data-bs-target="#peerSettingsAccordionOptional">
Optional Settings
</button>
</h2>
<div id="peerSettingsAccordionOptional" class="accordion-collapse collapse"
data-bs-parent="#peerSettingsAccordion">
<div class="accordion-body d-flex flex-column gap-2 mb-2">
<div>
<label for="peer_preshared_key_textbox" class="form-label">
<small class="text-muted">Pre-Shared Key</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.preshared_key"
id="peer_preshared_key_textbox">
</div>
<div>
<label for="peer_mtu" class="form-label"><small class="text-muted">MTU</small></label>
<input type="number" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.mtu"
id="peer_mtu">
</div>
<div>
<label for="peer_keep_alive" class="form-label">
<small class="text-muted">Persistent Keepalive</small>
</label>
<input type="number" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.keepalive"
id="peer_keep_alive">
</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 class="d-flex align-items-center gap-2">
<button class="btn btn-secondary rounded-3 shadow"
@click="this.reset()"
:disabled="!this.dataChanged || this.saving">
Revert <i class="bi bi-arrow-clockwise ms-2"></i>
</button>
<button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow"
:disabled="!this.dataChanged || this.saving"
@click="this.savePeer()"
>
Save Peer<i class="bi bi-save-fill ms-2"></i></button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.toggleShowKey{
position: absolute;
top: 35px;
right: 12px;
}
</style>

View File

@@ -0,0 +1,170 @@
<script>
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "peerSettingsDropdown",
setup(){
const dashboardStore = DashboardConfigurationStore()
return {dashboardStore}
},
props: {
Peer: Object
},
data(){
return{
deleteBtnDisabled: false,
restrictBtnDisabled: false,
allowAccessBtnDisabled: false,
}
},
methods: {
downloadPeer(){
fetchGet("/api/downloadPeer/"+this.$route.params.id, {
id: this.Peer.id
}, (res) => {
if (res.status){
const blob = new Blob([res.data.file], { type: "text/plain" });
const jsonObjectUrl = URL.createObjectURL(blob);
const filename = `${res.data.fileName}.conf`;
const anchorEl = document.createElement("a");
anchorEl.href = jsonObjectUrl;
anchorEl.download = filename;
anchorEl.click();
this.dashboardStore.newMessage("WGDashboard", "Peer download started", "success")
}else{
this.dashboardStore.newMessage("Server", res.message, "danger")
}
})
},
downloadQRCode(){
fetchGet("/api/downloadPeer/"+this.$route.params.id, {
id: this.Peer.id
}, (res) => {
if (res.status){
this.$emit("qrcode", res.data.file)
}else{
this.dashboardStore.newMessage("Server", res.message, "danger")
}
})
},
deletePeer(){
this.deleteBtnDisabled = true
fetchPost(`/api/deletePeers/${this.$route.params.id}`, {
peers: [this.Peer.id]
}, (res) => {
this.dashboardStore.newMessage("Server", res.message, res.status ? "success":"danger")
this.$emit("refresh")
this.deleteBtnDisabled = false
})
},
restrictPeer(){
this.restrictBtnDisabled = true
fetchPost(`/api/restrictPeers/${this.$route.params.id}`, {
peers: [this.Peer.id]
}, (res) => {
this.dashboardStore.newMessage("Server", res.message, res.status ? "success":"danger")
this.$emit("refresh")
this.restrictBtnDisabled = false
})
},
allowAccessPeer(){
this.allowAccessBtnDisabled = true
fetchPost(`/api/allowAccessPeers/${this.$route.params.id}`, {
peers: [this.Peer.id]
}, (res) => {
this.dashboardStore.newMessage("Server", res.message, res.status ? "success":"danger")
this.$emit("refresh")
this.allowAccessBtnDisabled = false
})
}
}
}
</script>
<template>
<ul class="dropdown-menu mt-2 shadow-lg d-block rounded-3" style="max-width: 200px">
<template v-if="!this.Peer.restricted">
<template v-if="!this.Peer.private_key">
<li>
<small class="w-100 dropdown-item text-muted"
style="white-space: break-spaces; font-size: 0.7rem"
>Download & QR Code is not available due to no <code>private key</code>
set for this peer
</small>
</li>
</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>
<a class="dropdown-item d-flex" role="button"
@click="this.$emit('setting')"
>
<i class="me-auto bi bi-pen"></i> Edit
</a>
</li>
<li>
<a class="dropdown-item d-flex" role="button"
@click="this.$emit('jobs')"
>
<i class="me-auto bi bi-app-indicator"></i> Schedule Jobs
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item d-flex text-warning"
@click="this.restrictPeer()"
:class="{disabled: this.restrictBtnDisabled}"
role="button">
<i class="me-auto bi bi-lock"></i> {{!this.restrictBtnDisabled ? "Restrict Access":"Restricting..."}}
</a>
</li>
<li>
<a class="dropdown-item d-flex fw-bold text-danger"
@click="this.deletePeer()"
:class="{disabled: this.deleteBtnDisabled}"
role="button">
<i class="me-auto bi bi-trash"></i> {{!this.deleteBtnDisabled ? "Delete":"Deleting..."}}
</a>
</li>
</template>
<template v-else>
<li>
<a class="dropdown-item d-flex text-warning"
@click="this.allowAccessPeer()"
:class="{disabled: this.restrictBtnDisabled}"
role="button">
<i class="me-auto bi bi-unlock"></i>
{{!this.allowAccessBtnDisabled ? "Allow Access":"Allowing..."}}
</a>
</li>
</template>
</ul>
</template>
<style scoped>
.dropdown-menu{
right: 1rem;
min-width: 200px;
}
.dropdown-item.disabled, .dropdown-item:disabled{
opacity: 0.7;
}
</style>

View File

@@ -0,0 +1,166 @@
<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(){
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>

View File

@@ -0,0 +1,23 @@
<script>
export default {
name: "restrictedPeers"
}
</script>
<template>
<div class="container">
<div class="mb-4">
<RouterLink to="peers" is="div" class="d-flex align-items-center gap-4 text-decoration-none">
<h3 class="mb-0 text-body">
<i class="bi bi-chevron-left"></i>
</h3>
<h3 class="text-body mb-0">Restricted Peers</h3>
</RouterLink>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,64 @@
<script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import ConfigurationCard from "@/components/configurationListComponents/configurationCard.vue";
export default {
name: "configurationList",
components: {ConfigurationCard},
async setup(){
const wireguardConfigurationsStore = WireguardConfigurationsStore();
return {wireguardConfigurationsStore}
},
data(){
return {
configurationLoaded: false
}
},
async mounted() {
await this.wireguardConfigurationsStore.getConfigurations();
this.configurationLoaded = true;
this.wireguardConfigurationsStore.ConfigurationListInterval = setInterval(() => {
this.wireguardConfigurationsStore.getConfigurations()
}, 10000)
},
beforeUnmount() {
clearInterval(this.wireguardConfigurationsStore.ConfigurationListInterval)
}
}
</script>
<template>
<div class="mt-5">
<div class="container">
<div class="d-flex mb-4 ">
<h3 class="text-body">
<i class="bi bi-body-text me-2"></i>
WireGuard Configurations</h3>
<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>
Configuration
</RouterLink>
</div>
<Transition name="fade" mode="out-in">
<div v-if="this.configurationLoaded">
<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".
</p>
<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>
</div>
</div>
</Transition>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,98 @@
<script>
import {fetchGet} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "configurationCard",
props: {
c: {
Name: String,
Status: Boolean,
PublicKey: String,
PrivateKey: String
}
},
data(){
return{
configurationToggling: false
}
},
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
return {dashboardConfigurationStore}
},
methods: {
toggle(){
this.configurationToggling = true;
fetchGet("/api/toggleWireguardConfiguration/", {
configurationName: this.c.Name
}, (res) => {
if (res.status){
this.dashboardConfigurationStore.newMessage("Server",
`${this.c.Name} is ${res.data ? 'is on':'is off'}`)
}else{
this.dashboardConfigurationStore.newMessage("Server",
res.message, 'danger')
}
this.c.Status = res.data
this.configurationToggling = false;
})
}
}
}
</script>
<template>
<div class="card conf_card rounded-3 shadow text-decoration-none">
<RouterLink :to="'/configuration/' + c.Name + '/peers'" class="card-body d-flex align-items-center gap-3 flex-wrap text-decoration-none">
<h6 class="mb-0"><span class="dot" :class="{active: c.Status}"></span></h6>
<h6 class="card-title mb-0"><samp>{{c.Name}}</samp></h6>
<h6 class="mb-0 ms-auto">
<i class="bi bi-chevron-right"></i>
</h6>
</RouterLink>
<div class="card-footer d-flex gap-2 flex-column">
<div class="d-flex gap-4">
<small >
<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">
<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">
<i class="bi bi-arrow-up me-2"></i>{{c.DataUsage.Sent > 0 ? c.DataUsage.Sent.toFixed(4) : 0}} GB
</small>
<small class="ms-auto">
<span class="dot me-2" :class="{active: c.ConnectedPeers > 0}"></span>{{c.ConnectedPeers}} Peers
</small>
</div>
<div class="d-flex align-items-center">
<small class="me-2 text-muted">
<strong>Public Key</strong>
</small>
<small class="mb-0 d-block d-lg-inline-block ">
<samp style="line-break: anywhere">{{c.PublicKey}}</samp>
</small>
<div class="form-check form-switch ms-auto">
<label class="form-check-label" style="cursor: pointer" :for="'switch' + c.PrivateKey">
{{this.configurationToggling ? 'Turning ':''}}
{{c.Status ? "On":"Off"}}
<span v-if="this.configurationToggling"
class="spinner-border spinner-border-sm" aria-hidden="true"></span>
</label>
<input class="form-check-input"
style="cursor: pointer"
:disabled="this.configurationToggling"
type="checkbox" role="switch" :id="'switch' + c.PrivateKey"
@change="this.toggle()"
v-model="c.Status"
>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,32 @@
<script>
export default {
name: "message",
props: {
message: Object
},
mounted() {
setTimeout(() => {
this.message.show = false
}, 5000)
}
}
</script>
<template>
<div class="card shadow rounded-3 position-relative mb-2"
:class="{
'text-bg-danger': this.message.type === 'danger',
'text-bg-success': this.message.type === 'success',
'text-bg-warning': this.message.type === 'warning'}"
:id="this.message.id"
style="width: 400px">
<div class="card-body">
<small class="fw-bold d-block" style="text-transform: uppercase">FROM {{this.message.from}}</small>
{{this.message.content}}
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,79 @@
<script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "navbar",
setup(){
const wireguardConfigurationsStore = WireguardConfigurationsStore();
const dashboardConfigurationStore = DashboardConfigurationStore();
return {wireguardConfigurationsStore, dashboardConfigurationStore}
}
}
</script>
<template>
<div class="col-md-3 col-lg-2 d-md-block p-3" style="height: calc(-50px + 100vh);">
<nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow overflow-y-scroll" >
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column px-2">
<li class="nav-item">
<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">
<RouterLink class="nav-link rounded-3" to="/settings"
exact-active-class="active">
<i class="bi bi-gear me-2"></i>
Settings</RouterLink></li>
</ul>
<hr class="text-body">
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted text-center">
<i class="bi bi-body-text me-2"></i>
Configurations
</h6>
<ul class="nav flex-column px-2">
<li class="nav-item">
<RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link rounded-3"
active-class="active"
v-for="c in this.wireguardConfigurationsStore.Configurations">
{{c.Name}}
</RouterLink>
</li>
</ul>
<hr class="text-body">
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted text-center">
<i class="bi bi-tools me-2"></i>
Tools
</h6>
<ul class="nav flex-column px-2">
<li class="nav-item">
<RouterLink to="/ping" class="nav-link rounded-3" active-class="active">Ping</RouterLink></li>
<li class="nav-item">
<RouterLink to="/traceroute" class="nav-link rounded-3" active-class="active">Traceroute</RouterLink>
</li>
</ul>
<hr class="text-body">
<ul class="nav flex-column px-2">
<li class="nav-item"><a class="nav-link text-danger rounded-3"
@click="this.dashboardConfigurationStore.signOut()"
role="button" style="font-weight: bold">
<i class="bi bi-box-arrow-left me-2"></i>
Sign Out</a></li>
</ul>
<ul class="nav flex-column">
<li class="nav-item"><a href="https://github.com/donaldzou/WGDashboard/releases/tag/"><small class="nav-link text-muted"></small></a></li>
</ul>
</div>
</nav>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,120 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "accountSettingsInputPassword",
props:{
targetData: String,
warning: false,
warningText: ""
},
setup(){
const store = DashboardConfigurationStore();
const uuid = `input_${v4()}`;
return {store, uuid};
},
data(){
return{
value: {
currentPassword: "",
newPassword: "",
repeatNewPassword: ""
},
invalidFeedback: "",
showInvalidFeedback: false,
isValid: false,
timeout: undefined
}
},
methods:{
async useValidation(){
if (Object.values(this.value).find(x => x.length === 0) === undefined){
if (this.value.newPassword === this.value.repeatNewPassword){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Account",
key: this.targetData,
value: this.value
}, (res) => {
if (res.status){
this.isValid = true;
this.showInvalidFeedback = false;
this.store.Configuration.Account[this.targetData] = this.value
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.isValid = false;
this.value = {
currentPassword: "",
newPassword: "",
repeatNewPassword: ""
}
}, 5000);
}else{
this.isValid = false;
this.showInvalidFeedback = true;
this.invalidFeedback = res.message
}
})
}else{
this.showInvalidFeedback = true;
this.invalidFeedback = "New passwords does not match"
}
}else{
this.showInvalidFeedback = true;
this.invalidFeedback = "Please fill in all required fields."
}
}
}
}
</script>
<template>
<div class="d-flex flex-column">
<div class="row">
<div class="col-sm">
<div class="form-group mb-2">
<label :for="'currentPassword_' + this.uuid" class="text-muted mb-1">
<strong><small>Current Password</small></strong>
</label>
<input type="password" class="form-control mb-2"
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
v-model="this.value.currentPassword"
:id="'currentPassword_' + this.uuid">
<div class="invalid-feedback d-block" v-if="showInvalidFeedback">{{this.invalidFeedback}}</div>
</div>
</div>
<div class="col-sm">
<div class="form-group mb-2">
<label :for="'newPassword_' + this.uuid" class="text-muted mb-1">
<strong><small>New Password</small></strong>
</label>
<input type="password" class="form-control mb-2"
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
v-model="this.value.newPassword"
:id="'newPassword_' + this.uuid">
</div>
</div>
<div class="col-sm">
<div class="form-group mb-2">
<label :for="'repeatNewPassword_' + this.uuid" class="text-muted mb-1">
<strong><small>Repeat New Password</small></strong>
</label>
<input type="password" class="form-control mb-2"
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
v-model="this.value.repeatNewPassword"
:id="'repeatNewPassword_' + this.uuid">
</div>
</div>
</div>
<button class="ms-auto btn bg-success-subtle text-success-emphasis border-1 border-success-subtle rounded-3 shadow-sm" @click="this.useValidation()">
<i class="bi bi-save2-fill me-2"></i>Update Password
</button>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,86 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "accountSettingsInputUsername",
props:{
targetData: String,
title: String,
warning: false,
warningText: ""
},
setup(){
const store = DashboardConfigurationStore();
const uuid = `input_${v4()}`;
return {store, uuid};
},
data(){
return{
value:"",
invalidFeedback: "",
showInvalidFeedback: false,
isValid: false,
timeout: undefined,
changed: false,
updating: false,
}
},
mounted() {
this.value = this.store.Configuration.Account[this.targetData];
},
methods:{
async useValidation(){
if (this.changed){
this.updating = true
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Account",
key: this.targetData,
value: this.value
}, (res) => {
if (res.status){
this.isValid = true;
this.showInvalidFeedback = false;
this.store.Configuration.Account[this.targetData] = this.value
clearTimeout(this.timeout)
this.timeout = setTimeout(() => this.isValid = false, 5000);
}else{
this.isValid = false;
this.showInvalidFeedback = true;
this.invalidFeedback = res.message
}
this.changed = false
this.updating = false;
})
}
}
}
}
</script>
<template>
<div class="form-group mb-2">
<label :for="this.uuid" class="text-muted mb-1">
<strong><small>{{this.title}}</small></strong>
</label>
<input type="text" class="form-control"
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
:id="this.uuid"
v-model="this.value"
@keydown="this.changed = true"
@blur="useValidation()"
:disabled="this.updating"
>
<div class="invalid-feedback">{{this.invalidFeedback}}</div>
<div class="px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1"
v-if="warning"
>
<small><i class="bi bi-exclamation-triangle-fill me-2"></i><span v-html="warningText"></span></small>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -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="allowAPIKeysSwitch">
</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>

View File

@@ -0,0 +1,129 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import NewDashboardAPIKey from "@/components/settingsComponent/dashboardAPIKeysComponents/newDashboardAPIKey.vue";
import DashboardAPIKey from "@/components/settingsComponent/dashboardAPIKeysComponents/dashboardAPIKey.vue";
export default {
name: "dashboardAPIKeys",
components: {DashboardAPIKey, NewDashboardAPIKey},
setup(){
const store = DashboardConfigurationStore();
return {store};
},
data(){
return {
value: this.store.Configuration.Server.dashboard_api_key,
apiKeys: [],
newDashboardAPIKey: false
}
},
methods: {
async toggleDashboardAPIKeys(){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Server",
key: "dashboard_api_key",
value: this.value
}, (res) => {
if (res.status){
this.store.Configuration.Peers[this.targetData] = this.value;
this.store.newMessage("Server",
`API Keys function is successfully ${this.value ? 'enabled':'disabled'}`, "success")
}else{
this.value = this.store.Configuration.Peers[this.targetData];
this.store.newMessage("Server",
`API Keys function is failed ${this.value ? 'enabled':'disabled'}`, "danger")
}
})
},
},
watch: {
value:{
immediate: true,
handler(newValue){
if (newValue){
fetchGet("/api/getDashboardAPIKeys", {}, (res) => {
console.log(res)
if(res.status){
this.apiKeys = res.data
}else{
this.apiKeys = []
this.store.newMessage("Server", res.message, "danger")
}
})
}else{
this.apiKeys = []
}
}
}
}
}
</script>
<template>
<div class="card mb-4 shadow rounded-3">
<div class="card-header d-flex">
API Keys
<div class="form-check form-switch ms-auto">
<input class="form-check-input" type="checkbox"
v-model="this.value"
@change="this.toggleDashboardAPIKeys()"
role="switch" id="allowAPIKeysSwitch">
<label class="form-check-label" for="allowAPIKeysSwitch">
{{this.value ? 'Enabled':'Disabled'}}
</label>
</div>
</div>
<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"
@click="this.newDashboardAPIKey = true"
>
<i class="bi bi-key me-2"></i> Create
</button>
<div class="card" style="height: 300px" v-if="this.apiKeys.length === 0">
<div class="card-body d-flex text-muted">
<span class="m-auto">
No Dashboard API Key
</span>
</div>
</div>
<div class="d-flex flex-column gap-2 position-relative" v-else style="min-height: 300px">
<TransitionGroup name="apiKey">
<DashboardAPIKey v-for="key in this.apiKeys" :apiKey="key"
:key="key.Key"
@deleted="(nkeys) => this.apiKeys = nkeys"></DashboardAPIKey>
</TransitionGroup>
</div>
<Transition name="zoomReversed">
<NewDashboardAPIKey v-if="this.newDashboardAPIKey"
@created="(data) => this.apiKeys = data"
@close="this.newDashboardAPIKey = false"
></NewDashboardAPIKey>
</Transition>
</div>
</div>
</template>
<style scoped>
.apiKey-move, /* apply transition to moving elements */
.apiKey-enter-active,
.apiKey-leave-active {
transition: all 0.5s ease;
}
.apiKey-enter-from,
.apiKey-leave-to {
opacity: 0;
transform: translateY(30px) scale(0.9);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.apiKey-leave-active {
position: absolute;
width: 100%;
}
</style>

View File

@@ -0,0 +1,66 @@
<script>
import {fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "dashboardAPIKey",
props: {
apiKey: Object
},
setup(){
const store = DashboardConfigurationStore();
return {store};
},
data(){
return {
confirmDelete: false
}
},
methods: {
deleteAPIKey(){
fetchPost("/api/deleteDashboardAPIKey", {
Key: this.apiKey.Key
}, (res) => {
if (res.status){
this.$emit('deleted', res.data);
this.store.newMessage("Server", "API Key deleted", "success");
}else{
this.store.newMessage("Server", res.message, "danger")
}
})
}
}
}
</script>
<template>
<div class="card rounded-3 shadow-sm">
<div class="card-body d-flex gap-3 align-items-center" v-if="!this.confirmDelete">
<div class="d-flex align-items-center gap-2">
<small class="text-muted">Key</small>{{this.apiKey.Key}}
</div>
<div class="d-flex align-items-center gap-2 ms-auto">
<small class="text-muted">Expire At</small>
{{this.apiKey.ExpiredAt ? this.apiKey.ExpiredAt : 'Never'}}
</div>
<a role="button" class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3" @click="this.confirmDelete = true">
<i class="bi bi-trash-fill"></i>
</a>
</div>
<div v-else class="card-body d-flex gap-3 align-items-center justify-content-end">
Are you sure to delete this API key?
<a role="button" class="btn btn-sm bg-success-subtle text-success-emphasis rounded-3"
@click="this.deleteAPIKey()"
>
<i class="bi bi-check-lg"></i>
</a>
<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>
</a>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,100 @@
<script>
import dayjs from "dayjs";
import {fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import VueDatePicker from "@vuepic/vue-datepicker";
export default {
name: "newDashboardAPIKey",
components: {VueDatePicker},
data(){
return{
newKeyData:{
ExpiredAt: dayjs().add(7, 'd').format("YYYY-MM-DD HH:mm:ss"),
neverExpire: false
},
submitting: false
}
},
setup(){
const store = DashboardConfigurationStore();
return {store};
},
mounted() {
console.log(this.newKeyData.ExpiredAt)
},
methods: {
submitNewAPIKey(){
this.submitting = true;
fetchPost('/api/newDashboardAPIKey', this.newKeyData, (res) => {
if (res.status){
this.$emit('created', res.data);
this.store.newMessage("Server", "New API Key created", "success");
this.$emit('close')
}else{
this.store.newMessage("Server", res.message, "danger")
}
this.submitting = false;
})
},
fixDate(date){
console.log(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
}
}
}
}
</script>
<template>
<div class="position-absolute w-100 h-100 top-0 start-0 rounded-bottom-3 p-3 d-flex"
style="background-color: #00000060; backdrop-filter: blur(3px)">
<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">
<h6 class="mb-0">Create API Key</h6>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div>
<div class="card-body d-flex gap-2 p-4 flex-column">
<small class="text-muted">When should this API Key expire?</small>
<div class="d-flex align-items-center gap-2">
<VueDatePicker
: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"
:dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
/>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox"
v-model="this.newKeyData.neverExpire" id="neverExpire" :disabled="this.submitting">
<label class="form-check-label" for="neverExpire">
Never Expire (<i class="bi bi-emoji-grimace-fill"></i> Don't think that's a good idea)
</label>
</div>
<button class="ms-auto btn bg-success-subtle text-success-emphasis border-1 border-success-subtle rounded-3 shadow-sm"
:class="{disabled: this.submitting}"
@click="this.submitNewAPIKey()"
>
<i class="bi bi-check-lg me-2" v-if="!this.submitting"></i>
{{this.submitting ? 'Creating...':'Done'}}
</button>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,91 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "dashboardSettingsInputIPAddressAndPort",
props:{
// targetData: String,
// title: String,
// warning: false,
// warningText: ""
},
setup(){
const store = DashboardConfigurationStore();
const uuid = `input_${v4()}`;
return {store, uuid};
},
data(){
return{
app_ip:"",
app_port:"",
invalidFeedback: "",
showInvalidFeedback: false,
isValid: false,
timeout: undefined,
changed: false,
updating: false,
}
},
mounted() {
this.app_ip = this.store.Configuration.Server.app_ip;
this.app_port = this.store.Configuration.Server.app_port;
},
methods:{
async useValidation(){
if(this.changed){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Server",
key: this.targetData,
value: this.value
}, (res) => {
if (res.status){
this.isValid = true;
this.showInvalidFeedback = false;
this.store.Configuration.Account[this.targetData] = this.value
clearTimeout(this.timeout)
this.timeout = setTimeout(() => this.isValid = false, 5000);
}else{
this.isValid = false;
this.showInvalidFeedback = true;
this.invalidFeedback = res.message
}
})
}
}
}
}
</script>
<template>
<div>
<div class="invalid-feedback d-block mt-0">{{this.invalidFeedback}}</div>
<div class="row">
<div class="form-group mb-2 col-sm">
<label :for="'app_ip_' + this.uuid" class="text-muted mb-1">
<strong><small>Dashboard IP Address</small></strong>
</label>
<input type="text" class="form-control mb-2" :id="'app_ip_' + this.uuid" v-model="this.app_ip">
<div class="px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block">
<small><i class="bi bi-exclamation-triangle-fill me-2"></i><code>0.0.0.0</code> means it can be access by anyone with your server
IP Address.</small>
</div>
</div>
<div class="form-group col-sm">
<label :for="'app_port_' + this.uuid" class="text-muted mb-1">
<strong><small>Dashboard Port</small></strong>
</label>
<input type="text" class="form-control mb-2" :id="'app_port_' + this.uuid" v-model="this.app_port">
</div>
</div>
<button class="btn btn-success btn-sm fw-bold rounded-3">
<i class="bi bi-floppy-fill me-2"></i>Update Dashboard Settings & Restart
</button>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,86 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "dashboardSettingsInputWireguardConfigurationPath",
props:{
targetData: String,
title: String,
warning: false,
warningText: ""
},
setup(){
const store = DashboardConfigurationStore();
const uuid = `input_${v4()}`;
return {store, uuid};
},
data(){
return{
value:"",
invalidFeedback: "",
showInvalidFeedback: false,
isValid: false,
timeout: undefined,
changed: false,
updating: false,
}
},
mounted() {
this.value = this.store.Configuration.Server[this.targetData];
},
methods:{
async useValidation(){
if(this.changed){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Server",
key: this.targetData,
value: this.value
}, (res) => {
if (res.status){
this.isValid = true;
this.showInvalidFeedback = false;
this.store.Configuration.Account[this.targetData] = this.value
clearTimeout(this.timeout)
this.timeout = setTimeout(() => this.isValid = false, 5000);
}else{
this.isValid = false;
this.showInvalidFeedback = true;
this.invalidFeedback = res.message
}
this.changed = false;
this.updating = false
})
}
}
}
}
</script>
<template>
<div class="form-group mb-2">
<label :for="this.uuid" class="text-muted mb-1">
<strong><small>{{this.title}}</small></strong>
</label>
<input type="text" class="form-control"
:class="{'is-invalid': this.showInvalidFeedback, 'is-valid': this.isValid}"
:id="this.uuid"
v-model="this.value"
@keydown="this.changed = true"
@blur="this.useValidation()"
:disabled="this.updating"
>
<div class="invalid-feedback">{{this.invalidFeedback}}</div>
<div class="px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1"
v-if="warning"
>
<small><i class="bi bi-exclamation-triangle-fill me-2"></i><span v-html="warningText"></span></small>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,49 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "dashboardTheme",
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
return {dashboardConfigurationStore}
},
methods: {
async switchTheme(value){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Server",
key: "dashboard_theme",
value: value
}, (res) => {
if (res.status){
this.dashboardConfigurationStore.Configuration.Server.dashboard_theme = value;
}
});
}
}
}
</script>
<template>
<div class="card mb-4 shadow rounded-3">
<p class="card-header">Dashboard Theme</p>
<div class="card-body d-flex gap-2">
<button class="btn bg-primary-subtle text-primary-emphasis flex-grow-1"
@click="this.switchTheme('light')"
:class="{active: this.dashboardConfigurationStore.Configuration.Server.dashboard_theme === 'light'}">
<i class="bi bi-sun-fill"></i>
Light
</button>
<button class="btn bg-primary-subtle text-primary-emphasis flex-grow-1"
@click="this.switchTheme('dark')"
:class="{active: this.dashboardConfigurationStore.Configuration.Server.dashboard_theme === 'dark'}">
<i class="bi bi-moon-fill"></i>
Dark
</button>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,85 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchPost} from "@/utilities/fetch.js";
export default {
props:{
targetData: String,
title: String,
warning: false,
warningText: "",
},
setup(){
const store = DashboardConfigurationStore();
const uuid = `input_${v4()}`;
return {store, uuid};
},
data(){
return{
value:"",
invalidFeedback: "",
showInvalidFeedback: false,
isValid: false,
timeout: undefined,
changed: false,
updating: false,
}
},
mounted() {
this.value = this.store.Configuration.Peers[this.targetData];
},
methods:{
async useValidation(){
if(this.changed){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Peers",
key: this.targetData,
value: this.value
}, (res) => {
if (res.status){
this.isValid = true;
this.showInvalidFeedback = false;
this.store.Configuration.Peers[this.targetData] = this.value
clearTimeout(this.timeout)
this.timeout = setTimeout(() => this.isValid = false, 5000);
}else{
this.isValid = false;
this.showInvalidFeedback = true;
this.invalidFeedback = res.message
}
this.changed = false
this.updating = false;
})
}
}
}
}
</script>
<template>
<div class="form-group mb-2">
<label :for="this.uuid" class="text-muted mb-1">
<strong><small>{{this.title}}</small></strong>
</label>
<input type="text" class="form-control"
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
:id="this.uuid"
v-model="this.value"
@keydown="this.changed = true"
@blur="useValidation()"
:disabled="this.updating"
>
<div class="invalid-feedback">{{this.invalidFeedback}}</div>
<div class="px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1"
v-if="warning"
>
<small><i class="bi bi-exclamation-triangle-fill me-2"></i><span v-html="warningText"></span></small>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,119 @@
<script>
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import QRCode from "qrcode";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "totp",
async setup(){
const store = DashboardConfigurationStore();
let l = ""
await fetchGet("/api/Welcome_GetTotpLink", {}, (res => {
if (res.status) l = res.data;
}));
return {l, store}
},
mounted() {
if (this.l) {
QRCode.toCanvas(document.getElementById('qrcode'), this.l, function (error) {})
}
},
data(){
return {
totp: "",
totpInvalidMessage: "",
verified: false
}
},
methods: {
validateTotp(){
}
},
watch: {
totp(newVal){
const input = document.querySelector("#totp");
input.classList.remove("is-invalid", "is-valid")
if (newVal.length === 6){
console.log(newVal)
if (/[0-9]{6}/.test(newVal)){
fetchPost("/api/Welcome_VerifyTotpLink", {
totp: newVal
}, (res) => {
if (res.status){
this.verified = true;
input.classList.add("is-valid");
this.$emit("verified")
}else{
input.classList.add("is-invalid")
this.totpInvalidMessage = "TOTP does not match."
}
})
}else{
input.classList.add("is-invalid");
this.totpInvalidMessage = "TOTP can only contain numbers"
}
}
}
}
}
</script>
<template>
<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>
<canvas id="qrcode" class="rounded-3 mb-2"></canvas>
<div class="p-3 bg-body-secondary rounded-3 border mb-3">
<p class="text-muted mb-0"><small>Or you can click the link below:</small>
</p><a :href="this.l"><code style="line-break: anywhere">{{this.l}}</code></a>
</div>
<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 mb-2">
<input class="form-control text-center totp"
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
v-model="this.totp"
:disabled="this.verified"
>
<div class="invalid-feedback">
{{this.totpInvalidMessage}}
</div>
<div class="valid-feedback">
TOTP verified!
</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>
<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>
<style scoped>
</style>

View File

@@ -0,0 +1,26 @@
import '../../css/dashboard.css'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.js'
import 'bootstrap-icons/font/bootstrap-icons.css'
import 'animate.css/animate.compat.css'
import '@vuepic/vue-datepicker/dist/main.css'
import {createApp, markRaw} from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
const pinia = createPinia();
pinia.use(({ store }) => {
store.$router = markRaw(router)
})
app.use(pinia)
app.mount('#app')

View File

@@ -0,0 +1,18 @@
import {fetchGet} from "@/utilities/fetch.js";
export class WireguardConfigurations{
Configurations = undefined;
constructor() {
this.Configurations = undefined
}
async initialization(){
await this.getConfigurations()
}
async getConfigurations(){
await fetchGet("/api/getWireguardConfigurations", {}, (res) => {
if (res.status) this.Configurations = res.data
});
}
}

View File

@@ -0,0 +1,157 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import {cookie} from "../utilities/cookie.js";
import Index from "@/views/index.vue"
import Signin from "@/views/signin.vue";
import ConfigurationList from "@/components/configurationList.vue";
import {fetchGet} from "@/utilities/fetch.js";
import Settings from "@/views/settings.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Setup from "@/views/setup.vue";
import NewConfiguration from "@/views/newConfiguration.vue";
import Configuration from "@/views/configuration.vue";
import PeerList from "@/components/configurationComponents/peerList.vue";
import PeerCreate from "@/components/configurationComponents/peerCreate.vue";
import Ping from "@/views/ping.vue";
import Traceroute from "@/views/traceroute.vue";
import Totp from "@/components/setupComponent/totp.vue";
import Share from "@/views/share.vue";
const checkAuth = async () => {
let result = false
await fetchGet("/api/validateAuthentication", {}, (res) => {
result = res.status
});
return result;
}
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
name: "Index",
path: '/',
component: Index,
meta: {
requiresAuth: true,
},
children: [
{
name: "Configuration List",
path: '',
component: ConfigurationList,
meta: {
title: "WireGuard Configurations"
}
},
{
name: "Settings",
path: '/settings',
component: Settings,
meta: {
title: "Settings"
}
},
{
path: '/ping',
name: "Ping",
component: Ping,
},
{
path: '/traceroute',
name: "Traceroute",
component: Traceroute,
},
{
name: "New Configuration",
path: '/new_configuration',
component: NewConfiguration,
meta: {
title: "New Configuration"
}
},
{
name: "Configuration",
path: '/configuration/:id',
component: Configuration,
meta: {
title: "Configuration"
},
children: [
{
name: "Peers List",
path: 'peers',
component: PeerList
},
{
name: "Peers Create",
path: 'create',
component: PeerCreate
},
]
},
]
},
{
path: '/signin', component: Signin,
meta: {
title: "Sign In"
}
},
{
path: '/welcome', component: Setup,
meta: {
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"
}
}
]
});
router.beforeEach(async (to, from, next) => {
const wireguardConfigurationsStore = WireguardConfigurationsStore();
const dashboardConfigurationStore = DashboardConfigurationStore();
if (to.meta.title){
if (to.params.id){
document.title = to.params.id + " | WGDashboard";
}else{
document.title = to.meta.title + " | WGDashboard";
}
}else{
document.title = "WGDashboard"
}
if (to.meta.requiresAuth){
if (cookie.getCookie("authToken") && await checkAuth()){
await dashboardConfigurationStore.getConfiguration()
if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){
await wireguardConfigurationsStore.getConfigurations();
}
dashboardConfigurationStore.Redirect = undefined;
next()
}else{
dashboardConfigurationStore.Redirect = to;
next("/signin")
dashboardConfigurationStore.newMessage("WGDashboard", "Session Ended", "warning")
}
}else {
next();
}
});
export default router

View File

@@ -0,0 +1,43 @@
import {defineStore} from "pinia";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {v4} from "uuid";
export const DashboardConfigurationStore = defineStore('DashboardConfigurationStore', {
state: () => ({
Redirect: undefined,
Configuration: undefined,
Messages: [],
Peers: {
Selecting: false,
RefreshInterval: undefined
}
}),
actions: {
async getConfiguration(){
await fetchGet("/api/getDashboardConfiguration", {}, (res) => {
if (res.status) this.Configuration = res.data
});
},
async updateConfiguration(){
await fetchPost("/api/updateDashboardConfiguration", {
DashboardConfiguration: this.Configuration
}, (res) => {
console.log(res)
})
},
async signOut(){
await fetchGet("/api/signout", {}, (res) => {
this.$router.go('/signin')
});
},
newMessage(from, content, type){
this.Messages.push({
id: v4(),
from: from,
content: content,
type: type,
show: true
})
}
}
});

View File

@@ -0,0 +1,83 @@
import {defineStore} from "pinia";
import {fetchGet} from "@/utilities/fetch.js";
import isCidr from "is-cidr";
export const WireguardConfigurationsStore = defineStore('WireguardConfigurationsStore', {
state: () => ({
Configurations: undefined,
searchString: "",
ConfigurationListInterval: undefined,
PeerScheduleJobs: {
dropdowns: {
Field: [
{
display: "Total Received",
value: "total_receive",
unit: "GB",
type: 'number'
},
{
display: "Total Sent",
value: "total_sent",
unit: "GB",
type: 'number'
},
{
display: "Total Data",
value: "total_data",
unit: "GB",
type: 'number'
},
{
display: "Date",
value: "date",
type: 'date'
}
],
Operator: [
{
display: "equal",
value: "eq"
},
{
display: "not equal",
value: "neq"
},
{
display: "larger than",
value: "lgt"
},
{
display: "less than",
value: "lst"
},
],
Action: [
{
display: "Restrict Peer",
value: "restrict"
},
{
display: "Delete Peer",
value: "delete"
}
]
}
}
}),
actions: {
async getConfigurations(){
await fetchGet("/api/getWireguardConfigurations", {}, (res) => {
if (res.status) this.Configurations = res.data
});
},
regexCheckIP(ip){
let regex = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/;
return regex.test(ip)
},
checkCIDR(ip){
return isCidr(ip) !== 0
},
}
});

View File

@@ -0,0 +1,20 @@
import {defineStore} from "pinia";
import {fetchGet} from "@/utilities/fetch.js";
export const wgdashboardStore = defineStore('WGDashboardStore', {
state: () => ({
WireguardConfigurations: undefined,
DashboardConfiguration: undefined
}),
actions: {
async getDashboardConfiguration(){
await fetchGet("/api/getDashboardConfiguration", {}, (res) => {
console.log(res.status)
if (res.status) this.DashboardConfiguration = res.data
})
}
}
});

View File

@@ -0,0 +1,17 @@
import subprocess
def _generateKeyPairs(amount: int) -> list[list[str]] | None:
try:
pairs = subprocess.check_output(
f'''for ((i = 0 ; i<{amount} ; i++ ));do privateKey=$(wg genkey) presharedKey=$(wg genkey) publicKey=$(wg pubkey <<< "$privateKey") echo "$privateKey,$publicKey,$presharedKey"; done''', shell=True, stderr=subprocess.STDOUT
)
pairs = pairs.decode().split("\n")
print(pairs)
return [x.split(",") for x in pairs]
except subprocess.CalledProcessError as exp:
print(str(exp))
return []
_generateKeyPairs(20)

Some files were not shown because too many files have changed in this diff Show More