Compare commits
389 Commits
v2.0
...
v4.0.beta2
Author | SHA1 | Date | |
---|---|---|---|
|
410b81f46f | ||
|
aa3711c5cc | ||
|
d6b1f97a04 | ||
|
b4e8e57a22 | ||
|
9644e6195c | ||
|
764e0c7607 | ||
|
97d640dd40 | ||
|
d2915b5b05 | ||
|
f274f6fd18 | ||
|
f507ac2569 | ||
|
208cbd6d89 | ||
|
fa2d7fa3da | ||
|
7463767781 | ||
|
958bc864c9 | ||
|
4484668750 | ||
|
d5dea4b87f | ||
|
0fdef6a0a2 | ||
|
bd71b6bad8 | ||
|
9b7887b279 | ||
|
3960e43872 | ||
|
201c8f9ec9 | ||
|
8c8374a08c | ||
|
467595afc9 | ||
|
acb54f098c | ||
|
5755d13460 | ||
|
2c3500315d | ||
|
47ea60c0cd | ||
|
18b18c1396 | ||
|
ff227de5fa | ||
|
6799692811 | ||
|
c6173f7f6f | ||
|
d0e4dabc44 | ||
|
f815dae300 | ||
|
b3bd6bb39e | ||
|
71df6409c2 | ||
|
e4f9a1e0cf | ||
|
ca6a05e393 | ||
|
c0d26164dc | ||
|
76fe2a1ba9 | ||
|
8cbdb54402 | ||
|
764ef80a62 | ||
|
0c37d93c01 | ||
|
c57a5128e5 | ||
|
6cf4eba20a | ||
|
6825d728c2 | ||
|
6d3091b2a2 | ||
|
61473877a4 | ||
|
52989c8f5c | ||
|
b64ba2ef16 | ||
|
461ae99dd8 | ||
|
8681df6f02 | ||
|
ba081ee442 | ||
|
cf90d05115 | ||
|
658c6554af | ||
|
94d9d608f7 | ||
|
015b50be5f | ||
|
85970f8c96 | ||
|
881d62d69d | ||
|
935129f0a5 | ||
|
63e8553a09 | ||
|
b65828416f | ||
|
48dc8033f5 | ||
|
2d838b69fd | ||
|
6c529a6908 | ||
|
9baefec541 | ||
|
327d66bb80 | ||
|
760a4dfcb9 | ||
|
8ed75d1d21 | ||
|
54710b8221 | ||
|
ff794033e1 | ||
|
f0f486da9e | ||
|
eb18857ecc | ||
|
9a280e99ad | ||
|
c7ca20b45a | ||
|
41e05ddf9c | ||
|
5a34f16dcf | ||
|
769ca4e26d | ||
|
57c2e89f00 | ||
|
914a0bf514 | ||
|
75fbdb653a | ||
|
bdfe75cff3 | ||
|
bcd845fd59 | ||
|
f1e71ecb78 | ||
|
0aa4c8af6f | ||
|
a950b80d5a | ||
|
ed3bb6429b | ||
|
1e88491ca1 | ||
|
6b6ad05e3a | ||
|
5f4a364095 | ||
|
95a8867527 | ||
|
7cb1301e80 | ||
|
e6e070d89e | ||
|
ba2bcaba07 | ||
|
864f82ba11 | ||
|
f671c992e1 | ||
|
0c0bce9755 | ||
|
f07508073f | ||
|
e06cc1bd2d | ||
|
36e33a4c10 | ||
|
7f668c1653 | ||
|
b464fa98df | ||
|
23491f1e8c | ||
|
2b90a2eed2 | ||
|
13b9d15d8f | ||
|
a053504bb8 | ||
|
dcdd4aec85 | ||
|
179da2ac05 | ||
|
4848739b6e | ||
|
46da285831 | ||
|
71a6a36a54 | ||
|
c8ca9ef7ab | ||
|
5af2fff9ca | ||
|
337c9bc01e | ||
|
a196dce1fa | ||
|
b9633bbcd6 | ||
|
46efe2b8dd | ||
|
cc1dd682e8 | ||
|
bdd984a887 | ||
|
2d3dffe5fc | ||
|
65f31a0b38 | ||
|
4a1a6c5933 | ||
|
7e1fd99c37 | ||
|
8fe8209580 | ||
|
264a050360 | ||
|
3623104e3b | ||
|
191ff1abec | ||
|
3bb86493cc | ||
|
d1d3151e1e | ||
|
f9dc9ebdb3 | ||
|
3b478bcc2d | ||
|
77bb78c381 | ||
|
cafa97f502 | ||
|
0d858493d5 | ||
|
99fb07c6b3 | ||
|
eaad971c0a | ||
|
377abd87fd | ||
|
af71176296 | ||
|
69737177ef | ||
|
efae1222c1 | ||
|
3caea1a903 | ||
|
a43478d627 | ||
|
ed2c3f43c7 | ||
|
84c9846f7b | ||
|
fcfd816cec | ||
|
2e3977e59c | ||
|
3c68430336 | ||
|
43afb86fa8 | ||
|
194ccbdbb4 | ||
|
7139e230cf | ||
|
120d3b9f54 | ||
|
712460a040 | ||
|
be5594f1c9 | ||
|
0a885117db | ||
|
5b73654544 | ||
|
1485b78b7b | ||
|
bfec57172a | ||
|
9f8559c12d | ||
|
83dde12ca9 | ||
|
d17888db4e | ||
|
096529af96 | ||
|
8a56b22635 | ||
|
dcdd353981 | ||
|
258564208b | ||
|
e2e8222005 | ||
|
75f2826805 | ||
|
730af4f2ee | ||
|
e5737ebd9d | ||
|
7f24904164 | ||
|
9ff7198602 | ||
|
6d56967a0f | ||
|
584118805a | ||
|
68b3f01082 | ||
|
77a82cb84b | ||
|
844b19495d | ||
|
b8b721f2bd | ||
|
4f93d48ac6 | ||
|
02d1bf5a2c | ||
|
fb3606c2b7 | ||
|
a5bc2cd48f | ||
|
72151ac6a3 | ||
|
e2c8b3bf02 | ||
|
06dd641274 | ||
|
7d9bf01d27 | ||
|
b9553e0a05 | ||
|
993dcaf235 | ||
|
8d7c39bac4 | ||
|
80933ab571 | ||
|
b5b30c8119 | ||
|
bca06129d9 | ||
|
a136b86d81 | ||
|
8d555d8ae7 | ||
|
c22e61add2 | ||
|
2c83f1ce57 | ||
|
f2750de153 | ||
|
e0934f11d6 | ||
|
422924026a | ||
|
332e22e534 | ||
|
c7bf30e18e | ||
|
5ade5176c0 | ||
|
f9e4fafe9f | ||
|
201f2ef1f0 | ||
|
32af4fdcca | ||
|
d254b4cc39 | ||
|
d0ad4e05bf | ||
|
c75854cb7b | ||
|
8cc71913b3 | ||
|
99abe6bcd2 | ||
|
f5ad7a49ce | ||
|
e42ecf3c99 | ||
|
b58bf9dc17 | ||
|
745c8aaf3e | ||
|
0ca73bf80b | ||
|
fd76cabd01 | ||
|
fcf9300a49 | ||
|
649b66ba1a | ||
|
b5d73a0861 | ||
|
1453996ccb | ||
|
5bf9b97d70 | ||
|
5c1e46f5a6 | ||
|
c2665cbae8 | ||
|
3934248a6b | ||
|
7ae3bb5601 | ||
|
9890d4f3bc | ||
|
5b47fc16d9 | ||
|
344770a0a4 | ||
|
fc11cacd24 | ||
|
737ebdd0c6 | ||
|
d06dddfa24 | ||
|
710d631ffe | ||
|
7d9a845ca0 | ||
|
cc6e5ffb97 | ||
|
8d29cb7488 | ||
|
5c588ea01a | ||
|
dade9935da | ||
|
34cd3aaaca | ||
|
78d3f338fb | ||
|
d801c5911e | ||
|
003743c074 | ||
|
4cef39d3b1 | ||
|
83f0df32d7 | ||
|
24f269191c | ||
|
94a0d5a0a4 | ||
|
c0fbf4dd0c | ||
|
37b6161075 | ||
|
c9b792c370 | ||
|
2103d547a1 | ||
|
3918e220d2 | ||
|
c3eaaed43b | ||
|
e0bf648076 | ||
|
bb298164e4 | ||
|
898694b9be | ||
|
f0f9ac92e6 | ||
|
ea0229a8ab | ||
|
66fc7cd91d | ||
|
c11d4f01b1 | ||
|
a8f498f292 | ||
|
6854e2275f | ||
|
6662e79ac5 | ||
|
9e77430634 | ||
|
b8e070824a | ||
|
304d4293f3 | ||
|
a03e7f5b53 | ||
|
ea57c8cad4 | ||
|
280802cf74 | ||
|
fbce01d846 | ||
|
591b60c336 | ||
|
78ef870d91 | ||
|
bc19d82aed | ||
|
79b41e1056 | ||
|
805f03d231 | ||
|
62acc1081f | ||
|
257a2c2d9f | ||
|
ed9b05cdf9 | ||
|
2cc6fbd643 | ||
|
bdfc260dd2 | ||
|
7ad65956c9 | ||
|
b3dc60b7ca | ||
|
bed08109fa | ||
|
06cedec090 | ||
|
1d2c468d03 | ||
|
e84616f5f5 | ||
|
caa9a90476 | ||
|
62be683dd8 | ||
|
ffa63b3f82 | ||
|
c5a7788394 | ||
|
99c42ff2c1 | ||
|
fd2b7bb917 | ||
|
922a494df0 | ||
|
c478ec0095 | ||
|
3abbe17910 | ||
|
349076599f | ||
|
92d299507f | ||
|
6f2d61e918 | ||
|
ed13babf6a | ||
|
80db82eb93 | ||
|
8d20334d6e | ||
|
03a95ad018 | ||
|
aa59cc8af0 | ||
|
80d63c2c89 | ||
|
03a4acd72b | ||
|
53118cf660 | ||
|
16b1baf81e | ||
|
960e0d0965 | ||
|
8f41222e43 | ||
|
e7bc447166 | ||
|
1736359615 | ||
|
58784b1e22 | ||
|
07f2bfd279 | ||
|
6d62db2890 | ||
|
a1b23c4aab | ||
|
e272a096da | ||
|
b8ca24c5bf | ||
|
a05d09f0de | ||
|
1807a79c4f | ||
|
77f6826af9 | ||
|
7e39eedf35 | ||
|
0b4b48ceae | ||
|
dad931e898 | ||
|
2e387730cb | ||
|
c9b6775793 | ||
|
a6eaf30c2b | ||
|
6f91d199d8 | ||
|
7ea486307a | ||
|
37d254c827 | ||
|
b4014eb317 | ||
|
c7824fa184 | ||
|
13c5bd45f9 | ||
|
465d102bd0 | ||
|
93007cd56c | ||
|
8367576dc8 | ||
|
dbc06145b3 | ||
|
4efcf0e7c8 | ||
|
0a4bbc1106 | ||
|
b08c1f6699 | ||
|
6828b905ef | ||
|
f72b405f6d | ||
|
27ae99864c | ||
|
82ba15ba5a | ||
|
af01db7bc9 | ||
|
6ec0a65d0c | ||
|
c32ccfc9ed | ||
|
ef818388c8 | ||
|
f5aa45a3e1 | ||
|
b7a047a743 | ||
|
05ec985453 | ||
|
88ed9c3e79 | ||
|
3fb03c8dfe | ||
|
ff304027cd | ||
|
3140d46c94 | ||
|
9d476af384 | ||
|
0d380672f3 | ||
|
e2fb8dca5b | ||
|
eaba4c90c5 | ||
|
b4ca7373f3 | ||
|
905c068ed5 | ||
|
02886dd420 | ||
|
5bf7754d6c | ||
|
7a0a3055c4 | ||
|
bf73efb3b4 | ||
|
b4da212158 | ||
|
4a04d42600 | ||
|
42bbfbe538 | ||
|
31ec2430c4 | ||
|
f0b56c9f1d | ||
|
6f9f01c991 | ||
|
b822ccb19d | ||
|
dcb11671f3 | ||
|
282599d36f | ||
|
f11acdc68f | ||
|
971e127cfd | ||
|
9b2c59d529 | ||
|
14dc049a66 | ||
|
d3d0e7c615 | ||
|
89423ccb8b | ||
|
746f31f70c | ||
|
3aa9eab2bd | ||
|
1bcc8fed5e | ||
|
1213a030e3 | ||
|
3b2344b477 | ||
|
c53b0fe2d4 | ||
|
0a92269456 | ||
|
1cb8906893 | ||
|
f76579fe5e | ||
|
db05ff5e98 | ||
|
a05d6979ec | ||
|
be40d17172 | ||
|
9e11339979 | ||
|
a56129afe0 | ||
|
8933ea999c |
@@ -23,10 +23,37 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "reafian",
|
||||
"name": "Richard Newton",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11992416?v=4",
|
||||
"profile": "https://github.com/reafian",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "davejlong",
|
||||
"name": "David Long",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/175317?v=4",
|
||||
"profile": "http://www.davejlong.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "marneu",
|
||||
"name": "Markus Neubauer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5978293?v=4",
|
||||
"profile": "http://www.std-soft.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "wireguard-dashboard",
|
||||
"projectName": "WGDashboard",
|
||||
"projectOwner": "donaldzou",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
|
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [donaldzou]
|
54
.gitignore
vendored
@@ -1,12 +1,52 @@
|
||||
.vscode/sftp.json
|
||||
src/.vscode/sftp.json
|
||||
.vscode
|
||||
.DS_Store
|
||||
wg.db
|
||||
*.json
|
||||
.idea
|
||||
src/test.py
|
||||
tmp
|
||||
src/db
|
||||
__pycache__
|
||||
src/wg-dashboard.ini
|
||||
src/test.py
|
||||
*.db
|
||||
src/wg-dashboard.ini
|
||||
src/static/pic.xd
|
||||
*.conf
|
||||
private_key.txt
|
||||
public_key.txt
|
||||
venv/**
|
||||
log/**
|
||||
release/*
|
||||
src/db/wgdashboard.db
|
||||
.jshintrc
|
||||
node_modules/**
|
||||
*/proxy.js
|
||||
src/static/app/proxy.js
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
proxy.js
|
||||
.vite/*
|
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ARG WG_ADDRESS=$WG_ADDRESS
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends iproute2 wireguard-tools iptables nano net-tools python3 python3-pip python3-venv procps openresolv inotify-tools && \
|
||||
apt-get clean
|
||||
|
||||
RUN mkdir -p /etc/wireguard/
|
||||
RUN mkdir -p /opt/wgdashboard
|
||||
RUN mkdir -p /opt/wgdashboard_tmp
|
||||
# configure wireguard
|
||||
RUN wg genkey | tee /opt/wgdashboard_tmp/privatekey | wg pubkey | tee /opt/wgdashboard_tmp/publickey
|
||||
|
||||
RUN cd / && echo "[Interface]" > wg0.conf && echo "SaveConfig = true" >> wg0.conf && echo -n "PrivateKey = " >> wg0.conf && cat /opt/wgdashboard_tmp/privatekey >> wg0.conf \
|
||||
&& echo "ListenPort = 51820" >> wg0.conf && echo "Address = ${WG_ADDRESS}" >> wg0.conf && chmod 700 wg0.conf
|
||||
|
||||
COPY ./src /opt/wgdashboard_tmp
|
||||
RUN pip3 install -r /opt/wgdashboard_tmp/requirements.txt --no-cache-dir
|
||||
RUN rm -rf /opt/wgdashboard_tmp
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
RUN chmod u+x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
WORKDIR /opt/wgdashboard
|
||||
|
||||
EXPOSE 10086
|
||||
EXPOSE 51820/udp
|
568
README.md
@@ -1,7 +1,7 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/donaldzou/wireguard-dashboard/main/img/Group%202.png" width="128">
|
||||
<img alt="WGDashboard" src="img/logo.png" width="128">
|
||||
</p>
|
||||
<h1 align="center"> Wireguard Dashboard</h1>
|
||||
<h1 align="center">WGDashboard</h1>
|
||||
|
||||
|
||||
<p align="center">
|
||||
@@ -9,166 +9,534 @@
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/donaldzou/wireguard-dashboard/releases/latest"><img src="https://img.shields.io/github/v/release/donaldzou/wireguard-dashboard"></a>
|
||||
<a href="https://wakatime.com/badge/user/45f53c7c-9da9-4cb0-85d6-17bd38cc748b/project/5334ae20-e9a6-4c55-9fea-52d4eb9dfba6"><img src="https://wakatime.com/badge/user/45f53c7c-9da9-4cb0-85d6-17bd38cc748b/project/5334ae20-e9a6-4c55-9fea-52d4eb9dfba6.svg" alt="wakatime"></a>
|
||||
<a href="https://hits.seeyoufarm.com"><img src="https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fdonaldzou%2FWGDashboard&count_bg=%2379C83D&title_bg=%23555555&icon=github.svg&icon_color=%23E7E7E7&title=Visitor&edge_flat=false"/></a>
|
||||
</p>
|
||||
<p align="center">Monitoring Wireguard is not convinient, need to login into server and type <code>wg show</code>. That's why this platform is being created, to view all configurations and manage them in a easier way.</p>
|
||||
<p align="center">Monitoring WireGuard is not convenient, need to login into server and type <code>wg show</code>. That's why this project is being created, to view all configurations and manage them in a easy way.</p>
|
||||
<p align="center"><small><i>This project is not affiliate to the official WireGuard Project</i></small></p>
|
||||
|
||||
## 📣 What's New: v4.0
|
||||
|
||||
> I can't thank enough for all of you who wait for this release, and for those who are new to this project, welcome :) Also, huge thanks who sponsored me GitHub :heart:
|
||||
|
||||
- 🎉 **New Features**
|
||||
- **Updated dashboard design**: Re-designed some of the section with more modern style and layout, the UI is faster and more responsive, it also uses less memory. But overall is still the same dashboard you're familiarized.
|
||||
- **Peer Job Scheduler**: Now you can schedule jobs for each peer to either **restrict** or **delete** the peer if the peer's total / upload / download data usage exceeded a limit, or you can set a specific datetime to restrict or delete the peer.
|
||||
- **API Key for WGDashboard's REST API**: You can now request all the api endpoint used in the dashboard. For more details please review the API Documentation below.
|
||||
- **Logging**: Dashboard will now log all activity on the dashboard and API requests.
|
||||
- **Time-Based One-Time Password (TOTP)**: You can enable this function to add one more layer of security, and generate the TOTP with your choice of authenticator.
|
||||
- **Designs**
|
||||
- **Real-time Graphs**: You can view real-time data changes with graphs in each configuration.
|
||||
- **Night mode**: You know what that means, it avoids bugs ;)
|
||||
- **Enforce Python Virtual Environment**: I noticed newer Python version (3.12) does not allow to install packages globally, and plus I think is a good idea to use venv.
|
||||
|
||||
- **🧐 Other Changes**
|
||||
- **Deprecated jQuery from the project, and migrated and rewrote the whole front-end with Vue.js. This allow the dashboard is future proofed, and potential cross server access with a desktop app.**
|
||||
- Rewrote the backend into a REST API structure
|
||||
- Improved SQL query efficient
|
||||
- Removed all templates, except for `index.html` where it will load the Vue.js app.
|
||||
|
||||
|
||||
|
||||
## 📣 What's New: Version 2.0
|
||||
|
||||
### ⚠️ **Update from v1.x.x**
|
||||
|
||||
1. Stop the dashboard if it is running.
|
||||
2. You can use `git pull https://github.com/donaldzou/Wireguard-Dashboard.git v2.0` to get the new update inside `Wireguard-Dashboard` directory.
|
||||
3. Proceed **Step 2 & 3** in the Install step down below.
|
||||
**For users who is using `v2.x.x` please be sure to read [this](#please-note-for-user-who-is-using-v231-or-below) before updating WGDashboard**
|
||||
|
||||
<hr>
|
||||
|
||||
- Added login function to dashboard
|
||||
## Table of Content
|
||||
|
||||
- ***I'm not using the most ideal way to store the username and password, feel free to provide a better way to do this if you any good idea!***
|
||||
|
||||
- Added a config file to the dashboard
|
||||
|
||||
- Dashboard config can be change within the **Setting** tab on the side bar
|
||||
|
||||
- Adjusted UI
|
||||
|
||||
- And much more!
|
||||
|
||||
|
||||
- [💡 Features](#-features)
|
||||
- [📝 Requirement](#-requirement)
|
||||
- [🛠 Install](#-install)
|
||||
- [🪜 Usage](#-usage)
|
||||
- [Start/Stop/Restart WGDashboard](#startstoprestart-wgdashboard)
|
||||
- [Autostart WGDashboard on boot](#autostart-wgdashboard-on-boot--v22)
|
||||
- [✂️ Dashboard Configuration](#%EF%B8%8F-dashboard-configuration)
|
||||
- [Dashboard Configuration file](#dashboard-configuration-file)
|
||||
- [Generating QR code and peer configuration file (.conf)](#generating-qr-code-and-peer-configuration-file-conf)
|
||||
- [❓ How to update the dashboard?](#-how-to-update-the-dashboard)
|
||||
- [🥘 Experimental Functions](#-experimental-functions)
|
||||
- [🔍 Screenshot](#-screenshot)
|
||||
- [⏰ Changelog](#--changelog)
|
||||
- [🛒 Dependencies](#-dependencies)
|
||||
- [✨ Contributors](#-contributors)
|
||||
|
||||
## 💡 Features
|
||||
|
||||
- Add peers for each WireGuard configuration
|
||||
- Automatically look for existing WireGuard configuration under `/etc/wireguard`
|
||||
- Easy to use interface, provided credential and TOTP protection to the dashboard
|
||||
- Manage peers and configuration
|
||||
- Add Peers or by bulk with auto-generated information
|
||||
- Edit peer information
|
||||
- Delete peers with ease
|
||||
- Restrict peers
|
||||
- Generate QR Code and `.conf` file for peers
|
||||
- Schedule jobs to delete / restrict peer when conditions are met
|
||||
- View real time peer status
|
||||
- Testing tool: Ping and Traceroute to your peer
|
||||
|
||||
- Manage peer
|
||||
|
||||
- Delete peers
|
||||
|
||||
- And many more coming up! Welcome to contribute to this project!
|
||||
|
||||
|
||||
|
||||
## 📝 Requirement
|
||||
|
||||
- Ubuntu or Debian based OS, other might work, but haven't test yet. Tested on the following OS:
|
||||
- [x] Ubuntu 18.04.1 LTS
|
||||
- [ ] If you have tested on other OS and it works perfectly please provide it to me!
|
||||
- Recommend the following OS, tested by our beloved users:
|
||||
- [x] Ubuntu 18.04.1 LTS, 20.04.1 LTS, 22.04.4 LTS [@Me]
|
||||
- [x] Debian GNU/Linux 10 (buster) [❤️ @[robchez](https://github.com/robchez)]
|
||||
- [x] AlmaLinux 8.4 (Electric Cheetah) [❤️ @[barry-smithjr](https://github.com/)]
|
||||
- [x] CentOS 7 [❤️ @[PrzemekSkw](https://github.com/PrzemekSkw)]
|
||||
|
||||
- ‼️ Make sure you have **Wireguard** and **Wireguard-Tools (`wg-quick`)** installed.‼️ <a href="https://www.wireguard.com/install/">How to install?</a>
|
||||
- Configuration files under **/etc/wireguard**
|
||||
> **If you have tested on other OS and it works perfectly please provide it to me in [#31](https://github.com/donaldzou/wireguard-dashboard/issues/31). Thank you!**
|
||||
|
||||
- **Note: For peers, `PublicKey` & `AllowedIPs` is required.**
|
||||
- Python 3.7+ & Pip3
|
||||
```
|
||||
$ sudo apt-get install python3 python3-pip
|
||||
- **WireGuard** and **WireGuard-Tools (`wg-quick`)** are installed.
|
||||
|
||||
> Don't know how? Check this <a href="https://www.wireguard.com/install/">official documentation</a>
|
||||
|
||||
- Configuration files under **`/etc/wireguard`**, but please note the following sample
|
||||
|
||||
```ini
|
||||
[Interface]
|
||||
...
|
||||
SaveConfig = true
|
||||
# Need to include this line to allow WireGuard Tool to save your configuration,
|
||||
# or if you just want it to monitor your WireGuard Interface and don't need to
|
||||
# make any changes with the dashboard, you can set it to false.
|
||||
|
||||
[Peer]
|
||||
PublicKey = abcd1234
|
||||
AllowedIPs = 1.2.3.4/32
|
||||
# Must have for each peer
|
||||
```
|
||||
|
||||
|
||||
- **Python 3.10** for v4.0+, **Python 3.7 - 3.9** for v2.0 - v3.0.6.2
|
||||
|
||||
## 🛠 Install
|
||||
1. Download WGDashboard
|
||||
|
||||
1. Download Wireguard Dashboard
|
||||
```shell
|
||||
git clone -b v4.0 https://github.com/donaldzou/WGDashboard.git wgdashboard
|
||||
|
||||
2. Open the WGDashboard folder
|
||||
|
||||
```
|
||||
$ git clone -b v2.0 https://github.com/donaldzou/Wireguard-Dashboard.git
|
||||
```
|
||||
```shell
|
||||
cd wgdashboard/src
|
||||
```
|
||||
|
||||
3. Install WGDashboard
|
||||
|
||||
**2. Install Python Dependencies**
|
||||
```shell
|
||||
sudo chmod u+x wgd.sh
|
||||
sudo ./wgd.sh install
|
||||
```
|
||||
|
||||
```
|
||||
$ cd Wireguard-Dashboard/src
|
||||
$ python3 -m pip install -r requirements.txt
|
||||
```
|
||||
4. Give read and execute permission to root of the WireGuard configuration folder, you can change the path if your configuration files are not stored in `/etc/wireguard`
|
||||
|
||||
**3. Install & run Wireguard Dashboard**
|
||||
```shell
|
||||
sudo chmod -R 755 /etc/wireguard
|
||||
```
|
||||
|
||||
```
|
||||
$ sudo sh wgd.sh start
|
||||
```
|
||||
5. Run WGDashboard
|
||||
|
||||
Access your server with port `10086` ! e.g (http://your_server_ip:10086), continue to read to on how to change port and ip that dashboard is running with.
|
||||
```shell
|
||||
sudo ./wgd.sh start
|
||||
```
|
||||
|
||||
**Note**:
|
||||
|
||||
> For [`pivpn`](https://github.com/pivpn/pivpn) user, please use `sudo ./wgd.sh start` to run if your current account does not have the permission to run `wg show` and `wg-quick`.
|
||||
|
||||
6. Access dashboard
|
||||
|
||||
Access your server with port `10086` (e.g. http://your_server_ip:10086), using username `admin` and password `admin`. See below how to change port and ip that the dashboard is running with.
|
||||
|
||||
## 🪜 Usage
|
||||
|
||||
**1. Start/Stop/Restart Wireguard Dashboard**
|
||||
#### Start/Stop/Restart WGDashboard
|
||||
|
||||
```
|
||||
$ cd Wireguard-Dashboard/src
|
||||
$ sudo sh wgd.sh start # Start the dashboard in background
|
||||
$ sudo sh wgd.sh debug # Start the dashboard in foreground (debug mode)
|
||||
$ sudo sh wgd.sh stop # Stop the dashboard
|
||||
$ sudo sh wgd.sh restart # Restart the dasboard
|
||||
$ sudo sh wgd.sh update # Update the dashboard
|
||||
|
||||
```shell
|
||||
cd wgdashboard/src
|
||||
-----------------------------
|
||||
./wgd.sh start # Start the dashboard in background
|
||||
-----------------------------
|
||||
./wgd.sh debug # Start the dashboard in foreground (debug mode)
|
||||
-----------------------------
|
||||
./wgd.sh stop # Stop the dashboard
|
||||
-----------------------------
|
||||
./wgd.sh restart # Restart the dasboard
|
||||
```
|
||||
|
||||
⚠️ **For first time user please also read the next section.**
|
||||
#### Autostart WGDashboard on boot (>= v2.2)
|
||||
|
||||
In the `src` folder, it contained a file called `wg-dashboard.service`, we can use this file to let our system to autostart the dashboard after reboot. The following guide has tested on **Ubuntu**, most **Debian** based OS might be the same, but some might not. Please don't hesitate to provide your system if you have tested the autostart on another system.
|
||||
|
||||
1. Changing the directory to the dashboard's directory
|
||||
|
||||
```shell
|
||||
cd wgdashboard/src
|
||||
```
|
||||
|
||||
2. Get the full path of the dashboard's directory
|
||||
|
||||
```shell
|
||||
pwd
|
||||
#Output: /root/wgdashboard/src
|
||||
```
|
||||
|
||||
For this example, the output is `/root/wireguard-dashboard/src`, your path might be different since it depends on where you downloaded the dashboard in the first place. **Copy the the output to somewhere, we will need this in the next step.**
|
||||
|
||||
3. Edit the service file, the service file is located in `wireguard-dashboard/src`, you can use other editor you like, here will be using `nano`
|
||||
|
||||
```shell
|
||||
nano wg-dashboard.service
|
||||
```
|
||||
|
||||
You will see something like this:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
After=network.service
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=<your dashboard directory full path here>
|
||||
ExecStart=/usr/bin/python3 <your dashboard directory full path here>/dashboard.py
|
||||
Restart=always
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Now, we need to replace both `<your dashboard directory full path here>` to the one you just copied from step 2. After doing this, the file will become something like this, your file might be different:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
After=netword.service
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/root/wgdashboard/src
|
||||
ExecStart=/usr/bin/python3 /root/wgdashboard/src/dashboard.py
|
||||
Restart=always
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
**Be aware that after the value of `WorkingDirectory`, it does not have a `/` (slash).** And then save the file after you edited it
|
||||
|
||||
4. Copy the service file to systemd folder
|
||||
|
||||
```bash
|
||||
$ cp wg-dashboard.service /etc/systemd/system/wg-dashboard.service
|
||||
```
|
||||
|
||||
To make sure you copy the file successfully, you can use this command `cat /etc/systemd/system/wg-dashboard.service` to see if it will output the file you just edited.
|
||||
|
||||
5. Enable the service
|
||||
|
||||
```bash
|
||||
$ sudo chmod 664 /etc/systemd/system/wg-dashboard.service
|
||||
$ sudo systemctl daemon-reload
|
||||
$ sudo systemctl enable wg-dashboard.service
|
||||
$ sudo systemctl start wg-dashboard.service # <-- To start the service
|
||||
```
|
||||
|
||||
6. Check if the service run correctly
|
||||
|
||||
```bash
|
||||
$ sudo systemctl status wg-dashboard.service
|
||||
```
|
||||
|
||||
And you should see something like this
|
||||
|
||||
```shell
|
||||
● wg-dashboard.service
|
||||
Loaded: loaded (/etc/systemd/system/wg-dashboard.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Tue 2021-08-03 22:31:26 UTC; 4s ago
|
||||
Main PID: 6602 (python3)
|
||||
Tasks: 1 (limit: 453)
|
||||
Memory: 26.1M
|
||||
CGroup: /system.slice/wg-dashboard.service
|
||||
└─6602 /usr/bin/python3 /root/wgdashboard/src/dashboard.py
|
||||
|
||||
Aug 03 22:31:26 ubuntu-wg systemd[1]: Started wg-dashboard.service.
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Serving Flask app1 "WGDashboard" (lazy loading)
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Environment: production
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: WARNING: This is a development server. Do not use it in a production deployment.
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: Use a production WSGI server instead.
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Debug mode: off
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Running on all addresses.
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: WARNING: This is a development server. Do not use it in a production deployment.
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Running on http://0.0.0.0:10086/ (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
If you see `Active:` followed by `active (running) since...` then it means it run correctly.
|
||||
|
||||
7. Stop/Start/Restart the service
|
||||
|
||||
```bash
|
||||
sudo systemctl stop wg-dashboard.service # <-- To stop the service
|
||||
sudo systemctl start wg-dashboard.service # <-- To start the service
|
||||
sudo systemctl restart wg-dashboard.service # <-- To restart the service
|
||||
```
|
||||
|
||||
8. **And now you can reboot your system, and use the command at step 6 to see if it will auto start after the reboot, or just simply access the dashboard through your browser. If you have any questions or problem, please report it in the issue page.**
|
||||
|
||||
## ✂️ Dashboard Configuration
|
||||
|
||||
Since version 2.0, Wireguard Dashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following config:
|
||||
#### Dashboard Configuration file
|
||||
|
||||
### `[Account]`
|
||||
Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following configurations:
|
||||
|
||||
`username` - Username (Default: `admin`)
|
||||
| | Description | Default | Edit Available |
|
||||
| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | -------------- |
|
||||
| **`[Account]`** | *Configuration on account* | | |
|
||||
| `username` | Dashboard login username | `admin` | Yes |
|
||||
| `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes |
|
||||
| | | | |
|
||||
| **`[Server]`** | *Configuration on dashboard* | | |
|
||||
| `wg_conf_path` | The path of all the Wireguard configurations | `/etc/wireguard` | Yes |
|
||||
| `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes |
|
||||
| `app_port` | Port the the dashboard will run with | `10086` | Yes |
|
||||
| `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** |
|
||||
| `version` | Dashboard Version | `v3.0.5` | **No** |
|
||||
| `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes |
|
||||
| `dashboard_sort` | How configuration is sorting | `status` | Yes |
|
||||
| | | | |
|
||||
| **`[Peers]`** | *Default Settings on a new peer* | | |
|
||||
| `peer_global_dns` | DNS Server | `1.1.1.1` | Yes |
|
||||
| `peer_endpoint_allowed_ip` | Endpoint Allowed IP | `0.0.0.0/0` | Yes |
|
||||
| `peer_display_mode` | How peer will display | `grid` | Yes |
|
||||
| `remote_endpoint` | Remote Endpoint (i.e where your peers will connect to) | *depends on your server's default network interface* | Yes |
|
||||
| `peer_mtu` | Maximum Transmit Unit | `1420` | |
|
||||
| `peer_keep_alive` | Keep Alive | `21` | Yes |
|
||||
|
||||
`password` - Password, will be hash with SHA256 (Default: `admin`).
|
||||
#### Generating QR code and peer configuration file (.conf)
|
||||
|
||||
### `[Server]`
|
||||
Starting version 2.2, dashboard can now generate QR code and configuration file for each peer. Here is a template of what each QR code encoded with and the same content will be inside the file:
|
||||
|
||||
`wg_conf_path` - The path of all the Wireguard configurations (Default: `/etc/wireguard`)
|
||||
|
||||
`app_ip` - IP address the flask will run with (Default: `0.0.0.0`)
|
||||
|
||||
`app_port` - Port the flask will run with (Default: `10086`)
|
||||
|
||||
`auth_req` - Does the dashboard need authentication (Default: `true`)
|
||||
|
||||
- If `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only change the file directly in system**.
|
||||
|
||||
`version` - Dashboard Version
|
||||
|
||||
All these settings will be able to configure within the dashboard in **Settings** on the sidebar, without changing the actual file. **Except `version` and `auth_req` due to security consideration.**
|
||||
```ini
|
||||
[Interface]
|
||||
PrivateKey = QWERTYUIOPO234567890YUSDAKFH10E1B12JE129U21=
|
||||
Address = 0.0.0.0/32
|
||||
DNS = 1.1.1.1
|
||||
|
||||
[Peer]
|
||||
PublicKey = QWERTYUIOPO234567890YUSDAKFH10E1B12JE129U21=
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = 0.0.0.0:51820
|
||||
```
|
||||
|
||||
| | Description | Default Value | Available in Peer setting |
|
||||
| ----------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------- |
|
||||
| **`[Interface]`** | | | |
|
||||
| `PrivateKey` | The private key of this peer | Private key generated by WireGuard (`wg genkey`) or provided by user | Yes |
|
||||
| `Address` | The `allowed_ips` of your peer | N/A | Yes |
|
||||
| `DNS` | The DNS server your peer will use | `1.1.1.1` - Cloud flare DNS, you can change it when you adding the peer or in the peer setting. | Yes |
|
||||
| **`[Peer]`** | | | |
|
||||
| `PublicKey` | The public key of your server | N/A | No |
|
||||
| `AllowedIPs` | IP ranges for which a peer will route traffic | `0.0.0.0/0` - Indicated a default route to send all internet and VPN traffic through that peer. | Yes |
|
||||
| `Endpoint` | Your wireguard server ip and port, the dashboard will search for your server's default interface's ip. | `<your server default interface ip>:<listen port>` | Yes |
|
||||
|
||||
## ❓ How to update the dashboard?
|
||||
|
||||
```
|
||||
$ cd wireguard-dashboard
|
||||
$ sudo sh wgd.sh update # Perform update
|
||||
$ sudo sh wgd.sh start # Start dashboard
|
||||
```
|
||||
#### **Please note for user who is using `v2.3.1` or below**
|
||||
|
||||
- For user who is using `v2.3.1` or below, please notice that all data that stored in the current database will **not** transfer to the new database. This is hard decision to move from TinyDB to SQLite. But SQLite does provide a thread-safe access and TinyDB doesn't. I couldn't find a safe way to transfer the data, so you need to do them manually... Sorry about that :pensive: . But I guess this would be a great start for future development :sunglasses:.
|
||||
|
||||
<hr>
|
||||
|
||||
#### Update Method 1 (For `v3.0` or above)
|
||||
|
||||
1. Change your directory to `wgdashboard/src`
|
||||
|
||||
```bash
|
||||
cd wgdashboard/src
|
||||
```
|
||||
|
||||
2. Update the dashboard with the following
|
||||
|
||||
```bash
|
||||
./wgd.sh update
|
||||
```
|
||||
|
||||
> If this doesn't work, please use the method below. Sorry about that :(
|
||||
|
||||
#### Update Method 2
|
||||
|
||||
|
||||
1. Change your directory to `wgdashboard`
|
||||
|
||||
```shell
|
||||
cd wgdashboard/src
|
||||
```
|
||||
|
||||
2. Update the dashboard
|
||||
```shell
|
||||
git pull https://github.com/donaldzou/WGDashboard.git v3.0.5 --force
|
||||
```
|
||||
|
||||
3. Install
|
||||
|
||||
```shell
|
||||
./wgd.sh install
|
||||
```
|
||||
|
||||
|
||||
|
||||
Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol)
|
||||
|
||||
## 🥘 Experimental Functions
|
||||
|
||||
#### Progressive Web App (PWA) for WGDashboard
|
||||
|
||||
- With `v3.0`, I've added a `manifest.json` into the dashboard, so user could add their dashboard as a PWA to their browser or mobile device.
|
||||
|
||||
<img src="img/PWA.gif"/>
|
||||
|
||||
|
||||
|
||||
## 🔍 Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
<p align=center>Index Page</p>
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
<p align=center>Signin Page</p>
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
<p align=center>Configuration Page</p>
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
<p align=center>Settings Page</p>
|
||||

|
||||
|
||||
## Contributors ✨
|
||||

|
||||
|
||||
## ⏰ Changelog
|
||||
|
||||
#### v3.0.0 - v3.0.6.2 - Jan 18, 2022
|
||||
|
||||
- 🎉 **New Features**
|
||||
- **Moved from TinyDB to SQLite**: SQLite provide a better performance and loading speed when getting peers! Also avoided crashing the database due to **race condition**.
|
||||
- **Added Gunicorn WSGI Server**: This could provide more stable on handling HTTP request, and more flexibility in the future (such as HTTPS support). **BIG THANKS to @pgalonza :heart:**
|
||||
- **Add Peers by Bulk:** User can add peers by bulk, just simply set the amount and click add.
|
||||
- **Delete Peers by Bulk**: User can delete peers by bulk, without deleting peers one by one.
|
||||
- **Download Peers in Zip**: User can download all *downloadable* peers in a zip.
|
||||
- **Added Pre-shared Key to peers:** Now each peer can add with a pre-shared key to enhance security. Previously added peers can add the pre-shared key through the peer setting button.
|
||||
- **Redirect Back to Previous Page:** The dashboard will now redirect you back to your previous page if the current session got timed out and you need to sign in again.
|
||||
- **Added Some [🥘 Experimental Functions](#-experimental-functions)**
|
||||
|
||||
- 🪚 **Bug Fixed**
|
||||
- [IP Sorting range issues #99](https://github.com/donaldzou/WGDashboard/issues/99) [❤️ @barryboom]
|
||||
- [INvalid character written to tunnel json file #108](https://github.com/donaldzou/WGDashboard/issues/108) [❤️ @ikidd]
|
||||
- [Add IPv6 #91](https://github.com/donaldzou/WGDashboard/pull/91) [❤️ @pgalonza]
|
||||
- [Added MTU and PersistentKeepalive to QR code and download files #112](https://github.com/donaldzou/WGDashboard/pull/112) [:heart: @reafian]
|
||||
- **And many other bugs provided by our beloved users** :heart:
|
||||
- **🧐 Other Changes**
|
||||
- **Key generating moved to front-end**: No longer need to use the server's WireGuard to generate keys, thanks to the `wireguard.js` from the [official repository](https://git.zx2c4.com/wireguard-tools/tree/contrib/keygen-html/wireguard.js)!
|
||||
- **Peer transfer calculation**: each peer will now show all transfer amount (previously was only showing transfer amount from the last configuration start-up).
|
||||
- **UI adjustment on running peers**: peers will have a new style indicating that it is running.
|
||||
- **`wgd.sh` finally can update itself**: So now user could update the whole dashboard from `wgd.sh`, with the `update` command.
|
||||
- **Minified JS and CSS files**: Although only a small changes on the file size, but I think is still a good practice to save a bit of bandwidth ;)
|
||||
|
||||
*And many other small changes for performance and bug fixes! :laughing:*
|
||||
|
||||
#### v2.3.1 - Sep 8, 2021
|
||||
|
||||
- Updated dashboard's name to **WGDashboard**!!
|
||||
|
||||
#### v2.3 - Sep 8, 2021
|
||||
|
||||
- 🎉 **New Features**
|
||||
- **Update directly from `wgd.sh`:** Now you can update WGDashboard directly from the bash script.
|
||||
- **Displaying Peers:** You can switch the display mode between list and table in the configuration page.
|
||||
- 🪚 **Bug Fixed**
|
||||
- [Peer DNS Validation Fails #67](issues/67): Added DNS format check. [❤️ @realfian]
|
||||
- [configparser.NoSectionError: No section: 'Interface' #66](issues/66): Changed permission requirement for `etc/wireguard` from `744` to `755`. [❤️ @ramalmaty]
|
||||
- [Feature request: Interface not loading when information missing #73](issues/73): Fixed when Configuration Address and Listen Port is missing will crash the dashboard. [❤️ @js32]
|
||||
- [Remote Peer, MTU and PersistentKeepalives added #70](pull/70): Added MTU, remote peer and Persistent Keepalive. [❤️ @realfian]
|
||||
- [Fixes DNS check to support search domain #65](pull/65): Added allow input domain into DNS. [❤️@davejlong]
|
||||
- **🧐 Other Changes**
|
||||
- Moved Add Peer Button into the right bottom corner.
|
||||
|
||||
#### v2.2.1 - Aug 16, 2021
|
||||
|
||||
Bug Fixed:
|
||||
- Added support for full subnet on Allowed IP
|
||||
- Peer setting Save button
|
||||
|
||||
#### v2.2 - Aug 14, 2021
|
||||
|
||||
- 🎉 **New Features**
|
||||
- **Add new peers**: Now you can add peers directly on dashboard, it will generate a pair of private key and public key. You can also set its DNS, endpoint allowed IPs. Both can set a default value in the setting page. [❤️ in [#44](https://github.com/donaldzou/wireguard-dashboard/issues/44)]
|
||||
- **QR Code:** You can add the private key in peer setting of your existed peer to create a QR code. Or just create a new one, dashboard will now be able to auto generate a private key and public key ;) Don't worry, all keys will be generated on your machine, and **will delete all key files after they got generated**. [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
|
||||
- **Peer configuration file download:** Same as QR code, you now can download the peer configuration file, so you don't need to manually input all the details on the peer machine! [❤️ in [#40](https://github.com/donaldzou/wireguard-dashboard/issues/40)]
|
||||
- **Search peers**: You can now search peers by their name.
|
||||
- **Autostart on boot:** Added a tutorial on how to start the dashboard to on boot! Please read the [tutorial below](#autostart-wireguard-dashboard-on-boot). [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
|
||||
- **Click to copy**: You can now click and copy all peer's public key and configuration's public key.
|
||||
- ....
|
||||
- 🪚 **Bug Fixed**
|
||||
- When there are comments in the wireguard config file, will cause the dashboard to crash.
|
||||
- Used regex to search for config files.
|
||||
- **🧐 Other Changes**
|
||||
- Moved all external CSS and JavaScript file to local hosting (Except Bootstrap Icon, due to large amount of SVG files).
|
||||
- Updated Python dependencies
|
||||
- Flask: `v1.1.2 => v2.0.1`
|
||||
- Jinja: `v2.10.1 => v3.0.1`
|
||||
- icmplib: `v2.1.1 => v3.0.1`
|
||||
- Updated CSS/JS dependencies
|
||||
- Bootstrap: `v4.5.3 => v4.6.0`
|
||||
- UI adjustment
|
||||
- Adjusted how peers will display in larger screens, used to be 1 row per peer, now is 3 peers in 1 row.
|
||||
|
||||
#### v2.1 - Jul 2, 2021
|
||||
|
||||
- Added **Ping** and **Traceroute** tools!
|
||||
- Adjusted the calculation of data usage on each peers
|
||||
- Added refresh interval of the dashboard
|
||||
- Bug fixed when no configuration on fresh install ([#23](https://github.com/donaldzou/wireguard-dashboard/issues/23))
|
||||
- Fixed crash when too many peers ([#22](https://github.com/donaldzou/wireguard-dashboard/issues/22))
|
||||
|
||||
#### v2.0 - May 5, 2021
|
||||
|
||||
- Added login function to dashboard
|
||||
- ***I'm not using the most ideal way to store the username and password, feel free to provide a better way to do this if you any good idea!***
|
||||
- Added a config file to the dashboard
|
||||
- Dashboard config can be change within the **Setting** tab on the side bar
|
||||
- Adjusted UI
|
||||
- And much more!
|
||||
|
||||
#### v1.1.2 - Apr 3, 2021
|
||||
|
||||
- Resolved issue [#3](https://github.com/donaldzou/wireguard-dashboard/issues/3).
|
||||
|
||||
#### v1.1.1 - Apr 2, 2021
|
||||
|
||||
- Able to add a friendly name to each peer. Thanks [#2](https://github.com/donaldzou/wireguard-dashboard/issues/2) !
|
||||
|
||||
#### v1.0 - Dec 27, 2020
|
||||
|
||||
- Added the function to remove peers
|
||||
|
||||
## 🛒 Dependencies
|
||||
|
||||
- CSS/JS
|
||||
- [Bootstrap](https://getbootstrap.com/docs/4.6/getting-started/introduction/) `v4.6.0`
|
||||
- [Bootstrap Icon](https://icons.getbootstrap.com) `v1.4.0`
|
||||
- [jQuery](https://jquery.com) `v3.5.1`
|
||||
- Python
|
||||
- [Flask](https://pypi.org/project/Flask/) `v2.0.1`
|
||||
- [ifcfg](https://pypi.org/project/ifcfg/) `v0.21`
|
||||
- [icmplib](https://pypi.org/project/icmplib/) `v2.1.1`
|
||||
- [flask-qrcode](https://pypi.org/project/Flask-QRcode/) `v3.0.0`
|
||||
|
||||
## ✨ Contributors
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
@@ -178,8 +546,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/antonioag95"><img src="https://avatars.githubusercontent.com/u/30556866?v=4?s=100" width="100px;" alt=""/><br /><sub><b>antonioag95</b></sub></a><br /><a href="https://github.com/donaldzou/wireguard-dashboard/commits?author=antonioag95" title="Tests">⚠️</a> <a href="https://github.com/donaldzou/wireguard-dashboard/commits?author=antonioag95" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/tonjo"><img src="https://avatars.githubusercontent.com/u/4726289?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tonjo</b></sub></a><br /><a href="https://github.com/donaldzou/wireguard-dashboard/commits?author=tonjo" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/antonioag95"><img src="https://avatars.githubusercontent.com/u/30556866?v=4?s=100" width="100px;" alt=""/><br /><sub><b>antonioag95</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=antonioag95" title="Tests">⚠️</a> <a href="https://github.com/donaldzou/WGDashboard/commits?author=antonioag95" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/tonjo"><img src="https://avatars.githubusercontent.com/u/4726289?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tonjo</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=tonjo" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/reafian"><img src="https://avatars.githubusercontent.com/u/11992416?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Richard Newton</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=reafian" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.davejlong.com"><img src="https://avatars.githubusercontent.com/u/175317?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Long</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=davejlong" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.std-soft.com"><img src="https://avatars.githubusercontent.com/u/5978293?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Markus Neubauer</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=marneu" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -188,5 +559,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
19
docker-compose.yml
Normal file
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
||||
# API Documents for WGDashboard
|
||||
|
||||
**Version: v4.0**
|
||||
|
||||
**Created by: Donald Zou**
|
||||
|
||||
<hr>
|
||||
|
0
docs/changelogs.md
Normal file
16
entrypoint.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# if [ -z "$(ls -A /etc/wireguard)" ]; then
|
||||
# mv /wg0.conf /etc/wireguard
|
||||
# echo "Moved conf file to /etc/wireguard"
|
||||
# else
|
||||
# rm wg0.conf
|
||||
# echo "Removed unneeded conf file"
|
||||
# fi
|
||||
|
||||
# wg-quick up wg0
|
||||
chmod u+x /opt/wgdashboard/wgd.sh
|
||||
if [ ! -f "/opt/wgdashboard/wg-dashboard.ini" ]; then
|
||||
/opt/wgdashboard/wgd.sh install
|
||||
fi
|
||||
/opt/wgdashboard/wgd.sh debug
|
BIN
img/AddPeer.png
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
img/Configuration.png
Normal file
After Width: | Height: | Size: 181 KiB |
BIN
img/DashboardSetting.png
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
img/DeleteBulk.png
Normal file
After Width: | Height: | Size: 210 KiB |
BIN
img/DeletePeer.png
Normal file
After Width: | Height: | Size: 211 KiB |
BIN
img/EditPeer.png
Normal file
After Width: | Height: | Size: 149 KiB |
BIN
img/Group 2.png
Before Width: | Height: | Size: 49 KiB |
BIN
img/Group 3.png
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 221 KiB |
BIN
img/HomePage.png
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
img/PWA.gif
Normal file
After Width: | Height: | Size: 5.5 MiB |
BIN
img/Ping.png
Normal file
After Width: | Height: | Size: 229 KiB |
BIN
img/QRCode.png
Normal file
After Width: | Height: | Size: 239 KiB |
BIN
img/SearchIP.png
Normal file
After Width: | Height: | Size: 153 KiB |
BIN
img/SignIn.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
img/Traceroute.png
Normal file
After Width: | Height: | Size: 240 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 198 KiB |
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
17
package-lock.json
generated
Normal 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
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.12"
|
||||
}
|
||||
}
|
244
src/api.py
Normal 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
@@ -0,0 +1,7 @@
|
||||
for ((i = 0 ; i<$1 ; i++ ))
|
||||
do
|
||||
privateKey=$(wg genkey)
|
||||
presharedKey=$(wg genkey)
|
||||
publicKey=$(wg pubkey <<< "$privateKey")
|
||||
echo "$privateKey,$publicKey,$presharedKey"
|
||||
done
|
4
src/certbot.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
authenticator = standalone
|
||||
noninteractive = true
|
||||
agree-tos = true
|
||||
rsa-key-size = 2048
|
2515
src/dashboard.py
@@ -1 +0,0 @@
|
||||
You can delete this later ;)
|
26
src/gunicorn.conf.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import dashboard
|
||||
from datetime import datetime
|
||||
|
||||
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger
|
||||
app_host, app_port = dashboard.gunicornConfig()
|
||||
date = datetime.today().strftime('%Y_%m_%d_%H_%M_%S')
|
||||
|
||||
|
||||
def post_worker_init(worker):
|
||||
dashboard.startThreads()
|
||||
|
||||
|
||||
worker_class = 'gthread'
|
||||
workers = 1
|
||||
threads = 1
|
||||
bind = f"{app_host}:{app_port}"
|
||||
daemon = True
|
||||
pidfile = './gunicorn.pid'
|
||||
wsgi_app = "dashboard:app"
|
||||
accesslog = f"./log/access_{date}.log"
|
||||
log_level = "debug"
|
||||
capture_output = True
|
||||
errorlog = f"./log/error_{date}.log"
|
||||
print(f"[WGDashboard] WGDashboard w/ Gunicorn will be running on {bind}", flush=True)
|
||||
print(f"[WGDashboard] Access log file is at {accesslog}", flush=True)
|
||||
print(f"[WGDashboard] Error log file is at {errorlog}", flush=True)
|
@@ -1,3 +1,8 @@
|
||||
Flask==1.1.2
|
||||
tinydb==4.3.0
|
||||
ifcfg==0.21
|
||||
bcrypt
|
||||
ifcfg
|
||||
psutil
|
||||
pyotp
|
||||
Flask
|
||||
flask-cors
|
||||
icmplib
|
||||
gunicorn
|
BIN
src/static/.DS_Store
vendored
30
src/static/app/.gitignore
vendored
Normal 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/*
|
BIN
src/static/app/dist/assets/bootstrap-icons.woff
vendored
Normal file
BIN
src/static/app/dist/assets/bootstrap-icons.woff2
vendored
Normal file
15
src/static/app/dist/assets/index.css
vendored
Normal file
68
src/static/app/dist/assets/index.js
vendored
Normal file
BIN
src/static/app/dist/favicon.png
vendored
Normal file
After Width: | Height: | Size: 180 KiB |
14
src/static/app/dist/index.html
vendored
Normal 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
@@ -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>
|
8
src/static/app/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
4124
src/static/app/package-lock.json
generated
Normal file
36
src/static/app/package.json
Normal 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"
|
||||
}
|
||||
}
|
BIN
src/static/app/public/favicon.png
Normal file
After Width: | Height: | Size: 180 KiB |
37
src/static/app/src/App.vue
Normal 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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
125
src/static/app/src/components/configurationComponents/peer.vue
Normal 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>
|
@@ -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>
|
@@ -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>
|
@@ -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}} •
|
||||
</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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
64
src/static/app/src/components/configurationList.vue
Normal 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>
|
@@ -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>
|
@@ -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>
|
79
src/static/app/src/components/navbar.vue
Normal 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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
119
src/static/app/src/components/setupComponent/totp.vue
Normal 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>
|
26
src/static/app/src/main.js
Normal 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')
|
18
src/static/app/src/models/WireguardConfigurations.js
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
157
src/static/app/src/router/index.js
Normal 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
|
43
src/static/app/src/stores/DashboardConfigurationStore.js
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
83
src/static/app/src/stores/WireguardConfigurationsStore.js
Normal 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
|
||||
},
|
||||
|
||||
}
|
||||
});
|
20
src/static/app/src/stores/wgdashboardStore.js
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
17
src/static/app/src/test.py
Normal 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)
|