Compare commits

...

140 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
84c9846f7b Hmmmm... What about SocketIO? 2022-01-19 10:27:17 -05:00
129 changed files with 16175 additions and 11106 deletions

38
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.vscode/sftp.json .vscode
src/.vscode/sftp.json
.DS_Store .DS_Store
.idea .idea
src/db src/db
@@ -16,3 +15,38 @@ log/**
release/* release/*
src/db/wgdashboard.db src/db/wgdashboard.db
.jshintrc .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

130
README.md
View File

@@ -10,40 +10,35 @@
<p align="center"> <p align="center">
<a href="https://github.com/donaldzou/wireguard-dashboard/releases/latest"><img src="https://img.shields.io/github/v/release/donaldzou/wireguard-dashboard"></a> <a href="https://github.com/donaldzou/wireguard-dashboard/releases/latest"><img src="https://img.shields.io/github/v/release/donaldzou/wireguard-dashboard"></a>
<a href="https://wakatime.com/badge/user/45f53c7c-9da9-4cb0-85d6-17bd38cc748b/project/5334ae20-e9a6-4c55-9fea-52d4eb9dfba6"><img src="https://wakatime.com/badge/user/45f53c7c-9da9-4cb0-85d6-17bd38cc748b/project/5334ae20-e9a6-4c55-9fea-52d4eb9dfba6.svg" alt="wakatime"></a> <a href="https://wakatime.com/badge/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>
<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>Note: This project is not affiliate to the official WireGuard Project ;)</small></p> <p align="center"><small><i>This project is not affiliate to the official WireGuard Project</i></small></p>
## 📣 What's New: v3.0 ## 📣 What's New: v4.0
> I can't thank enough for all of you who wait for this release, and for those who are new to this project, welcome :) Also, huge thanks who sponsored me GitHub :heart:
- 🎉 **New Features** - 🎉 **New Features**
- **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**. - **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.
- **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:** - **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.
- **Add Peers by Bulk:** User can add peers by bulk, just simply set the amount and click add. - **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.
- **Delete Peers by Bulk**: User can delete peers by bulk, without deleting peers one by one. - **Logging**: Dashboard will now log all activity on the dashboard and API requests.
- **Download Peers in Zip**: User can download all *downloadable* peers in a zip. - **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.
- **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. - **Designs**
- **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. - **Real-time Graphs**: You can view real-time data changes with graphs in each configuration.
- **Added Some [🥘 Experimental Functions](#-experimental-functions)** - **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.
- 🪚 **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** - **🧐 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)! - **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.**
- **Peer transfer calculation**: each peer will now show all transfer amount (previously was only showing transfer amount from the last configuration start-up). - Rewrote the backend into a REST API structure
- **UI adjustment on running peers**: peers will have a new style indicating that it is running. - Improved SQL query efficient
- **`wgd.sh` finally can update itself**: So now user could update the whole dashboard from `wgd.sh`, with the `update` command. - Removed all templates, except for `index.html` where it will load the Vue.js app.
- **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:*
> If you have any other brilliant ideas for this project, please shout it in here [#129](https://github.com/donaldzou/WGDashboard/issues/129) :heart:
**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 ;)** **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> <hr>
@@ -68,19 +63,23 @@
## 💡 Features ## 💡 Features
- **No need to re-configure existing WireGuard configuration! It can search for existed configuration files.** - Automatically look for existing WireGuard configuration under `/etc/wireguard`
- Easy to use interface, provided username and password protection to the dashboard - Easy to use interface, provided credential and TOTP protection to the dashboard
- Add peers and edit (Allowed IPs, DNS, Private Key...) - Manage peers and configuration
- View peers and configuration real time details (Data Usage, Latest Handshakes...) - Add Peers or by bulk with auto-generated information
- Share your peer configuration with QR code or file download - Edit peer information
- Testing tool: Ping and Traceroute to your peer's ip - Delete peers with ease
- **And more functions are coming up!** - 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
## 📝 Requirement ## 📝 Requirement
- Recommend the following OS, tested by our beloved users: - Recommend the following OS, tested by our beloved users:
- [x] Ubuntu 18.04.1 LTS - 20.04.1 LTS [@Me] - [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] Debian GNU/Linux 10 (buster) [❤️ @[robchez](https://github.com/robchez)]
- [x] AlmaLinux 8.4 (Electric Cheetah) [❤️ @[barry-smithjr](https://github.com/)] - [x] AlmaLinux 8.4 (Electric Cheetah) [❤️ @[barry-smithjr](https://github.com/)]
- [x] CentOS 7 [❤️ @[PrzemekSkw](https://github.com/PrzemekSkw)] - [x] CentOS 7 [❤️ @[PrzemekSkw](https://github.com/PrzemekSkw)]
@@ -107,15 +106,13 @@
# Must have for each peer # Must have for each peer
``` ```
- Python 3.7+ & Pip3 - **Python 3.10** for v4.0+, **Python 3.7 - 3.9** for v2.0 - v3.0.6.2
- Browser support CSS3 and ES6
## 🛠 Install ## 🛠 Install
1. Download WGDashboard 1. Download WGDashboard
```shell ```shell
git clone -b v3.0.1 https://github.com/donaldzou/WGDashboard.git wgdashboard git clone -b v4.0 https://github.com/donaldzou/WGDashboard.git wgdashboard
2. Open the WGDashboard folder 2. Open the WGDashboard folder
@@ -139,7 +136,7 @@
5. Run WGDashboard 5. Run WGDashboard
```shell ```shell
./wgd.sh start sudo ./wgd.sh start
``` ```
**Note**: **Note**:
@@ -262,7 +259,7 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
└─6602 /usr/bin/python3 /root/wgdashboard/src/dashboard.py └─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:26 ubuntu-wg systemd[1]: Started wg-dashboard.service.
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Serving Flask app "WGDashboard" (lazy loading) 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]: * 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]: 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]: Use a production WSGI server instead.
@@ -301,7 +298,7 @@ Since version 2.0, WGDashboard will be using a configuration file called `wg-das
| `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes | | `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes |
| `app_port` | Port the the dashboard will run with | `10086` | Yes | | `app_port` | Port the the dashboard will run with | `10086` | Yes |
| `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** | | `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** |
| `version` | Dashboard Version | `v3.0.1` | **No** | | `version` | Dashboard Version | `v3.0.5` | **No** |
| `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes | | `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes |
| `dashboard_sort` | How configuration is sorting | `status` | Yes | | `dashboard_sort` | How configuration is sorting | `status` | Yes |
| | | | | | | | | |
@@ -348,15 +345,34 @@ Endpoint = 0.0.0.0:51820
<hr> <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` 1. Change your directory to `wgdashboard`
```shell ```shell
cd wgdashboard cd wgdashboard/src
``` ```
2. Update the dashboard 2. Update the dashboard
```shell ```shell
git pull https://github.com/donaldzou/WGDashboard.git v3.0.1 --force git pull https://github.com/donaldzou/WGDashboard.git v3.0.5 --force
``` ```
3. Install 3. Install
@@ -365,6 +381,8 @@ Endpoint = 0.0.0.0:51820
./wgd.sh install ./wgd.sh install
``` ```
Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol) Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol)
## 🥘 Experimental Functions ## 🥘 Experimental Functions
@@ -399,6 +417,33 @@ Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol)
## ⏰ Changelog ## ⏰ 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 #### v2.3.1 - Sep 8, 2021
- Updated dashboard's name to **WGDashboard**!! - Updated dashboard's name to **WGDashboard**!!
@@ -515,4 +560,3 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- ALL-CONTRIBUTORS-LIST:END --> <!-- 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>

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,26 @@
import multiprocessing
import dashboard 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()
app_host, app_port = dashboard.get_host_bind()
worker_class = 'gthread' worker_class = 'gthread'
workers = multiprocessing.cpu_count() * 2 + 1 workers = 1
threads = 4 threads = 1
bind = f"{app_host}:{app_port}" bind = f"{app_host}:{app_port}"
daemon = True daemon = True
pidfile = './gunicorn.pid' pidfile = './gunicorn.pid'
wsgi_app = "dashboard:app"
accesslog = f"./log/access_{date}.log"
log_level = "debug"
capture_output = True
errorlog = f"./log/error_{date}.log"
print(f"[WGDashboard] WGDashboard w/ Gunicorn will be running on {bind}", flush=True)
print(f"[WGDashboard] Access log file is at {accesslog}", flush=True)
print(f"[WGDashboard] Error log file is at {errorlog}", flush=True)

View File

@@ -1,6 +1,8 @@
Flask bcrypt
ifcfg ifcfg
psutil
pyotp
Flask
flask-cors
icmplib icmplib
flask-qrcode
gunicorn gunicorn
certbot

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)

View File

@@ -0,0 +1,9 @@
export const cookie = {
//https://stackoverflow.com/a/15724300
getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
}

View File

@@ -0,0 +1,51 @@
import router from "@/router/index.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export const fetchGet = async (url, params=undefined, callback=undefined) => {
const urlSearchParams = new URLSearchParams(params);
await fetch(`${url}?${urlSearchParams.toString()}`, {
headers: {
"content-type": "application/json"
}
})
.then((x) => {
const store = DashboardConfigurationStore();
if (!x.ok){
if (x.status !== 200){
if (x.status === 401){
router.push({path: '/signin'})
store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
})
}
export const fetchPost = async (url, body, callback) => {
await fetch(`${url}`, {
headers: {
"content-type": "application/json"
},
method: "POST",
body: JSON.stringify(body)
}).then((x) => {
const store = DashboardConfigurationStore();
if (!x.ok){
if (x.status !== 200){
if (x.status === 401){
router.push({path: '/signin'})
store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
})
}

View File

@@ -0,0 +1,3 @@
export const ipV46RegexCheck = (input) => {
}

View File

@@ -0,0 +1,313 @@
/*! SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
(function() {
function gf(init) {
var r = new Float64Array(16);
if (init) {
for (var i = 0; i < init.length; ++i)
r[i] = init[i];
}
return r;
}
function pack(o, n) {
var b, m = gf(), t = gf();
for (var i = 0; i < 16; ++i)
t[i] = n[i];
carry(t);
carry(t);
carry(t);
for (var j = 0; j < 2; ++j) {
m[0] = t[0] - 0xffed;
for (var i = 1; i < 15; ++i) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
m[i - 1] &= 0xffff;
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
b = (m[15] >> 16) & 1;
m[14] &= 0xffff;
cswap(t, m, 1 - b);
}
for (var i = 0; i < 16; ++i) {
o[2 * i] = t[i] & 0xff;
o[2 * i + 1] = t[i] >> 8;
}
}
function carry(o) {
var c;
for (var i = 0; i < 16; ++i) {
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
o[i] &= 0xffff;
}
}
function cswap(p, q, b) {
var t, c = ~(b - 1);
for (var i = 0; i < 16; ++i) {
t = c & (p[i] ^ q[i]);
p[i] ^= t;
q[i] ^= t;
}
}
function add(o, a, b) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] + b[i]) | 0;
}
function subtract(o, a, b) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] - b[i]) | 0;
}
function multmod(o, a, b) {
var t = new Float64Array(31);
for (var i = 0; i < 16; ++i) {
for (var j = 0; j < 16; ++j)
t[i + j] += a[i] * b[j];
}
for (var i = 0; i < 15; ++i)
t[i] += 38 * t[i + 16];
for (var i = 0; i < 16; ++i)
o[i] = t[i];
carry(o);
carry(o);
}
function invert(o, i) {
var c = gf();
for (var a = 0; a < 16; ++a)
c[a] = i[a];
for (var a = 253; a >= 0; --a) {
multmod(c, c, c);
if (a !== 2 && a !== 4)
multmod(c, c, i);
}
for (var a = 0; a < 16; ++a)
o[a] = c[a];
}
function clamp(z) {
z[31] = (z[31] & 127) | 64;
z[0] &= 248;
}
function generatePublicKey(privateKey) {
var r, z = new Uint8Array(32);
var a = gf([1]),
b = gf([9]),
c = gf(),
d = gf([1]),
e = gf(),
f = gf(),
_121665 = gf([0xdb41, 1]),
_9 = gf([9]);
for (var i = 0; i < 32; ++i)
z[i] = privateKey[i];
clamp(z);
for (var i = 254; i >= 0; --i) {
r = (z[i >>> 3] >>> (i & 7)) & 1;
cswap(a, b, r);
cswap(c, d, r);
add(e, a, c);
subtract(a, a, c);
add(c, b, d);
subtract(b, b, d);
multmod(d, e, e);
multmod(f, a, a);
multmod(a, c, a);
multmod(c, b, e);
add(e, a, c);
subtract(a, a, c);
multmod(b, a, a);
subtract(c, d, f);
multmod(a, c, _121665);
add(a, a, d);
multmod(c, c, a);
multmod(a, d, f);
multmod(d, b, _9);
multmod(b, e, e);
cswap(a, b, r);
cswap(c, d, r);
}
invert(c, c);
multmod(a, a, c);
pack(z, a);
return z;
}
function generatePresharedKey() {
var privateKey = new Uint8Array(32);
window.crypto.getRandomValues(privateKey);
return privateKey;
}
function generatePrivateKey() {
var privateKey = generatePresharedKey();
clamp(privateKey);
return privateKey;
}
function encodeBase64(dest, src) {
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
for (var i = 0; i < 4; ++i)
dest[i] = input[i] + 65 +
(((25 - input[i]) >> 8) & 6) -
(((51 - input[i]) >> 8) & 75) -
(((61 - input[i]) >> 8) & 15) +
(((62 - input[i]) >> 8) & 3);
}
function keyToBase64(key) {
var i, base64 = new Uint8Array(44);
for (i = 0; i < 32 / 3; ++i)
encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
base64[43] = 61;
return String.fromCharCode.apply(null, base64);
}
function base64ToKey(base64) {
let binary_string = window.atob(base64);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
let uint8 = new Uint8Array(bytes.buffer);
return uint8;
}
function putU32(b, n)
{
b.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff);
}
function putU16(b, n)
{
b.push(n & 0xff, (n >>> 8) & 0xff);
}
function putBytes(b, a)
{
for (var i = 0; i < a.length; ++i)
b.push(a[i] & 0xff);
}
function encodeString(s)
{
var utf8 = unescape(encodeURIComponent(s));
var b = new Uint8Array(utf8.length);
for (var i = 0; i < utf8.length; ++i)
b[i] = utf8.charCodeAt(i);
return b;
}
function crc32(b)
{
if (!crc32.table) {
crc32.table = [];
for (var c = 0, n = 0; n < 256; c = ++n) {
for (var k = 0; k < 8; ++k)
c = ((c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1));
crc32.table[n] = c;
}
}
var crc = -1;
for (var i = 0; i < b.length; ++i)
crc = (crc >>> 8) ^ crc32.table[(crc ^ b[i]) & 0xff];
return (crc ^ (-1)) >>> 0;
}
function createZipFile(files)
{
var b = [];
var cd = [];
var offset = 0;
for (var i = 0; i < files.length; ++i) {
var name = encodeString(files[i].fileName);
var contents = encodeString(files[i].file);
var crc = crc32(contents);
putU32(b, 0x04034b50); /* signature */
putU16(b, 20); /* version needed */
putU16(b, 0); /* flags */
putU16(b, 0); /* compression method */
putU16(b, 0); /* mtime */
putU16(b, 0); /* mdate */
putU32(b, crc); /* crc32 */
putU32(b, contents.length); /* compressed size */
putU32(b, contents.length); /* uncompressed size */
putU16(b, name.length); /* file name length */
putU16(b, 0); /* extra field length */
putBytes(b, name);
putBytes(b, contents);
putU32(cd, 0x02014b50); /* signature */
putU16(cd, 0); /* version made */
putU16(cd, 20); /* version needed */
putU16(cd, 0); /* flags */
putU16(cd, 0); /* compression method */
putU16(cd, 0); /* mtime */
putU16(cd, 0); /* mdate */
putU32(cd, crc); /* crc32 */
putU32(cd, contents.length); /* compressed size */
putU32(cd, contents.length); /* uncompressed size */
putU16(cd, name.length); /* file name length */
putU16(cd, 0); /* extra field length */
putU16(cd, 0); /* file comment length */
putU16(cd, 0); /* disk number start */
putU16(cd, 0); /* internal file attributes */
putU32(cd, 32); /* external file attributes - 'archive' bit set (32) */
putU32(cd, offset); /* relative offset of local header */
putBytes(cd, name); /* file name */
offset += 30 + contents.length + name.length
}
putBytes(b, cd); /* central directory */
putU32(b, 0x06054b50); /* end of central directory signature */
putU16(b, 0); /* number of this disk */
putU16(b, 0); /* number of disk with central directory start */
putU16(b, files.length); /* number of entries on disk */
putU16(b, files.length); /* number of entries */
putU32(b, cd.length); /* length of central directory */
putU32(b, offset); /* offset to start of central directory */
putU16(b, 0); /* zip comment size */
return Uint8Array.from(b);
}
window.wireguard = {
generateKeypair: function() {
var privateKey = generatePrivateKey();
var publicKey = generatePublicKey(privateKey);
var presharedKey = generatePresharedKey();
return {
publicKey: keyToBase64(publicKey),
privateKey: keyToBase64(privateKey),
presharedKey: keyToBase64(presharedKey)
};
},
generatePublicKey: function (privateKey){
privateKey = base64ToKey(privateKey);
return keyToBase64(generatePublicKey(privateKey));
},
generateZipFiles: function(res, zipFileName){
var files = res.data;
var zipFile = createZipFile(files);
var blob = new Blob([zipFile], { type: "application/zip" });
var a = document.createElement("a");
a.download = zipFileName;
a.href = URL.createObjectURL(blob);
a.style.display = "none";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
};
})();

View File

@@ -0,0 +1,21 @@
<script>
export default {
name: "configuration",
}
</script>
<template>
<div class="mt-5 text-body">
<RouterView v-slot="{ Component, route }">
<Transition name="fade2" mode="out-in">
<Suspense>
<Component :is="Component" :key="route.path"></Component>
</Suspense>
</Transition>
</RouterView>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,51 @@
<script>
import Navbar from "@/components/navbar.vue";
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurations} from "@/models/WireguardConfigurations.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Message from "@/components/messageCentreComponent/message.vue";
export default {
name: "index",
components: {Message, Navbar},
async setup(){
const dashboardConfigurationStore = DashboardConfigurationStore()
return {dashboardConfigurationStore}
},
computed: {
getMessages(){
return this.dashboardConfigurationStore.Messages.filter(x => x.show)
}
}
}
</script>
<template>
<div class="container-fluid flex-grow-1 main" :data-bs-theme="this.dashboardConfigurationStore.Configuration.Server.dashboard_theme">
<div class="row h-100">
<Navbar></Navbar>
<main class="col-md-9 ml-sm-auto col-lg-10 px-md-4 overflow-y-scroll mb-0" style="height: calc(100vh - 50px)">
<Suspense>
<RouterView v-slot="{Component}">
<Transition name="fade2" mode="out-in">
<Component :is="Component"></Component>
</Transition>
</RouterView>
</Suspense>
<div class="messageCentre text-body position-fixed">
<TransitionGroup name="message" tag="div" class="position-relative">
<Message v-for="m in getMessages.slice().reverse()"
:message="m" :key="m.id"></Message>
</TransitionGroup>
</div>
</main>
</div>
</div>
</template>
<style scoped>
.messageCentre{
top: calc(50px + 1rem);
right: 1rem;
}
</style>

View File

@@ -0,0 +1,296 @@
<script>
import {parse} from "cidr-tools";
import '@/utilities/wireguard.js'
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "newConfiguration",
setup(){
const store = WireguardConfigurationsStore()
return {store}
},
data(){
return {
newConfiguration: {
ConfigurationName: "",
Address: "",
ListenPort: "",
PrivateKey: "",
PublicKey: "",
PresharedKey: "",
PreUp: "",
PreDown: "",
PostUp: "",
PostDown: ""
},
numberOfAvailableIPs: "0",
error: false,
errorMessage: "",
success: false,
loading: false
}
},
created() {
this.wireguardGenerateKeypair();
},
methods: {
wireguardGenerateKeypair(){
const wg = window.wireguard.generateKeypair();
this.newConfiguration.PrivateKey = wg.privateKey;
this.newConfiguration.PublicKey = wg.publicKey;
this.newConfiguration.PresharedKey = wg.presharedKey;
},
async saveNewConfiguration(){
if (this.goodToSubmit){
this.loading = true;
await fetchPost("/api/addWireguardConfiguration", this.newConfiguration, async (res) => {
if (res.status){
this.success = true
await this.store.getConfigurations()
setTimeout(() => {
this.$router.push('/')
}, 1000)
}else{
this.error = true;
this.errorMessage = res.message;
document.querySelector(`#${res.data}`).classList.remove("is-valid")
document.querySelector(`#${res.data}`).classList.add("is-invalid")
}
})
}
}
},
computed: {
goodToSubmit(){
let requirements = ["ConfigurationName", "Address", "ListenPort", "PrivateKey"]
let elements = [...document.querySelectorAll("input[required]")];
return requirements.find(x => {
return this.newConfiguration[x].length === 0
}) === undefined && elements.find(x => {
return x.classList.contains("is-invalid")
}) === undefined
}
},
watch: {
'newConfiguration.Address'(newVal){
let ele = document.querySelector("#Address");
ele.classList.remove("is-invalid", "is-valid")
try{
if (newVal.trim().split("/").filter(x => x.length > 0).length !== 2){
throw Error()
}
let p = parse(newVal);
let i = p.end - p.start;
this.numberOfAvailableIPs = i.toLocaleString();
ele.classList.add("is-valid")
}catch (e) {
this.numberOfAvailableIPs = "0";
ele.classList.add("is-invalid")
}
},
'newConfiguration.ListenPort'(newVal){
let ele = document.querySelector("#ListenPort");
ele.classList.remove("is-invalid", "is-valid")
if (newVal < 0 || newVal > 65353 || !Number.isInteger(newVal)){
ele.classList.add("is-invalid")
}else{
ele.classList.add("is-valid")
}
},
'newConfiguration.ConfigurationName'(newVal){
let ele = document.querySelector("#ConfigurationName");
ele.classList.remove("is-invalid", "is-valid")
if (!/^[a-zA-Z0-9_=+.-]{1,15}$/.test(newVal) || newVal.length === 0 || this.store.Configurations.find(x => x.Name === newVal)){
ele.classList.add("is-invalid")
}else{
ele.classList.add("is-valid")
}
},
'newConfiguration.PrivateKey'(newVal){
let ele = document.querySelector("#PrivateKey");
ele.classList.remove("is-invalid", "is-valid")
try{
wireguard.generatePublicKey(newVal)
ele.classList.add("is-valid")
}catch (e) {
ele.classList.add("is-invalid")
}
}
}
}
</script>
<template>
<div class="mt-4">
<div class="container mb-4">
<div class="mb-4 d-flex align-items-center gap-4">
<RouterLink to="/">
<h3 class="mb-0 text-body">
<i class="bi bi-chevron-left"></i>
</h3>
</RouterLink>
<h3 class="text-body mb-0">New Configuration</h3>
</div>
<form class="text-body d-flex flex-column gap-3"
@submit="(e) => {e.preventDefault(); this.saveNewConfiguration();}"
>
<div class="card rounded-3 shadow">
<div class="card-header">Configuration Name</div>
<div class="card-body">
<input type="text" class="form-control" placeholder="ex. wg1" id="ConfigurationName"
v-model="this.newConfiguration.ConfigurationName"
:disabled="this.loading"
required>
<div class="invalid-feedback">
<div v-if="this.error">{{this.errorMessage}}</div>
<div v-else>
Configuration name is invalid. Possible reasons:
<ul class="mb-0">
<li>Configuration name already exist.</li>
<li>Configuration name can only contain 15 lower/uppercase alphabet, numbers, "_"(underscore), "="(equal), "+"(plus), "."(period/dot), "-"(dash/hyphen)</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card rounded-3 shadow">
<div class="card-header">Private Key / Public Key / Pre-Shared Key</div>
<div class="card-body" style="font-family: var(--bs-font-monospace)">
<div class="mb-2">
<label class="text-muted fw-bold mb-1"><small>PRIVATE KEY</small></label>
<div class="input-group">
<input type="text" class="form-control" id="PrivateKey" required
:disabled="this.loading"
v-model="this.newConfiguration.PrivateKey" disabled
>
<button class="btn btn-outline-primary" type="button"
title="Regenerate Private Key"
@click="wireguardGenerateKeypair()"
>
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div>
<label class="text-muted fw-bold mb-1"><small>PUBLIC KEY</small></label>
<input type="text" class="form-control" id="PublicKey"
v-model="this.newConfiguration.PublicKey" disabled
>
</div>
</div>
</div>
<div class="card rounded-3 shadow">
<div class="card-header">Listen Port</div>
<div class="card-body">
<input type="number" class="form-control" placeholder="0-65353" id="ListenPort"
min="1"
max="65353"
v-model="this.newConfiguration.ListenPort"
:disabled="this.loading"
required>
<div class="invalid-feedback">
<div v-if="this.error">{{this.errorMessage}}</div>
<div v-else>
Invalid port
</div>
</div>
</div>
</div>
<div class="card rounded-3 shadow">
<div class="card-header d-flex align-items-center">
IP Address & Range
<span class="badge rounded-pill text-bg-success ms-auto">{{ numberOfAvailableIPs }} Available IPs</span>
</div>
<div class="card-body">
<input type="text" class="form-control"
placeholder="Ex: 10.0.0.1/24" id="Address"
v-model="this.newConfiguration.Address"
:disabled="this.loading"
required>
<div class="invalid-feedback">
<div v-if="this.error">{{this.errorMessage}}</div>
<div v-else>
IP address & range is invalid.
</div>
</div>
</div>
</div>
<hr>
<div class="accordion" id="newConfigurationOptionalAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#newConfigurationOptionalAccordionCollapse">
Optional Settings
</button>
</h2>
<div id="newConfigurationOptionalAccordionCollapse"
class="accordion-collapse collapse" data-bs-parent="#newConfigurationOptionalAccordion">
<div class="accordion-body d-flex flex-column gap-3">
<div class="card rounded-3">
<div class="card-header">PreUp</div>
<div class="card-body">
<input type="text" class="form-control" id="preUp" v-model="this.newConfiguration.PreUp">
</div>
</div>
<div class="card rounded-3">
<div class="card-header">PreDown</div>
<div class="card-body">
<input type="text" class="form-control" id="preDown" v-model="this.newConfiguration.PreDown">
</div>
</div>
<div class="card rounded-3">
<div class="card-header">PostUp</div>
<div class="card-body">
<input type="text" class="form-control" id="postUp" v-model="this.newConfiguration.PostUp">
</div>
</div>
<div class="card rounded-3">
<div class="card-header">PostDown</div>
<div class="card-body">
<input type="text" class="form-control" id="postDown" v-model="this.newConfiguration.PostDown">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- <RouterLink to="/new_configuration" class="btn btn-success rounded-3 shadow ms-auto rounded-3">-->
<!-- <i class="bi bi-save me-2"></i>-->
<!-- Save-->
<!-- </RouterLink>-->
<button class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto"
:disabled="!this.goodToSubmit">
<span v-if="this.success" class="d-flex w-100">
Success! <i class="bi bi-check-circle-fill ms-2"></i>
</span>
<span v-else-if="!this.loading" class="d-flex w-100">
Save Configuration <i class="bi bi-save-fill ms-2"></i>
</span>
<span v-else class="d-flex w-100 align-items-center">
Saving...
<span class="ms-2 spinner-border spinner-border-sm" role="status">
<!-- <span class="visually-hidden">Loading...</span>-->
</span>
</span>
</button>
</form>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,197 @@
<script>
import {fetchGet} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "ping",
data(){
return {
loading: false,
cips: {},
selectedConfiguration: undefined,
selectedPeer: undefined,
selectedIp: undefined,
count: 4,
pingResult: undefined,
pinging: false
}
},
setup(){
const store = DashboardConfigurationStore();
return {store}
},
mounted() {
fetchGet("/api/ping/getAllPeersIpAddress", {}, (res)=> {
if (res.status){
this.loading = true;
this.cips = res.data;
console.log(this.cips)
}
});
},
methods: {
execute(){
if (this.selectedIp){
this.pinging = true;
this.pingResult = undefined
fetchGet("/api/ping/execute", {
ipAddress: this.selectedIp,
count: this.count
}, (res) => {
if (res.status){
this.pingResult = res.data
}else{
this.store.newMessage("Server", res.message, "danger")
}
})
}
{} }
},
watch: {
selectedConfiguration(){
this.selectedPeer = undefined;
this.selectedIp = undefined;
},
selectedPeer(){
this.selectedIp = undefined;
}
}
}
</script>
<template>
<div class="mt-5 text-body">
<div class="container">
<h3 class="mb-3 text-body">Ping</h3>
<div class="row">
<div class="col-sm-4 d-flex gap-2 flex-column">
<div>
<label class="mb-1 text-muted" for="configuration">
<small>Configuration</small></label>
<select class="form-select" v-model="this.selectedConfiguration">
<option disabled selected :value="undefined">Select a Configuration...</option>
<option :value="key" v-for="(val, key) in this.cips">
{{key}}
</option>
</select>
</div>
<div>
<label class="mb-1 text-muted" for="peer">
<small>Peer</small></label>
<select id="peer" class="form-select" v-model="this.selectedPeer" :disabled="this.selectedConfiguration === undefined">
<option disabled selected :value="undefined">Select a Peer...</option>
<option v-if="this.selectedConfiguration !== undefined" :value="key" v-for="(peer, key) in
this.cips[this.selectedConfiguration]">
{{key}}
</option>
</select>
</div>
<div>
<label class="mb-1 text-muted" for="ip">
<small>IP Address</small></label>
<select id="ip" class="form-select" v-model="this.selectedIp" :disabled="this.selectedPeer === undefined">
<option disabled selected :value="undefined">Select a IP...</option>
<option
v-if="this.selectedPeer !== undefined"
v-for="ip in this.cips[this.selectedConfiguration][this.selectedPeer].allowed_ips">
{{ip}}
</option>
</select>
</div>
<div>
<label class="mb-1 text-muted" for="count">
<small>Ping Count</small></label>
<input class="form-control" type="number"
v-model="this.count"
min="1" id="count" placeholder="How many times you want to ping?">
</div>
<button class="btn btn-primary rounded-3 mt-3"
:disabled="!this.selectedIp"
@click="this.execute()">
<i class="bi bi-person-walking me-2"></i>Go!
</button>
</div>
<div class="col-sm-8">
<TransitionGroup name="ping">
<div v-if="!this.pingResult" key="pingPlaceholder">
<div class="pingPlaceholder bg-body-secondary rounded-3 mb-3"
:class="{'animate__animated animate__flash animate__slower animate__infinite': this.pinging}"
:style="{'animation-delay': `${x*0.15}s`}"
v-for="x in 4" ></div>
</div>
<div v-else key="pingResult" class="d-flex flex-column gap-2 w-100">
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.15s">
<div class="card-body">
<p class="mb-0 text-muted"><small>Address</small></p>
{{this.pingResult.address}}
</div>
</div>
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.3s">
<div class="card-body">
<p class="mb-0 text-muted"><small>Is Alive</small></p>
<span :class="[this.pingResult.is_alive ? 'text-success':'text-danger']">
<i class="bi me-1"
:class="[this.pingResult.is_alive ? 'bi-check-circle-fill' : 'bi-x-circle-fill']"></i>
{{this.pingResult.is_alive ? "Yes": "No"}}
</span>
</div>
</div>
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.45s">
<div class="card-body">
<p class="mb-0 text-muted"><small>Average / Min / Max Round Trip Time</small></p>
<samp>{{this.pingResult.avg_rtt}}ms /
{{this.pingResult.min_rtt}}ms /
{{this.pingResult.max_rtt}}ms
</samp>
</div>
</div>
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.6s">
<div class="card-body">
<p class="mb-0 text-muted"><small>Sent / Received / Lost Package</small></p>
<samp>{{this.pingResult.package_sent}} /
{{this.pingResult.package_received}} /
{{this.pingResult.package_loss}}
</samp>
</div>
</div>
</div>
</TransitionGroup>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.pingPlaceholder{
width: 100%;
height: 79.98px;
}
.ping-move, /* apply transition to moving elements */
.ping-enter-active,
.ping-leave-active {
transition: all 0.4s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.ping-leave-active{
position: absolute;
}
.ping-enter-from,
.ping-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. */
.ping-leave-active {
position: absolute;
}
</style>

View File

@@ -0,0 +1,91 @@
<script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import PeersDefaultSettingsInput from "@/components/settingsComponent/peersDefaultSettingsInput.vue";
import {ipV46RegexCheck} from "@/utilities/ipCheck.js";
import AccountSettingsInputUsername from "@/components/settingsComponent/accountSettingsInputUsername.vue";
import AccountSettingsInputPassword from "@/components/settingsComponent/accountSettingsInputPassword.vue";
import DashboardSettingsInputWireguardConfigurationPath
from "@/components/settingsComponent/dashboardSettingsInputWireguardConfigurationPath.vue";
import DashboardTheme from "@/components/settingsComponent/dashboardTheme.vue";
import DashboardSettingsInputIPAddressAndPort
from "@/components/settingsComponent/dashboardSettingsInputIPAddressAndPort.vue";
import DashboardAPIKeys from "@/components/settingsComponent/dashboardAPIKeys.vue";
import AccountSettingsMFA from "@/components/settingsComponent/accountSettingsMFA.vue";
export default {
name: "settings",
methods: {ipV46RegexCheck},
components: {
AccountSettingsMFA,
DashboardAPIKeys,
DashboardSettingsInputIPAddressAndPort,
DashboardTheme,
DashboardSettingsInputWireguardConfigurationPath,
AccountSettingsInputPassword, AccountSettingsInputUsername, PeersDefaultSettingsInput},
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore()
return {dashboardConfigurationStore}
},
watch: {
// 'dashboardConfigurationStore.Configuration': {
// deep: true,
// handler(){
// this.dashboardConfigurationStore.updateConfiguration();
// }
// }
}
}
</script>
<template>
<div class="mt-5">
<div class="container">
<h3 class="mb-3 text-body">Settings</h3>
<DashboardTheme></DashboardTheme>
<div class="card mb-4 shadow rounded-3">
<p class="card-header">Peers Default Settings</p>
<div class="card-body">
<PeersDefaultSettingsInput targetData="peer_global_dns" title="DNS"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput targetData="peer_endpoint_allowed_ip" title="Peer Endpoint Allowed IPs"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput targetData="peer_mtu" title="MTU (Max Transmission Unit)"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput targetData="remote_endpoint" title="Peer Remote Endpoint"
:warning="true" warningText="This will be changed globally, and will be apply to all peer's QR code and configuration file."
></PeersDefaultSettingsInput>
</div>
</div>
<div class="card mb-4 shadow rounded-3">
<p class="card-header">WireGuard Configurations Settings</p>
<div class="card-body">
<DashboardSettingsInputWireguardConfigurationPath
targetData="wg_conf_path"
title="Configurations Directory"
:warning="true"
warning-text="Remember to remove <code>/</code> at the end of your path. e.g <code>/etc/wireguard</code>"
>
</DashboardSettingsInputWireguardConfigurationPath>
</div>
</div>
<div class="card mb-4 shadow rounded-3">
<p class="card-header">Account Settings</p>
<div class="card-body d-flex gap-4 flex-column">
<AccountSettingsInputUsername targetData="username"
title="Username"
></AccountSettingsInputUsername>
<hr class="m-0">
<AccountSettingsInputPassword
targetData="password">
</AccountSettingsInputPassword>
<hr class="m-0">
<AccountSettingsMFA></AccountSettingsMFA>
</div>
</div>
<DashboardAPIKeys></DashboardAPIKeys>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,121 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "setup",
components: {},
setup(){
const store = DashboardConfigurationStore();
return {store}
},
data(){
return {
setup: {
username: "",
newPassword: "",
repeatNewPassword: "",
enable_totp: true
},
loading: false,
errorMessage: "",
done: false
}
},
computed: {
goodToSubmit(){
return this.setup.username
&& this.setup.newPassword.length >= 8
&& this.setup.repeatNewPassword.length >= 8
&& this.setup.newPassword === this.setup.repeatNewPassword
}
},
methods: {
submit(){
this.loading = true
fetchPost("/api/Welcome_Finish", this.setup, (res) => {
if (res.status){
this.done = true;
this.$router.push('/2FASetup')
}else{
document.querySelectorAll("#createAccount input").forEach(x => x.classList.add("is-invalid"))
this.errorMessage = res.message;
document.querySelector(".login-container-fluid")
.scrollTo({
top: 0,
left: 0,
behavior: "smooth",
})
}
this.loading = false
})
}
}
}
</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">
<span class="dashboardLogo display-4">Nice to meet you!</span>
<p class="mb-5">Please fill in the following fields to finish setup 😊</p>
<div>
<h3>Create an account</h3>
<div class="alert alert-danger" v-if="this.errorMessage">
{{this.errorMessage}}
</div>
<div class="d-flex flex-column gap-3">
<div id="createAccount" class="d-flex flex-column gap-2">
<div class="form-group text-body">
<label for="username" class="mb-1 text-muted">
<small>Pick an username you like</small></label>
<input type="text"
v-model="this.setup.username"
class="form-control" id="username" name="username" placeholder="Maybe something like 'wiredragon'?" required>
</div>
<div class="form-group text-body">
<label for="password" class="mb-1 text-muted">
<small>Create a password (at least 8 characters)</small></label>
<input type="password"
v-model="this.setup.newPassword"
class="form-control" id="password" name="password" placeholder="Make sure is strong enough" required>
</div>
<div class="form-group text-body">
<label for="confirmPassword" class="mb-1 text-muted">
<small>Confirm password</small></label>
<input type="password"
v-model="this.setup.repeatNewPassword"
class="form-control" id="confirmPassword" name="confirmPassword" placeholder="and you can remember it :)" required>
</div>
</div>
<!-- <div class="form-check form-switch">-->
<!-- <input class="form-check-input" type="checkbox" role="switch" id="enable_totp" -->
<!-- v-model="this.setup.enable_totp">-->
<!-- <label class="form-check-label" -->
<!-- for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label>-->
<!-- </div>-->
<!-- <Suspense>-->
<!-- <Transition name="fade">-->
<!-- <Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp>-->
<!-- </Transition>-->
<!-- </Suspense>-->
<button class="btn btn-dark btn-lg mb-5 d-flex btn-brand shadow align-items-center"
ref="signInBtn"
:disabled="!this.goodToSubmit || this.loading || this.done" @click="this.submit()">
<span class="d-flex align-items-center w-100" v-if="!this.loading && !this.done">
Next<i class="bi bi-chevron-right ms-auto"></i></span>
<span class="d-flex align-items-center w-100" v-else>
Saving...<span class="spinner-border ms-auto spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</span></span>
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,109 @@
<script>
import {useRoute} from "vue-router";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchGet} from "@/utilities/fetch.js";
import {ref} from "vue";
import QRCode from "qrcode";
export default {
name: "share",
async setup(){
const route = useRoute();
const loaded = ref(false)
const store = DashboardConfigurationStore();
const theme = ref("");
const peerConfiguration = ref("");
const blob = ref(new Blob())
await fetchGet("/api/getDashboardTheme", {}, (res) => {
theme.value = res.data
});
const id = route.query.ShareID
if(id === undefined || id.length === 0){
peerConfiguration.value = undefined
loaded.value = true;
}else{
await fetchGet("/api/sharePeer/get", {
ShareID: id
}, (res) => {
if (res.status){
peerConfiguration.value = res.data;
blob.value = new Blob([peerConfiguration.value.file], { type: "text/plain" });
}else{
peerConfiguration.value = undefined
}
loaded.value = true;
})
}
return {store, theme, peerConfiguration, blob}
},
mounted() {
QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfiguration.file , (error) => {
if (error) console.error(error)
})
},
methods:{
download(){
const blob = new Blob([this.peerConfiguration.file], { type: "text/plain" });
const jsonObjectUrl = URL.createObjectURL(blob);
const filename = `${this.peerConfiguration.fileName}.conf`;
const anchorEl = document.createElement("a");
anchorEl.href = jsonObjectUrl;
anchorEl.download = filename;
anchorEl.click();
}
},
computed:{
getBlob(){
return URL.createObjectURL(this.blob)
}
}
}
</script>
<template>
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
:data-bs-theme="this.theme">
<div class="m-auto text-body" style="width: 500px">
<div class="text-center position-relative" style=""
v-if="!this.peerConfiguration">
<div class="animate__animated animate__fadeInUp">
<h1 style="font-size: 20rem; filter: blur(1rem); animation-duration: 7s"
class="animate__animated animate__flash animate__infinite">
<i class="bi bi-file-binary"></i>
</h1>
</div>
<div class="position-absolute w-100 h-100 top-0 start-0 d-flex animate__animated animate__fadeInUp"
style="animation-delay: 0.1s;"
>
<h3 class="m-auto">Oh no... This link is either expired or invalid.</h3>
</div>
</div>
<div v-else class="d-flex align-items-center flex-column gap-3">
<div class="h1 dashboardLogo text-center animate__animated animate__fadeInUp">
<h6>WGDashboard</h6>
Scan QR Code from the WireGuard App
</div>
<canvas id="qrcode" class="rounded-3 shadow animate__animated animate__fadeInUp mb-3" ref="qrcode"></canvas>
<p class="text-muted animate__animated animate__fadeInUp mb-1"
style="animation-delay: 0.2s;"
>or click the button below to download the <samp>.conf</samp> file</p>
<a
:download="this.peerConfiguration.fileName + '.conf'"
:href="getBlob"
class="btn btn-lg bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle animate__animated animate__fadeInUp shadow-sm"
style="animation-delay: 0.25s;"
>
<i class="bi bi-download"></i>
</a>
</div>
</div>
</div>
</template>
<style scoped>
.animate__fadeInUp{
animation-timing-function: cubic-bezier(0.42, 0, 0.22, 1.0)
}
</style>

View File

@@ -0,0 +1,150 @@
<script>
import {fetchGet, fetchPost} from "../utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Message from "@/components/messageCentreComponent/message.vue";
export default {
name: "signin",
components: {Message},
async setup(){
const store = DashboardConfigurationStore()
let theme = ""
let totpEnabled = false;
await fetchGet("/api/getDashboardTheme", {}, (res) => {
theme = res.data
});
await fetchGet("/api/isTotpEnabled", {}, (res) => {
totpEnabled = res.data
});
return {store, theme, totpEnabled}
},
data(){
return {
username: "",
password: "",
totp: "",
loginError: false,
loginErrorMessage: "",
loading: false
}
},
computed: {
getMessages(){
return this.store.Messages.filter(x => x.show)
}
},
methods: {
async auth(){
if (this.username && this.password && ((this.totpEnabled && this.totp) || !this.totpEnabled)){
this.loading = true
await fetchPost("/api/authenticate", {
username: this.username,
password: this.password,
totp: this.totp
}, (response) => {
if (response.status){
this.loginError = false;
this.$refs["signInBtn"].classList.add("signedIn")
if (response.message){
this.$router.push('/welcome')
}else{
if (this.store.Redirect !== undefined){
this.$router.push(this.store.Redirect)
}else{
this.$router.push('/')
}
}
}else{
this.loginError = true;
this.loginErrorMessage = response.message;
document.querySelectorAll("input[required]").forEach(x => {
x.classList.remove("is-valid")
x.classList.add("is-invalid")
});
this.loading = false
}
})
}else{
document.querySelectorAll("input[required]").forEach(x => {
if (x.value.length === 0){
x.classList.remove("is-valid")
x.classList.add("is-invalid")
}else{
x.classList.remove("is-invalid")
x.classList.add("is-valid")
}
});
}
}
}
}
</script>
<template>
<div class="container-fluid login-container-fluid d-flex main flex-column" :data-bs-theme="this.theme">
<div class="login-box m-auto" style="width: 600px;">
<div class="m-auto">
<div class="card px-4 py-5 rounded-4 shadow-lg">
<div class="card-body">
<h4 class="mb-0 text-body">Welcome to</h4>
<span class="dashboardLogo display-3"><strong>WGDashboard</strong></span>
<div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError">
{{this.loginErrorMessage}}
</div>
<form @submit="(e) => {e.preventDefault(); this.auth();}">
<div class="form-group text-body">
<label for="username" class="text-left" style="font-size: 1rem">
<i class="bi bi-person-circle"></i></label>
<input type="text" v-model="username" class="form-control" id="username" name="username"
autocomplete="on"
placeholder="Username" required>
</div>
<div class="form-group text-body">
<label for="password" class="text-left" style="font-size: 1rem"><i class="bi bi-key-fill"></i></label>
<input type="password"
v-model="password" class="form-control" id="password" name="password"
autocomplete="on"
placeholder="Password" required>
</div>
<div class="form-group text-body" v-if="totpEnabled">
<label for="totp" class="text-left" style="font-size: 1rem"><i class="bi bi-lock-fill"></i></label>
<input class="form-control totp"
required
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
placeholder="OTP from your authenticator"
v-model="this.totp"
>
</div>
<button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand signInBtn" ref="signInBtn">
<span v-if="!this.loading" class="d-flex w-100">
Sign In<i class="ms-auto bi bi-chevron-right"></i>
</span>
<span v-else class="d-flex w-100 align-items-center">
Signing In...
<span class="spinner-border ms-auto spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</span>
</span>
</button>
</form>
</div>
</div>
</div>
</div>
<small class="text-muted pb-3 d-block w-100 text-center">
WGDashboard v4.0 | Developed with by
<a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
</small>
<div class="messageCentre text-body position-absolute end-0 m-3">
<TransitionGroup name="message" tag="div" class="position-relative">
<Message v-for="m in getMessages.slice().reverse()"
:message="m" :key="m.id"></Message>
</TransitionGroup>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,136 @@
<script>
import {fetchGet} from "@/utilities/fetch.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
export default {
name: "traceroute",
data(){
return {
tracing: false,
ipAddress: undefined,
tracerouteResult: undefined
}
},
setup(){
const store = WireguardConfigurationsStore();
return {store}
},
methods: {
execute(){
if (this.ipAddress){
this.tracing = true;
this.tracerouteResult = undefined
fetchGet("/api/traceroute/execute", {
ipAddress: this.ipAddress,
}, (res) => {
if (res.status){
this.tracerouteResult = res.data
}else{
this.store.newMessage("Server", res.message, "danger")
}
this.tracing = false
})
}
}
},
}
</script>
<template>
<div class="mt-5 text-body">
<div class="container">
<h3 class="mb-3 text-body">Traceroute</h3>
<div class="row">
<div class="col-sm-4 d-flex gap-2 flex-column">
<div>
<label class="mb-1 text-muted" for="ipAddress">
<small>IP Address</small></label>
<input
id="ipAddress"
class="form-control"
v-model="this.ipAddress"
type="text" placeholder="Enter an IP Address you want to trace :)">
</div>
<button class="btn btn-primary rounded-3 mt-3"
:disabled="!this.store.regexCheckIP(this.ipAddress) || this.tracing"
@click="this.execute()">
<i class="bi bi-bullseye me-2"></i> {{this.tracing ? "Tracing...":"Trace It!"}}
</button>
</div>
<div class="col-sm-8 position-relative">
<TransitionGroup name="ping">
<div v-if="!this.tracerouteResult" key="pingPlaceholder">
<div class="pingPlaceholder bg-body-secondary rounded-3 mb-3"
:class="{'animate__animated animate__flash animate__slower animate__infinite': this.tracing}"
:style="{'animation-delay': `${x*0.05}s`}"
v-for="x in 10" ></div>
</div>
<div v-else key="table" class="w-100">
<table class="table table-borderless rounded-3 w-100">
<thead>
<tr>
<th scope="col">Hop</th>
<th scope="col">IP Address</th>
<th scope="col">Average / Min / Max Round Trip Time</th>
</tr>
</thead>
<tbody>
<tr v-for="(hop, key) in this.tracerouteResult"
class="animate__fadeInUp animate__animated"
:style="{'animation-delay': `${key * 0.05}s`}"
>
<td>{{hop.hop}}</td>
<td>{{hop.ip}}</td>
<td>{{hop.avg_rtt}} / {{hop.min_rtt}} / {{hop.max_rtt}}</td>
</tr>
</tbody>
</table>
</div>
</TransitionGroup>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.pingPlaceholder{
width: 100%;
height: 40px;
}
.ping-move, /* apply transition to moving elements */
.ping-enter-active,
.ping-leave-active {
transition: all 0.4s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.ping-leave-active{
position: absolute;
}
.ping-enter-from,
.ping-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. */
.ping-leave-active {
position: absolute;
}
table th, table td{
padding: 0.9rem;
}
table tbody{
border-top: 1em solid transparent;
}
.table > :not(caption) > * > *{
background-color: transparent !important;
}
</style>

View File

@@ -0,0 +1,32 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import {proxy} from "./proxy.js";
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
base: "/static/app/dist",
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server:{
proxy: {
'/api': proxy
}
},
build: {
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
}
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,17 @@
body { /**{*/
font-size: .875rem; /* font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";*/
/*}*/
.dp__input{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
}
::-webkit-scrollbar {
display: none;
}
.codeFont{
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
} }
.feather { .feather {
@@ -8,51 +20,133 @@ body {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
.btn-primary {
font-weight: bold;
}
/* /*
* Sidebar * Sidebar
*/ */
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
/*@media (max-width: 767.98px) {*/
/*.sidebar {*/ /*.sidebar {*/
/* top: 5rem;*/ /* position: fixed;*/
/* top: 0;*/
/* bottom: 0;*/
/* left: 0;*/
/* z-index: 100;*/
/* !* Behind the navbar *!*/
/* padding: 48px 0 0;*/
/* !* Height of navbar *!*/
/* box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);*/
/*}*/
/*.sidebar-sticky {*/
/* position: relative;*/
/* top: 0;*/
/* height: calc(100vh - 48px);*/
/* padding-top: .5rem;*/
/* overflow-x: hidden;*/
/* overflow-y: auto;*/
/* !* Scrollable contents if viewport is shorter than content. *!*/
/*}*/
/*@supports ((position: -webkit-sticky) or (position: sticky)) {*/
/* .sidebar-sticky {*/
/* position: -webkit-sticky;*/
/* position: sticky;*/
/* }*/ /* }*/
/*}*/ /*}*/
.sidebar-sticky { @property --brandColor1 {
position: relative; syntax: '<color>';
top: 0; initial-value: #009dff;
height: calc(100vh - 48px); inherits: false;
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
} }
@supports ((position: -webkit-sticky) or (position: sticky)) { @property --brandColor2 {
.sidebar-sticky { syntax: '<color>';
position: -webkit-sticky; initial-value: #ff4a00;
position: sticky; inherits: false;
}
@property --distance2{
syntax: '<percentage>';
initial-value: 0%;
inherits: false;
}
@property --degree{
syntax: '<angle>';
initial-value: 234deg;
inherits: false;
}
.dashboardLogo{
background: rgb(23,139,255);
background: linear-gradient(234deg, var(--brandColor1) var(--distance2), var(--brandColor2) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
transition: --brandColor1 1s, --brandColor2 0.3s, --distance2 1s !important;
}
.btn-brand{
/*background: rgb(23,139,255);*/
background: linear-gradient(var(--degree), var(--brandColor1) var(--distance2), var(--brandColor2) 100%);
border: 0 !important;
transition: --brandColor1 1s, --brandColor2 1s, --distance2 0.5s !important;
}
.btn-brand.loading{
animation: spin infinite forwards 3s linear;
}
.btn-brand:hover, .dashboardLogo:hover{
--brandColor1: #009dff;
--brandColor2: #ff875b;
--distance2: 30%;
}
.signInBtn.signedIn{
--distance2: 100%;
}
@keyframes spin {
0%{
--degree: 234deg;
}
100%{
--degree: 594deg;
} }
} }
.sidebar .nav-link { [data-bs-theme="dark"].main,
#app:has(.main[data-bs-theme="dark"]){
background-color: #1b1e21;
}
.sidebar .nav-link, .bottomNavContainer .nav-link{
font-weight: 500; font-weight: 500;
color: #333; color: #333;
transition: 0.2s cubic-bezier(0.82, -0.07, 0, 1.01); transition: 0.2s cubic-bezier(0.82, -0.07, 0, 1.01);
} }
[data-bs-theme="dark"] .sidebar .nav-link{
color: white;
}
[data-bs-theme="dark"] .sidebar .nav-link.active{
color: #74b7ff;
}
[data-bs-theme="dark"] .nav-link:hover{
background-color: #323844;
}
.nav-link:hover { .nav-link:hover {
padding-left: 30px; background-color: #e8e8e8;
background-color: #dfdfdf;
} }
.sidebar .nav-link .feather { .sidebar .nav-link .feather {
@@ -60,8 +154,9 @@ body {
color: #999; color: #999;
} }
.sidebar .nav-link.active { .sidebar .nav-link.active, .bottomNavContainer .nav-link.active {
color: #007bff; color: #007bff;
} }
.sidebar .nav-link:hover .feather, .sidebar .nav-link:hover .feather,
@@ -74,6 +169,7 @@ body {
text-transform: uppercase; text-transform: uppercase;
} }
/* /*
* Navbar * Navbar
*/ */
@@ -82,8 +178,8 @@ body {
padding-top: .75rem; padding-top: .75rem;
padding-bottom: .75rem; padding-bottom: .75rem;
font-size: 1rem; font-size: 1rem;
background-color: rgba(0, 0, 0, .25); /*background-color: rgba(0, 0, 0, .25);*/
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); /*box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);*/
} }
.navbar .navbar-toggler { .navbar .navbar-toggler {
@@ -121,10 +217,11 @@ body {
height: 10px; height: 10px;
border-radius: 50px; border-radius: 50px;
display: inline-block; display: inline-block;
margin-left: 10px; margin-left: auto !important;
background-color: #6c757d;
} }
.dot-running{ .dot.active{
background-color: #28a745!important; background-color: #28a745!important;
box-shadow: 0 0 0 0.2rem #28a74545; box-shadow: 0 0 0 0.2rem #28a74545;
} }
@@ -133,17 +230,14 @@ body {
margin-left: 0.3rem; margin-left: 0.3rem;
} }
.dot-stopped{
background-color: #6c757d!important;
}
.card-running { .card-running {
border-color: #28a745; border-color: #28a745;
} }
.info h6 { .info h6 {
line-break: anywhere; line-break: anywhere;
transition: 0.2s ease-in-out; transition: all 0.4s cubic-bezier(0.96, -0.07, 0.34, 1.01);
opacity: 1;
} }
.info .row .col-sm { .info .row .col-sm {
@@ -153,7 +247,6 @@ body {
.info .row .col-sm small { .info .row .col-sm small {
display: flex; display: flex;
} }
.info .row .col-sm small strong:last-child(1) { .info .row .col-sm small strong:last-child(1) {
@@ -162,10 +255,16 @@ body {
.btn-control { .btn-control {
border: none !important; border: none !important;
padding: 0 1rem 0 0; padding: 0;
margin: 0 1rem 0 0;
} }
.btn-control:active, .btn-control:focus{ .btn-control:hover{
background-color: transparent !important;
}
.btn-control:active,
.btn-control:focus {
background-color: transparent !important; background-color: transparent !important;
border: none !important; border: none !important;
box-shadow: none; box-shadow: none;
@@ -175,12 +274,14 @@ body {
padding: 0 !important; padding: 0 !important;
} }
.btn-qrcode-peer:active, .btn-qrcode-peer:hover{ .btn-qrcode-peer:active,
.btn-qrcode-peer:hover {
transform: scale(0.9) rotate(180deg); transform: scale(0.9) rotate(180deg);
border: 0 !important; border: 0 !important;
} }
.btn-download-peer:active, .btn-download-peer:hover{ .btn-download-peer:active,
.btn-download-peer:hover {
color: #17a2b8 !important; color: #17a2b8 !important;
transform: translateY(5px); transform: translateY(5px);
} }
@@ -199,10 +300,26 @@ body {
color: #dc3545; color: #dc3545;
} }
.btn-setting-peer:hover{ .btn-lock-peer:hover {
color: #28a745;
}
.btn-lock-peer.lock{
color: #6c757d
}
.btn-lock-peer.lock:hover{
color: #6c757d
}
.btn-control.btn-outline-primary:hover{
color: #007bff color: #007bff
} }
/* .btn-setting-peer:hover {
color: #007bff
} */
.btn-download-peer:hover { .btn-download-peer:hover {
color: #17a2b8; color: #17a2b8;
} }
@@ -220,6 +337,7 @@ body {
.switch { .switch {
font-size: 2rem; font-size: 2rem;
} }
.switch:hover { .switch:hover {
text-decoration: none text-decoration: none
} }
@@ -230,9 +348,17 @@ body {
background: white; background: white;
} }
/*.peer_data_group{*/ .peer_data_group {
/* text-align: right;*/ text-align: right;
/*}*/ display: flex;
margin-bottom: 0.5rem
}
.peer_data_group p {
text-transform: uppercase;
margin-bottom: 0;
margin-right: 1rem
}
@media (max-width: 768px) { @media (max-width: 768px) {
.peer_data_group { .peer_data_group {
@@ -241,7 +367,9 @@ body {
} }
.index-switch { .index-switch {
text-align: right; display: flex;
align-items: center;
justify-content: flex-end;
} }
main { main {
@@ -256,7 +384,6 @@ main{
.add_btn { .add_btn {
bottom: 1.5rem !important; bottom: 1.5rem !important;
} }
.peer_list { .peer_list {
margin-bottom: 7rem !important; margin-bottom: 7rem !important;
} }
@@ -278,7 +405,7 @@ main{
right: 0; right: 0;
box-shadow: 0 10px 20px rgb(0 0 0 / 19%), 0 6px 6px rgb(0 0 0 / 23%); box-shadow: 0 10px 20px rgb(0 0 0 / 19%), 0 6px 6px rgb(0 0 0 / 23%);
border-radius: 10px; border-radius: 10px;
min-width: 200px; min-width: 250px;
display: none; display: none;
transform: translateY(-30px); transform: translateY(-30px);
opacity: 0; opacity: 0;
@@ -331,8 +458,10 @@ main{
font-size: 1.5rem; font-size: 1.5rem;
} }
@-webkit-keyframes rotating
/* Safari and Chrome */
@-webkit-keyframes rotating /* Safari and Chrome */ { {
from { from {
-webkit-transform: rotate(0deg); -webkit-transform: rotate(0deg);
-o-transform: rotate(0deg); -o-transform: rotate(0deg);
@@ -344,6 +473,7 @@ main{
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes rotating { @keyframes rotating {
from { from {
-ms-transform: rotate(0deg); -ms-transform: rotate(0deg);
@@ -360,6 +490,7 @@ main{
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.rotating::before { .rotating::before {
-webkit-animation: rotating 0.75s linear infinite; -webkit-animation: rotating 0.75s linear infinite;
-moz-animation: rotating 0.75s linear infinite; -moz-animation: rotating 0.75s linear infinite;
@@ -376,7 +507,10 @@ main{
cursor: pointer; cursor: pointer;
} }
#peer_private_key_textbox, #private_key, #public_key{ #peer_private_key_textbox,
#private_key,
#public_key,
#peer_preshared_key_textbox {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
} }
@@ -407,8 +541,38 @@ main{
.btn { .btn {
border-radius: 8px; border-radius: 8px;
/*padding: 0.6rem 0.9em;*/
} }
.login-box #username,
.login-box #password,
.login-box #totp
{
padding: 0.6rem calc( 0.9rem + 32px);
height: inherit;
}
.login-box label[for="username"],
.login-box label[for="password"],
.login-box label[for="totp"]
{
font-size: 1rem;
margin: 0 !important;
transform: translateY(2.1rem) translateX(1rem);
padding: 0;
}
/*label[for="password"]{*/
/* transform: translateY(32px) translateX(16px);*/
/*}*/
.modal-content { .modal-content {
border-radius: 10px; border-radius: 10px;
} }
@@ -428,6 +592,7 @@ main{
background-color: #dfdfdf; background-color: #dfdfdf;
} }
} }
@-moz-keyframes loading { @-moz-keyframes loading {
0% { 0% {
background-color: #dfdfdf; background-color: #dfdfdf;
@@ -446,14 +611,16 @@ main{
.conf_card:hover { .conf_card:hover {
border-color: #007bff; border-color: #007bff;
cursor: pointer;
} }
.info_loading { .info_loading {
animation: loading 2s infinite ease-in-out; /* animation: loading 2s infinite ease-in-out;
border-radius: 5px; /* border-radius: 5px; */
height: 19px; height: 19.19px;
transition: 0.3s ease-in-out; /* transition: 0.3s ease-in-out; */
/* transform: translateX(40px); */
opacity: 0 !important;
} }
#conf_status_btn { #conf_status_btn {
@@ -470,7 +637,8 @@ main{
width: 100%; width: 100%;
} }
#selected_ip_list .badge, #selected_peer_list .badge{ #selected_ip_list .badge,
#selected_peer_list .badge {
margin: 0.1rem margin: 0.1rem
} }
@@ -530,3 +698,470 @@ pre.index-alert{
margin-top: 1rem; margin-top: 1rem;
color: white; color: white;
} }
.peerNameCol {
display: flex;
align-items: center;
margin-bottom: 0.2rem
}
.peerName {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.peerLightContainer {
text-transform: uppercase;
margin: 0;
margin-left: auto !important;
}
/*.conf_card .dot,*/
/*.info .dot {*/
/* transform: translateX(10px);*/
/*}*/
#config_body {
transition: 0.3s ease-in-out;
}
#config_body.firstLoading {
opacity: 0.2;
}
.chartTitle {
display: flex;
}
.chartControl {
margin-bottom: 1rem;
display: flex;
align-items: center;
}
.chartTitle h6 {
margin-bottom: 0;
line-height: 1;
margin-right: 0.5rem;
}
.chartContainer.fullScreen {
position: fixed;
z-index: 9999;
background-color: white;
top: 0;
left: 0;
width: calc( 100% + 15px);
height: 100%;
padding: 32px;
}
.chartContainer.fullScreen .col-sm {
padding-right: 0;
height: 100%;
}
.chartContainer.fullScreen .chartCanvasContainer {
width: 100%;
height: calc( 100% - 47px) !important;
max-height: calc( 100% - 47px) !important;
}
#switch{
transition: all 200ms ease-in;
}
.toggle--switch{
display: none;
}
.toggleLabel{
width: 64px;
height: 32px;
background-color: #6c757d17;
display: flex;
position: relative;
border: 2px solid #6c757d8c;
border-radius: 100px;
transition: all 200ms ease-in;
cursor: pointer;
margin: 0;
}
.toggle--switch.waiting + .toggleLabel{
opacity: 0.5;
}
.toggleLabel::before{
background-color: #6c757d;
height: 26px;
width: 26px;
content: "";
border-radius: 100px;
margin: 1px;
position: absolute;
animation-name: off;
animation-duration: 350ms;
animation-fill-mode: forwards;
transition: all 200ms ease-in;
cursor: pointer;
}
.toggleLabel:hover::before{
filter: brightness(1.2);
}
.toggle--switch:checked + .toggleLabel{
background-color: #007bff17 !important;
border: 2px solid #007bff8c;
}
.toggle--switch:checked + .toggleLabel::before{
background-color: #007bff;
animation-name: on;
animation-duration: 350ms;
animation-fill-mode: forwards;
}
@keyframes on {
0%{
left: 0px;
}
60%{
left: 0px;
width: 40px;
}
100%{
left: 32px;
width: 26px;
}
}
@keyframes off {
0%{
left: 32px;
}
60%{
left: 18px;
width: 40px;
}
100%{
left: 0px;
width: 26px;
}
}
.toastContainer{
z-index: 99999 !important;
}
.toast{
min-width: 300px;
background-color: rgba(255,255,255,1);
z-index: 99999;
}
.toast-header{
background-color: rgba(255,255,255);
}
.toast-progressbar{
width: 100%;
height: 4px;
background-color: #007bff;
border-bottom-left-radius: .25rem;
}
.addConfigurationAvailableIPs{
margin-bottom: 0;
}
.input-feedback{
display: none;
}
#addConfigurationModal label{
display: flex;
width: 100%;
align-items: center;
}
#addConfigurationModal label a{
margin-left: auto !important;
}
#reGeneratePrivateKey{
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.addConfigurationToggleStatus.waiting{
opacity: 0.5;
}
/*.conf_card .card-body .row .card-col{*/
/* margin-bottom: 0.5rem;*/
/*}*/
.peerDataUsageChartContainer{
min-height: 50vh;
width: 100%;
}
.peerDataUsageChartControl{
display: block !important;
margin: 0;
}
.peerDataUsageChartControl .switchUnit{
width: 33.3%;
}
.peerDataUsageChartControl .switchTimePeriod{
width: 25%;
}
@media (min-width: 1200px){
#peerDataUsage .modal-xl {
max-width: 95vw;
}
}
.bottom{
display: none;
}
@media (max-width: 768px){
.bottom{
display: block;
}
.btn-manage-group{
bottom: calc( 3rem + 40px + env(safe-area-inset-bottom, 5px));
}
main{
padding-bottom: calc( 3rem + 40px + env(safe-area-inset-bottom, 5px));
}
}
.bottomNavContainer{
display: flex;
color: #333;
padding-bottom: env(safe-area-inset-bottom, 5px);
box-shadow: inset 0 1px 0 rgb(0 0 0 / 10%);
}
.bottomNavButton{
width: 25vw;
display: flex;
flex-direction: column;
align-items: center;
margin: 0.7rem 0;
color: rgba(51, 51, 51, 0.5);
cursor: pointer;
transition: all ease-in 0.2s;
}
.bottomNavButton.active{
color: #333;
}
.bottomNavButton i{
font-size: 1.2rem;
}
.bottomNavButton .subNav{
width: 100vw;
position: absolute;
z-index: 10000;
bottom: 0;
left: 0;
background-color: #272b30;
display: none;
animation-duration: 400ms;
padding-bottom: env(safe-area-inset-bottom, 5px);
}
.bottomNavButton .subNav.active{
display: block;
}
.bottomNavButton .subNav .nav .nav-item .nav-link{
padding: 0.7rem 1rem;
}
.bottomNavWrapper{
height: 100%;
width: 100%;
background-color: #000000a1;
position: fixed;
z-index: 1030;
display: none;
left: 0;
}
.bottomNavWrapper.active{
display: block;
}
.sb-update-url .dot-running{
transform: translateX(10px);
}
.list-group-item{
transition: all 0.1s ease-in;
}
.theme-switch-btn{
width: 100%;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease-in-out;
/*position: absolute;*/
/*padding-top: 50px*/
}
.fade-enter-from,
.fade-leave-to {
transform: translateY(30px);
opacity: 0;
}
.fade2-enter-active,
.fade2-leave-active {
transition: all 0.15s ease-in-out;
}
.fade2-enter-from{
transform: translateX(20px);
opacity: 0;
}
.fade2-leave-to {
transform: translateX(-20px);
opacity: 0;
}
.login-container-fluid{
height: calc(100% - 50px) !important;
}
.totp{
font-family: var(--bs-font-monospace);
}
.message-move, /* apply transition to moving elements */
.message-enter-active,
.message-leave-active {
transition: all 0.5s ease;
}
.message-enter-from,
.message-leave-to {
filter: blur(2px);
opacity: 0;
}
.message-enter-from{
transform: translateY(-30px) scale(0.7);
}
.message-leave-to{
transform: translateY(30px);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.message-leave-active {
position: absolute;
}
.fade3-enter-active,
.fade3-leave-active {
transition: all 0.15s ease-in-out;
}
.fade3-enter-from{
transform: scale(1);
opacity: 0;
}
.fade3-leave-to {
transform: scale(0.8);
opacity: 0;
}
.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease-in-out;
}
/*.list-leave-active{*/
/* position: absolute !important;*/
/*}*/
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: scale(1.1);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.list-leave-active {
position: absolute;
}
.peerSettingContainer {
background-color: #00000060;
z-index: 9999;
backdrop-filter: blur(1px);
-webkit-backdrop-filter: blur(1px);
}
.dashboardModal{
min-height: calc(100% - 1.75rem * 2);
width: 700px
}
.dashboardModal > .card{
margin: 1.75rem;
}
.zoom-enter-active,
.zoom-leave-active, .zoomReversed-enter-active,
.zoomReversed-leave-active {
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 0.9);
/*position: absolute;*/
/*padding-top: 50px*/
}
.zoom-enter-from,
.zoom-leave-to {
transform: scale(1.1);
filter: blur(3px);
opacity: 0;
}
.zoomReversed-enter-from,
.zoomReversed-leave-to {
transform: scale(0.9);
filter: blur(3px);
opacity: 0;
}
.messageCentre{
z-index: 9999;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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