mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2025-10-04 08:16:17 +00:00
Compare commits
86 Commits
v3.0.5
...
v4.0.beta1
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
84c9846f7b |
40
.gitignore
vendored
40
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
.vscode/sftp.json
|
||||
src/.vscode/sftp.json
|
||||
.vscode
|
||||
.DS_Store
|
||||
.idea
|
||||
src/db
|
||||
@@ -15,4 +14,39 @@ venv/**
|
||||
log/**
|
||||
release/*
|
||||
src/db/wgdashboard.db
|
||||
.jshintrc
|
||||
.jshintrc
|
||||
node_modules/**
|
||||
*/proxy.js
|
||||
src/static/app/proxy.js
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
proxy.js
|
||||
.vite/*
|
28
Dockerfile
Normal file
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
|
103
README.md
103
README.md
@@ -10,40 +10,35 @@
|
||||
<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"><small>Note: This project is not affiliate to the official WireGuard Project ;)</small></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: v3.0
|
||||
## 📣 What's New: v4.0
|
||||
|
||||
> I can't thank enough for all of you who wait for this release, and for those who are new to this project, welcome :) Also, huge thanks who sponsored me GitHub :heart:
|
||||
|
||||
- 🎉 **New Features**
|
||||
- **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:
|
||||
- **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**
|
||||
- **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 ;)
|
||||
- **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.
|
||||
|
||||
*And many other small changes for performance and bug fixes! :laughing:*
|
||||
|
||||
> If you have any other brilliant ideas for this project, please shout it in here [#129](https://github.com/donaldzou/WGDashboard/issues/129) :heart:
|
||||
|
||||
**For users who is using `v2.x.x` please be sure to read [this](#please-note-for-user-who-is-using-v231-or-below) before updating WGDashboard ;)**
|
||||
**For users who is using `v2.x.x` please be sure to read [this](#please-note-for-user-who-is-using-v231-or-below) before updating WGDashboard**
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -68,19 +63,23 @@
|
||||
|
||||
## 💡 Features
|
||||
|
||||
- **No need to re-configure existing WireGuard configuration! It can search for existed configuration files.**
|
||||
- Easy to use interface, provided username and password protection to the dashboard
|
||||
- Add peers and edit (Allowed IPs, DNS, Private Key...)
|
||||
- View peers and configuration real time details (Data Usage, Latest Handshakes...)
|
||||
- Share your peer configuration with QR code or file download
|
||||
- Testing tool: Ping and Traceroute to your peer's ip
|
||||
- **And more functions are coming up!**
|
||||
- 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
|
||||
|
||||
|
||||
## 📝 Requirement
|
||||
|
||||
- Recommend the following OS, tested by our beloved users:
|
||||
- [x] Ubuntu 18.04.1 LTS - 20.04.1 LTS [@Me]
|
||||
- [x] Ubuntu 18.04.1 LTS, 20.04.1 LTS, 22.04.4 LTS [@Me]
|
||||
- [x] Debian GNU/Linux 10 (buster) [❤️ @[robchez](https://github.com/robchez)]
|
||||
- [x] AlmaLinux 8.4 (Electric Cheetah) [❤️ @[barry-smithjr](https://github.com/)]
|
||||
- [x] CentOS 7 [❤️ @[PrzemekSkw](https://github.com/PrzemekSkw)]
|
||||
@@ -115,7 +114,7 @@
|
||||
1. Download WGDashboard
|
||||
|
||||
```shell
|
||||
git clone -b v3.0.5 https://github.com/donaldzou/WGDashboard.git wgdashboard
|
||||
git clone -b v4.0 https://github.com/donaldzou/WGDashboard.git wgdashboard
|
||||
|
||||
2. Open the WGDashboard folder
|
||||
|
||||
@@ -139,7 +138,7 @@
|
||||
5. Run WGDashboard
|
||||
|
||||
```shell
|
||||
./wgd.sh start
|
||||
sudo ./wgd.sh start
|
||||
```
|
||||
|
||||
**Note**:
|
||||
@@ -262,7 +261,7 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
|
||||
└─6602 /usr/bin/python3 /root/wgdashboard/src/dashboard.py
|
||||
|
||||
Aug 03 22:31:26 ubuntu-wg systemd[1]: Started wg-dashboard.service.
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Serving Flask app "WGDashboard" (lazy loading)
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Serving Flask app1 "WGDashboard" (lazy loading)
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Environment: production
|
||||
Aug 03 22:31:27 ubuntu-wg python3[6602]: 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.
|
||||
@@ -420,6 +419,33 @@ Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol)
|
||||
|
||||
## ⏰ 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**!!
|
||||
@@ -535,5 +561,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
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
|
16
entrypoint.sh
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
|
17
package-lock.json
generated
Normal file
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
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.12"
|
||||
}
|
||||
}
|
244
src/api.py
Normal file
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
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
|
3386
src/dashboard.py
3386
src/dashboard.py
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,12 @@
|
||||
import multiprocessing
|
||||
import dashboard
|
||||
|
||||
app_host, app_port = dashboard.get_host_bind()
|
||||
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger
|
||||
app_host, app_port = dashboard.gunicornConfig()
|
||||
|
||||
worker_class = 'gthread'
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
threads = 4
|
||||
workers = 1
|
||||
threads = 1
|
||||
bind = f"{app_host}:{app_port}"
|
||||
daemon = True
|
||||
pidfile = './gunicorn.pid'
|
||||
pidfile = './gunicorn.pid'
|
@@ -1,6 +1,8 @@
|
||||
Flask
|
||||
bcrypt
|
||||
ifcfg
|
||||
psutil
|
||||
pyotp
|
||||
Flask
|
||||
flask-cors
|
||||
icmplib
|
||||
flask-qrcode
|
||||
gunicorn
|
||||
certbot
|
||||
gunicorn
|
BIN
src/static/.DS_Store
vendored
BIN
src/static/.DS_Store
vendored
Binary file not shown.
30
src/static/app/.gitignore
vendored
Normal file
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.woff
vendored
Normal file
Binary file not shown.
BIN
src/static/app/dist/assets/bootstrap-icons.woff2
vendored
Normal file
BIN
src/static/app/dist/assets/bootstrap-icons.woff2
vendored
Normal file
Binary file not shown.
15
src/static/app/dist/assets/index.css
vendored
Normal file
15
src/static/app/dist/assets/index.css
vendored
Normal file
File diff suppressed because one or more lines are too long
68
src/static/app/dist/assets/index.js
vendored
Normal file
68
src/static/app/dist/assets/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
src/static/app/dist/favicon.png
vendored
Normal file
BIN
src/static/app/dist/favicon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 180 KiB |
14
src/static/app/dist/index.html
vendored
Normal file
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
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
8
src/static/app/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
4100
src/static/app/package-lock.json
generated
Normal file
4100
src/static/app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
src/static/app/package.json
Normal file
35
src/static/app/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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
BIN
src/static/app/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 180 KiB |
35
src/static/app/src/App.vue
Normal file
35
src/static/app/src/App.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<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="fade2" mode="out-in">
|
||||
<Component :is="Component"></Component>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-enter-active,
|
||||
.app-leave-active {
|
||||
transition: all 0.3s ease-in-out;
|
||||
/*position: absolute;*/
|
||||
/*padding-top: 50px*/
|
||||
}
|
||||
|
||||
.app-enter-from,
|
||||
.app-leave-to {
|
||||
transform: translateX(-30px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,135 @@
|
||||
<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
124
src/static/app/src/components/configurationComponents/peer.vue
Normal file
124
src/static/app/src/components/configurationComponents/peer.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<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')"
|
||||
: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,611 @@
|
||||
<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";
|
||||
|
||||
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: {
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
watch: {
|
||||
'$route': {
|
||||
immediate: true,
|
||||
handler(){
|
||||
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
|
||||
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
|
||||
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
|
||||
|
||||
this.loading = true;
|
||||
let id = this.$route.params.id;
|
||||
this.configurationInfo = [];
|
||||
this.configurationPeers = [];
|
||||
if (id){
|
||||
this.getPeers(id)
|
||||
console.log("Changed..")
|
||||
this.setPeerInterval();
|
||||
}
|
||||
}
|
||||
},
|
||||
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
|
||||
console.log("Changed?")
|
||||
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))
|
||||
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
configurationSummary(){
|
||||
return {
|
||||
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length,
|
||||
totalUsage: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b) : 0,
|
||||
totalReceive: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b) : 0,
|
||||
totalSent: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b) : 0
|
||||
}
|
||||
},
|
||||
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.toFixed(4)}} 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.toFixed(4)}} 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.toFixed(4)}} 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"
|
||||
|
||||
@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>
|
||||
</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,36 @@
|
||||
<script>
|
||||
import QRCode from "qrcode";
|
||||
export default {
|
||||
name: "peerQRCode",
|
||||
props: {
|
||||
peerConfigData: String
|
||||
},
|
||||
mounted() {
|
||||
QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfigData , (error) => {
|
||||
console.log(this.peerConfigData)
|
||||
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,191 @@
|
||||
<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";
|
||||
|
||||
export default {
|
||||
name: "schedulePeerJob",
|
||||
components: {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')
|
||||
}
|
||||
},
|
||||
}
|
||||
</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>
|
||||
<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;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,168 @@
|
||||
<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>
|
||||
|
||||
<!-- <RouterLink-->
|
||||
<!-- to="jobs"-->
|
||||
<!-- class="text-decoration-none btn btn-primary rounded-3 btn-sm">-->
|
||||
<!-- <i class="bi bi-app-indicator me-2"></i>Jobs-->
|
||||
<!-- </RouterLink>-->
|
||||
<button class="btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm"
|
||||
@click="this.downloadAllPeer()">
|
||||
<i class="bi bi-download me-2"></i> Download All
|
||||
</button>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<!-- <label class="d-flex me-2 text-muted" for="searchPeers"><i class="bi bi-search me-1"></i></label>-->
|
||||
<input class="form-control rounded-3 bg-secondary-subtle border-1 border-secondary-subtle shadow-sm"
|
||||
placeholder="Search..."
|
||||
id="searchPeers"
|
||||
@keyup="this.debounce()"
|
||||
v-model="this.searchString">
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
|
||||
type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-filter-circle me-2"></i>
|
||||
Sort
|
||||
</button>
|
||||
<ul class="dropdown-menu mt-2 shadow rounded-3">
|
||||
<li v-for="(value, key) in this.sort">
|
||||
<a class="dropdown-item d-flex" role="button" @click="this.updateSort(key)">
|
||||
<span class="me-auto">{{value}}</span>
|
||||
<i class="bi bi-check text-primary"
|
||||
v-if="store.Configuration.Server.dashboard_sort === key"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
|
||||
type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Refresh Interval
|
||||
</button>
|
||||
<ul class="dropdown-menu shadow mt-2 rounded-3">
|
||||
<li v-for="(value, key) in this.interval">
|
||||
<a class="dropdown-item d-flex" role="button" @click="updateRefreshInterval(key)">
|
||||
<span class="me-auto">{{value}}</span>
|
||||
<i class="bi bi-check text-primary"
|
||||
v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i>
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
|
||||
type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-three-dots me-2"></i>More
|
||||
</button>
|
||||
<ul class="dropdown-menu shadow mt-2 rounded-3">
|
||||
<li>
|
||||
<h6 class="dropdown-header">Peer Jobs</h6>
|
||||
</li>
|
||||
<li>
|
||||
<a role="button" class="dropdown-item" @click="this.$emit('jobsAll')">
|
||||
All Active Jobs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a role="button" class="dropdown-item" @click="this.$emit('jobLogs')">
|
||||
Logs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -0,0 +1,194 @@
|
||||
<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")
|
||||
})
|
||||
}
|
||||
},
|
||||
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">
|
||||
<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>
|
||||
<small class="text-muted">Public Key</small><br>
|
||||
<small><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>
|
||||
<hr>
|
||||
<div class="accordion mt-2" 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>
|
||||
</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">
|
||||
Reset <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,168 @@
|
||||
<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>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
</template>
|
||||
<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>
|
||||
<template v-if="this.Peer.private_key">
|
||||
<li>
|
||||
<a class="dropdown-item d-flex" role="button" @click="this.downloadPeer()">
|
||||
<i class="me-auto bi bi-download"></i> Download
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex" role="button"
|
||||
@click="this.downloadQRCode()"
|
||||
>
|
||||
<i class="me-auto bi bi-qr-code"></i> QR Code
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<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,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>
|
56
src/static/app/src/components/configurationList.vue
Normal file
56
src/static/app/src/components/configurationList.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="container">
|
||||
<div class="d-flex mb-4 ">
|
||||
<h3 class="text-body">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" 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,83 @@
|
||||
<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 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>
|
||||
</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>
|
68
src/static/app/src/components/navbar.vue
Normal file
68
src/static/app/src/components/navbar.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<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">
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link" to="/" exact-active-class="active">Home</RouterLink></li>
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link" to="/settings"
|
||||
exact-active-class="active">Settings</RouterLink></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Configurations</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link"
|
||||
active-class="active"
|
||||
|
||||
v-for="c in this.wireguardConfigurationsStore.Configurations">
|
||||
<samp>{{c.Name}}</samp>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Tools</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/ping" class="nav-link" active-class="active">Ping</RouterLink></li>
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/traceroute" class="nav-link" active-class="active">Traceroute</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link text-danger" @click="this.dashboardConfigurationStore.signOut()" role="button" style="font-weight: bold">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,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,84 @@
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
export default {
|
||||
name: "newDashboardAPIKey",
|
||||
data(){
|
||||
return{
|
||||
newKeyData:{
|
||||
ExpiredAt: dayjs().add(1, 'd').format("YYYY-MM-DDTHH: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")
|
||||
}
|
||||
}
|
||||
}
|
||||
</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">
|
||||
Create API Key
|
||||
<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">
|
||||
<input class="form-control" type="datetime-local"
|
||||
@change="this.newKeyData.ExpiredAt = this.fixDate(this.newKeyData.ExpiredAt)"
|
||||
:disabled="this.newKeyData.neverExpire || this.submitting"
|
||||
v-model="this.newKeyData.ExpiredAt">
|
||||
</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>
|
87
src/static/app/src/components/setupComponent/totp.vue
Normal file
87
src/static/app/src/components/setupComponent/totp.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script>
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
export default {
|
||||
name: "totp",
|
||||
async setup(){
|
||||
let l = ""
|
||||
await fetchGet("/api/Welcome_GetTotpLink", {}, (res => {
|
||||
if (res.status) l = res.data;
|
||||
}));
|
||||
return {l}
|
||||
},
|
||||
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="mb-3">
|
||||
<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">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
25
src/static/app/src/main.js
Normal file
25
src/static/app/src/main.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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 {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
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
|
||||
});
|
||||
}
|
||||
}
|
144
src/static/app/src/router/index.js
Normal file
144
src/static/app/src/router/index.js
Normal file
@@ -0,0 +1,144 @@
|
||||
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 {wgdashboardStore} from "@/stores/wgdashboardStore.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 PeerSettings from "@/components/configurationComponents/peerSettings.vue";
|
||||
import PeerList from "@/components/configurationComponents/peerList.vue";
|
||||
import PeerCreate from "@/components/configurationComponents/peerCreate.vue";
|
||||
import RestrictedPeers from "@/components/configurationComponents/restrictedPeers.vue";
|
||||
import Ping from "@/views/ping.vue";
|
||||
import Traceroute from "@/views/traceroute.vue";
|
||||
import PeerJobs from "@/components/configurationComponents/peerJobs.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
|
||||
},
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
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")
|
||||
}
|
||||
}else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
export default router
|
43
src/static/app/src/stores/DashboardConfigurationStore.js
Normal file
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
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
82
src/static/app/src/stores/WireguardConfigurationsStore.js
Normal file
82
src/static/app/src/stores/WireguardConfigurationsStore.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import isCidr from "is-cidr";
|
||||
|
||||
export const WireguardConfigurationsStore = defineStore('WireguardConfigurationsStore', {
|
||||
state: () => ({
|
||||
Configurations: undefined,
|
||||
searchString: "",
|
||||
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
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
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)
|
9
src/static/app/src/utilities/cookie.js
Normal file
9
src/static/app/src/utilities/cookie.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const cookie = {
|
||||
|
||||
//https://stackoverflow.com/a/15724300
|
||||
getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
}
|
||||
}
|
31
src/static/app/src/utilities/fetch.js
Normal file
31
src/static/app/src/utilities/fetch.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export const fetchGet = async (url, params=undefined, callback=undefined) => {
|
||||
const urlSearchParams = new URLSearchParams(params);
|
||||
await fetch(`${url}?${urlSearchParams.toString()}`, {
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(x => x.json())
|
||||
.then(x => callback ? callback(x) : undefined)
|
||||
.catch(x => {
|
||||
// let router = useRouter()
|
||||
// if (x.status === 401){
|
||||
// router.push('/signin')
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchPost = async (url, body, callback) => {
|
||||
await fetch(`${url}`, {
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(x => x.json())
|
||||
.then(x => callback ? callback(x) : undefined)
|
||||
// .catch(() => {
|
||||
// alert("Error occurred! Check console")
|
||||
// });
|
||||
}
|
3
src/static/app/src/utilities/ipCheck.js
Normal file
3
src/static/app/src/utilities/ipCheck.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const ipV46RegexCheck = (input) => {
|
||||
|
||||
}
|
313
src/static/app/src/utilities/wireguard.js
Normal file
313
src/static/app/src/utilities/wireguard.js
Normal file
@@ -0,0 +1,313 @@
|
||||
/*! SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
function gf(init) {
|
||||
var r = new Float64Array(16);
|
||||
if (init) {
|
||||
for (var i = 0; i < init.length; ++i)
|
||||
r[i] = init[i];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function pack(o, n) {
|
||||
var b, m = gf(), t = gf();
|
||||
for (var i = 0; i < 16; ++i)
|
||||
t[i] = n[i];
|
||||
carry(t);
|
||||
carry(t);
|
||||
carry(t);
|
||||
for (var j = 0; j < 2; ++j) {
|
||||
m[0] = t[0] - 0xffed;
|
||||
for (var i = 1; i < 15; ++i) {
|
||||
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
|
||||
m[i - 1] &= 0xffff;
|
||||
}
|
||||
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
|
||||
b = (m[15] >> 16) & 1;
|
||||
m[14] &= 0xffff;
|
||||
cswap(t, m, 1 - b);
|
||||
}
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[2 * i] = t[i] & 0xff;
|
||||
o[2 * i + 1] = t[i] >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
function carry(o) {
|
||||
var c;
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
|
||||
o[i] &= 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
function cswap(p, q, b) {
|
||||
var t, c = ~(b - 1);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
t = c & (p[i] ^ q[i]);
|
||||
p[i] ^= t;
|
||||
q[i] ^= t;
|
||||
}
|
||||
}
|
||||
|
||||
function add(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] + b[i]) | 0;
|
||||
}
|
||||
|
||||
function subtract(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] - b[i]) | 0;
|
||||
}
|
||||
|
||||
function multmod(o, a, b) {
|
||||
var t = new Float64Array(31);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
for (var j = 0; j < 16; ++j)
|
||||
t[i + j] += a[i] * b[j];
|
||||
}
|
||||
for (var i = 0; i < 15; ++i)
|
||||
t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = t[i];
|
||||
carry(o);
|
||||
carry(o);
|
||||
}
|
||||
|
||||
function invert(o, i) {
|
||||
var c = gf();
|
||||
for (var a = 0; a < 16; ++a)
|
||||
c[a] = i[a];
|
||||
for (var a = 253; a >= 0; --a) {
|
||||
multmod(c, c, c);
|
||||
if (a !== 2 && a !== 4)
|
||||
multmod(c, c, i);
|
||||
}
|
||||
for (var a = 0; a < 16; ++a)
|
||||
o[a] = c[a];
|
||||
}
|
||||
|
||||
function clamp(z) {
|
||||
z[31] = (z[31] & 127) | 64;
|
||||
z[0] &= 248;
|
||||
}
|
||||
|
||||
function generatePublicKey(privateKey) {
|
||||
var r, z = new Uint8Array(32);
|
||||
var a = gf([1]),
|
||||
b = gf([9]),
|
||||
c = gf(),
|
||||
d = gf([1]),
|
||||
e = gf(),
|
||||
f = gf(),
|
||||
_121665 = gf([0xdb41, 1]),
|
||||
_9 = gf([9]);
|
||||
for (var i = 0; i < 32; ++i)
|
||||
z[i] = privateKey[i];
|
||||
clamp(z);
|
||||
for (var i = 254; i >= 0; --i) {
|
||||
r = (z[i >>> 3] >>> (i & 7)) & 1;
|
||||
cswap(a, b, r);
|
||||
cswap(c, d, r);
|
||||
add(e, a, c);
|
||||
subtract(a, a, c);
|
||||
add(c, b, d);
|
||||
subtract(b, b, d);
|
||||
multmod(d, e, e);
|
||||
multmod(f, a, a);
|
||||
multmod(a, c, a);
|
||||
multmod(c, b, e);
|
||||
add(e, a, c);
|
||||
subtract(a, a, c);
|
||||
multmod(b, a, a);
|
||||
subtract(c, d, f);
|
||||
multmod(a, c, _121665);
|
||||
add(a, a, d);
|
||||
multmod(c, c, a);
|
||||
multmod(a, d, f);
|
||||
multmod(d, b, _9);
|
||||
multmod(b, e, e);
|
||||
cswap(a, b, r);
|
||||
cswap(c, d, r);
|
||||
}
|
||||
invert(c, c);
|
||||
multmod(a, a, c);
|
||||
pack(z, a);
|
||||
return z;
|
||||
}
|
||||
|
||||
function generatePresharedKey() {
|
||||
var privateKey = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
function generatePrivateKey() {
|
||||
var privateKey = generatePresharedKey();
|
||||
clamp(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
function encodeBase64(dest, src) {
|
||||
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
|
||||
for (var i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 65 +
|
||||
(((25 - input[i]) >> 8) & 6) -
|
||||
(((51 - input[i]) >> 8) & 75) -
|
||||
(((61 - input[i]) >> 8) & 15) +
|
||||
(((62 - input[i]) >> 8) & 3);
|
||||
}
|
||||
|
||||
function keyToBase64(key) {
|
||||
var i, base64 = new Uint8Array(44);
|
||||
for (i = 0; i < 32 / 3; ++i)
|
||||
encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
|
||||
encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
|
||||
base64[43] = 61;
|
||||
return String.fromCharCode.apply(null, base64);
|
||||
}
|
||||
|
||||
function base64ToKey(base64) {
|
||||
let binary_string = window.atob(base64);
|
||||
let len = binary_string.length;
|
||||
let bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
let uint8 = new Uint8Array(bytes.buffer);
|
||||
return uint8;
|
||||
}
|
||||
|
||||
function putU32(b, n)
|
||||
{
|
||||
b.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff);
|
||||
}
|
||||
|
||||
function putU16(b, n)
|
||||
{
|
||||
b.push(n & 0xff, (n >>> 8) & 0xff);
|
||||
}
|
||||
|
||||
function putBytes(b, a)
|
||||
{
|
||||
for (var i = 0; i < a.length; ++i)
|
||||
b.push(a[i] & 0xff);
|
||||
}
|
||||
|
||||
function encodeString(s)
|
||||
{
|
||||
var utf8 = unescape(encodeURIComponent(s));
|
||||
var b = new Uint8Array(utf8.length);
|
||||
for (var i = 0; i < utf8.length; ++i)
|
||||
b[i] = utf8.charCodeAt(i);
|
||||
return b;
|
||||
}
|
||||
|
||||
function crc32(b)
|
||||
{
|
||||
if (!crc32.table) {
|
||||
crc32.table = [];
|
||||
for (var c = 0, n = 0; n < 256; c = ++n) {
|
||||
for (var k = 0; k < 8; ++k)
|
||||
c = ((c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1));
|
||||
crc32.table[n] = c;
|
||||
}
|
||||
}
|
||||
var crc = -1;
|
||||
for (var i = 0; i < b.length; ++i)
|
||||
crc = (crc >>> 8) ^ crc32.table[(crc ^ b[i]) & 0xff];
|
||||
return (crc ^ (-1)) >>> 0;
|
||||
}
|
||||
|
||||
function createZipFile(files)
|
||||
{
|
||||
var b = [];
|
||||
var cd = [];
|
||||
var offset = 0;
|
||||
|
||||
for (var i = 0; i < files.length; ++i) {
|
||||
var name = encodeString(files[i].fileName);
|
||||
var contents = encodeString(files[i].file);
|
||||
var crc = crc32(contents);
|
||||
|
||||
putU32(b, 0x04034b50); /* signature */
|
||||
putU16(b, 20); /* version needed */
|
||||
putU16(b, 0); /* flags */
|
||||
putU16(b, 0); /* compression method */
|
||||
putU16(b, 0); /* mtime */
|
||||
putU16(b, 0); /* mdate */
|
||||
putU32(b, crc); /* crc32 */
|
||||
putU32(b, contents.length); /* compressed size */
|
||||
putU32(b, contents.length); /* uncompressed size */
|
||||
putU16(b, name.length); /* file name length */
|
||||
putU16(b, 0); /* extra field length */
|
||||
putBytes(b, name);
|
||||
putBytes(b, contents);
|
||||
|
||||
putU32(cd, 0x02014b50); /* signature */
|
||||
putU16(cd, 0); /* version made */
|
||||
putU16(cd, 20); /* version needed */
|
||||
putU16(cd, 0); /* flags */
|
||||
putU16(cd, 0); /* compression method */
|
||||
putU16(cd, 0); /* mtime */
|
||||
putU16(cd, 0); /* mdate */
|
||||
putU32(cd, crc); /* crc32 */
|
||||
putU32(cd, contents.length); /* compressed size */
|
||||
putU32(cd, contents.length); /* uncompressed size */
|
||||
putU16(cd, name.length); /* file name length */
|
||||
putU16(cd, 0); /* extra field length */
|
||||
putU16(cd, 0); /* file comment length */
|
||||
putU16(cd, 0); /* disk number start */
|
||||
putU16(cd, 0); /* internal file attributes */
|
||||
putU32(cd, 32); /* external file attributes - 'archive' bit set (32) */
|
||||
putU32(cd, offset); /* relative offset of local header */
|
||||
putBytes(cd, name); /* file name */
|
||||
|
||||
offset += 30 + contents.length + name.length
|
||||
}
|
||||
putBytes(b, cd); /* central directory */
|
||||
putU32(b, 0x06054b50); /* end of central directory signature */
|
||||
putU16(b, 0); /* number of this disk */
|
||||
putU16(b, 0); /* number of disk with central directory start */
|
||||
putU16(b, files.length); /* number of entries on disk */
|
||||
putU16(b, files.length); /* number of entries */
|
||||
putU32(b, cd.length); /* length of central directory */
|
||||
putU32(b, offset); /* offset to start of central directory */
|
||||
putU16(b, 0); /* zip comment size */
|
||||
return Uint8Array.from(b);
|
||||
}
|
||||
|
||||
window.wireguard = {
|
||||
generateKeypair: function() {
|
||||
var privateKey = generatePrivateKey();
|
||||
var publicKey = generatePublicKey(privateKey);
|
||||
var presharedKey = generatePresharedKey();
|
||||
return {
|
||||
publicKey: keyToBase64(publicKey),
|
||||
privateKey: keyToBase64(privateKey),
|
||||
presharedKey: keyToBase64(presharedKey)
|
||||
};
|
||||
},
|
||||
generatePublicKey: function (privateKey){
|
||||
privateKey = base64ToKey(privateKey);
|
||||
return keyToBase64(generatePublicKey(privateKey));
|
||||
},
|
||||
|
||||
generateZipFiles: function(res, zipFileName){
|
||||
var files = res.data;
|
||||
var zipFile = createZipFile(files);
|
||||
var blob = new Blob([zipFile], { type: "application/zip" });
|
||||
var a = document.createElement("a");
|
||||
a.download = zipFileName;
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
};
|
||||
})();
|
21
src/static/app/src/views/configuration.vue
Normal file
21
src/static/app/src/views/configuration.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
export default {
|
||||
name: "configuration",
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-5 text-body">
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition name="fade2" mode="out-in">
|
||||
<Suspense>
|
||||
<Component :is="Component" :key="route.path"></Component>
|
||||
</Suspense>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
51
src/static/app/src/views/index.vue
Normal file
51
src/static/app/src/views/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
import Navbar from "@/components/navbar.vue";
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import {WireguardConfigurations} from "@/models/WireguardConfigurations.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import Message from "@/components/messageCentreComponent/message.vue";
|
||||
|
||||
export default {
|
||||
name: "index",
|
||||
components: {Message, Navbar},
|
||||
async setup(){
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
return {dashboardConfigurationStore}
|
||||
},
|
||||
computed: {
|
||||
getMessages(){
|
||||
return this.dashboardConfigurationStore.Messages.filter(x => x.show)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid flex-grow-1 main" :data-bs-theme="this.dashboardConfigurationStore.Configuration.Server.dashboard_theme">
|
||||
<div class="row h-100">
|
||||
<Navbar></Navbar>
|
||||
<main class="col-md-9 ml-sm-auto col-lg-10 px-md-4 overflow-y-scroll mb-0" style="height: calc(100vh - 50px)">
|
||||
<Suspense>
|
||||
<RouterView v-slot="{Component}">
|
||||
<Transition name="fade2" mode="out-in">
|
||||
<Component :is="Component"></Component>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</Suspense>
|
||||
<div class="messageCentre text-body position-fixed">
|
||||
<TransitionGroup name="message" tag="div" class="position-relative">
|
||||
<Message v-for="m in getMessages.slice().reverse()"
|
||||
:message="m" :key="m.id"></Message>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.messageCentre{
|
||||
top: calc(50px + 1rem);
|
||||
right: 1rem;
|
||||
}
|
||||
</style>
|
296
src/static/app/src/views/newConfiguration.vue
Normal file
296
src/static/app/src/views/newConfiguration.vue
Normal file
@@ -0,0 +1,296 @@
|
||||
<script>
|
||||
import {parse} from "cidr-tools";
|
||||
import '@/utilities/wireguard.js'
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "newConfiguration",
|
||||
setup(){
|
||||
const store = WireguardConfigurationsStore()
|
||||
return {store}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
newConfiguration: {
|
||||
ConfigurationName: "",
|
||||
Address: "",
|
||||
ListenPort: "",
|
||||
PrivateKey: "",
|
||||
PublicKey: "",
|
||||
PresharedKey: "",
|
||||
PreUp: "",
|
||||
PreDown: "",
|
||||
PostUp: "",
|
||||
PostDown: ""
|
||||
},
|
||||
numberOfAvailableIPs: "0",
|
||||
error: false,
|
||||
errorMessage: "",
|
||||
success: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.wireguardGenerateKeypair();
|
||||
},
|
||||
methods: {
|
||||
wireguardGenerateKeypair(){
|
||||
const wg = window.wireguard.generateKeypair();
|
||||
this.newConfiguration.PrivateKey = wg.privateKey;
|
||||
this.newConfiguration.PublicKey = wg.publicKey;
|
||||
this.newConfiguration.PresharedKey = wg.presharedKey;
|
||||
},
|
||||
async saveNewConfiguration(){
|
||||
if (this.goodToSubmit){
|
||||
this.loading = true;
|
||||
await fetchPost("/api/addWireguardConfiguration", this.newConfiguration, async (res) => {
|
||||
if (res.status){
|
||||
this.success = true
|
||||
await this.store.getConfigurations()
|
||||
setTimeout(() => {
|
||||
this.$router.push('/')
|
||||
}, 1000)
|
||||
}else{
|
||||
this.error = true;
|
||||
this.errorMessage = res.message;
|
||||
document.querySelector(`#${res.data}`).classList.remove("is-valid")
|
||||
document.querySelector(`#${res.data}`).classList.add("is-invalid")
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
goodToSubmit(){
|
||||
let requirements = ["ConfigurationName", "Address", "ListenPort", "PrivateKey"]
|
||||
let elements = [...document.querySelectorAll("input[required]")];
|
||||
return requirements.find(x => {
|
||||
return this.newConfiguration[x].length === 0
|
||||
}) === undefined && elements.find(x => {
|
||||
return x.classList.contains("is-invalid")
|
||||
}) === undefined
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'newConfiguration.Address'(newVal){
|
||||
let ele = document.querySelector("#Address");
|
||||
ele.classList.remove("is-invalid", "is-valid")
|
||||
try{
|
||||
if (newVal.trim().split("/").filter(x => x.length > 0).length !== 2){
|
||||
throw Error()
|
||||
}
|
||||
let p = parse(newVal);
|
||||
let i = p.end - p.start;
|
||||
this.numberOfAvailableIPs = i.toLocaleString();
|
||||
ele.classList.add("is-valid")
|
||||
}catch (e) {
|
||||
this.numberOfAvailableIPs = "0";
|
||||
ele.classList.add("is-invalid")
|
||||
}
|
||||
},
|
||||
'newConfiguration.ListenPort'(newVal){
|
||||
let ele = document.querySelector("#ListenPort");
|
||||
ele.classList.remove("is-invalid", "is-valid")
|
||||
|
||||
if (newVal < 0 || newVal > 65353 || !Number.isInteger(newVal)){
|
||||
ele.classList.add("is-invalid")
|
||||
}else{
|
||||
ele.classList.add("is-valid")
|
||||
}
|
||||
},
|
||||
'newConfiguration.ConfigurationName'(newVal){
|
||||
|
||||
let ele = document.querySelector("#ConfigurationName");
|
||||
ele.classList.remove("is-invalid", "is-valid")
|
||||
if (!/^[a-zA-Z0-9_=+.-]{1,15}$/.test(newVal) || newVal.length === 0 || this.store.Configurations.find(x => x.Name === newVal)){
|
||||
ele.classList.add("is-invalid")
|
||||
}else{
|
||||
ele.classList.add("is-valid")
|
||||
}
|
||||
},
|
||||
'newConfiguration.PrivateKey'(newVal){
|
||||
let ele = document.querySelector("#PrivateKey");
|
||||
ele.classList.remove("is-invalid", "is-valid")
|
||||
|
||||
try{
|
||||
wireguard.generatePublicKey(newVal)
|
||||
ele.classList.add("is-valid")
|
||||
}catch (e) {
|
||||
ele.classList.add("is-invalid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="container mb-4">
|
||||
<div class="mb-4 d-flex align-items-center gap-4">
|
||||
<RouterLink to="/">
|
||||
<h3 class="mb-0 text-body">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</h3>
|
||||
</RouterLink>
|
||||
<h3 class="text-body mb-0">New Configuration</h3>
|
||||
</div>
|
||||
|
||||
<form class="text-body d-flex flex-column gap-3"
|
||||
@submit="(e) => {e.preventDefault(); this.saveNewConfiguration();}"
|
||||
>
|
||||
<div class="card rounded-3 shadow">
|
||||
<div class="card-header">Configuration Name</div>
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control" placeholder="ex. wg1" id="ConfigurationName"
|
||||
v-model="this.newConfiguration.ConfigurationName"
|
||||
:disabled="this.loading"
|
||||
required>
|
||||
<div class="invalid-feedback">
|
||||
<div v-if="this.error">{{this.errorMessage}}</div>
|
||||
<div v-else>
|
||||
Configuration name is invalid. Possible reasons:
|
||||
<ul class="mb-0">
|
||||
<li>Configuration name already exist.</li>
|
||||
<li>Configuration name can only contain 15 lower/uppercase alphabet, numbers, "_"(underscore), "="(equal), "+"(plus), "."(period/dot), "-"(dash/hyphen)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3 shadow">
|
||||
<div class="card-header">Private Key / Public Key / Pre-Shared Key</div>
|
||||
<div class="card-body" style="font-family: var(--bs-font-monospace)">
|
||||
<div class="mb-2">
|
||||
<label class="text-muted fw-bold mb-1"><small>PRIVATE KEY</small></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="PrivateKey" required
|
||||
:disabled="this.loading"
|
||||
v-model="this.newConfiguration.PrivateKey" disabled
|
||||
>
|
||||
<button class="btn btn-outline-primary" type="button"
|
||||
title="Regenerate Private Key"
|
||||
@click="wireguardGenerateKeypair()"
|
||||
>
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted fw-bold mb-1"><small>PUBLIC KEY</small></label>
|
||||
<input type="text" class="form-control" id="PublicKey"
|
||||
v-model="this.newConfiguration.PublicKey" disabled
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card rounded-3 shadow">
|
||||
<div class="card-header">Listen Port</div>
|
||||
<div class="card-body">
|
||||
<input type="number" class="form-control" placeholder="0-65353" id="ListenPort"
|
||||
min="1"
|
||||
max="65353"
|
||||
v-model="this.newConfiguration.ListenPort"
|
||||
:disabled="this.loading"
|
||||
required>
|
||||
<div class="invalid-feedback">
|
||||
<div v-if="this.error">{{this.errorMessage}}</div>
|
||||
<div v-else>
|
||||
Invalid port
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3 shadow">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
IP Address & Range
|
||||
<span class="badge rounded-pill text-bg-success ms-auto">{{ numberOfAvailableIPs }} Available IPs</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Ex: 10.0.0.1/24" id="Address"
|
||||
v-model="this.newConfiguration.Address"
|
||||
:disabled="this.loading"
|
||||
required>
|
||||
<div class="invalid-feedback">
|
||||
<div v-if="this.error">{{this.errorMessage}}</div>
|
||||
<div v-else>
|
||||
IP address & range is invalid.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="accordion" id="newConfigurationOptionalAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#newConfigurationOptionalAccordionCollapse">
|
||||
Optional Settings
|
||||
</button>
|
||||
</h2>
|
||||
<div id="newConfigurationOptionalAccordionCollapse"
|
||||
class="accordion-collapse collapse" data-bs-parent="#newConfigurationOptionalAccordion">
|
||||
<div class="accordion-body d-flex flex-column gap-3">
|
||||
<div class="card rounded-3">
|
||||
<div class="card-header">PreUp</div>
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control" id="preUp" v-model="this.newConfiguration.PreUp">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3">
|
||||
<div class="card-header">PreDown</div>
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control" id="preDown" v-model="this.newConfiguration.PreDown">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3">
|
||||
<div class="card-header">PostUp</div>
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control" id="postUp" v-model="this.newConfiguration.PostUp">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3">
|
||||
<div class="card-header">PostDown</div>
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control" id="postDown" v-model="this.newConfiguration.PostDown">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <RouterLink to="/new_configuration" class="btn btn-success rounded-3 shadow ms-auto rounded-3">-->
|
||||
<!-- <i class="bi bi-save me-2"></i>-->
|
||||
<!-- Save-->
|
||||
<!-- </RouterLink>-->
|
||||
<button class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto"
|
||||
:disabled="!this.goodToSubmit">
|
||||
<span v-if="this.success" class="d-flex w-100">
|
||||
Success! <i class="bi bi-check-circle-fill ms-2"></i>
|
||||
</span>
|
||||
<span v-else-if="!this.loading" class="d-flex w-100">
|
||||
Save Configuration <i class="bi bi-save-fill ms-2"></i>
|
||||
</span>
|
||||
<span v-else class="d-flex w-100 align-items-center">
|
||||
Saving...
|
||||
<span class="ms-2 spinner-border spinner-border-sm" role="status">
|
||||
<!-- <span class="visually-hidden">Loading...</span>-->
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
197
src/static/app/src/views/ping.vue
Normal file
197
src/static/app/src/views/ping.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<script>
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
export default {
|
||||
name: "ping",
|
||||
data(){
|
||||
return {
|
||||
loading: false,
|
||||
cips: {},
|
||||
selectedConfiguration: undefined,
|
||||
selectedPeer: undefined,
|
||||
selectedIp: undefined,
|
||||
count: 4,
|
||||
pingResult: undefined,
|
||||
pinging: false
|
||||
}
|
||||
},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
return {store}
|
||||
},
|
||||
mounted() {
|
||||
fetchGet("/api/ping/getAllPeersIpAddress", {}, (res)=> {
|
||||
if (res.status){
|
||||
this.loading = true;
|
||||
this.cips = res.data;
|
||||
console.log(this.cips)
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
execute(){
|
||||
if (this.selectedIp){
|
||||
this.pinging = true;
|
||||
this.pingResult = undefined
|
||||
fetchGet("/api/ping/execute", {
|
||||
ipAddress: this.selectedIp,
|
||||
count: this.count
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.pingResult = res.data
|
||||
}else{
|
||||
this.store.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
})
|
||||
}
|
||||
{} }
|
||||
},
|
||||
watch: {
|
||||
selectedConfiguration(){
|
||||
this.selectedPeer = undefined;
|
||||
this.selectedIp = undefined;
|
||||
},
|
||||
selectedPeer(){
|
||||
this.selectedIp = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-5 text-body">
|
||||
<div class="container">
|
||||
<h3 class="mb-3 text-body">Ping</h3>
|
||||
<div class="row">
|
||||
<div class="col-sm-4 d-flex gap-2 flex-column">
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="configuration">
|
||||
<small>Configuration</small></label>
|
||||
<select class="form-select" v-model="this.selectedConfiguration">
|
||||
<option disabled selected :value="undefined">Select a Configuration...</option>
|
||||
<option :value="key" v-for="(val, key) in this.cips">
|
||||
{{key}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="peer">
|
||||
<small>Peer</small></label>
|
||||
<select id="peer" class="form-select" v-model="this.selectedPeer" :disabled="this.selectedConfiguration === undefined">
|
||||
<option disabled selected :value="undefined">Select a Peer...</option>
|
||||
<option v-if="this.selectedConfiguration !== undefined" :value="key" v-for="(peer, key) in
|
||||
this.cips[this.selectedConfiguration]">
|
||||
{{key}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="ip">
|
||||
<small>IP Address</small></label>
|
||||
<select id="ip" class="form-select" v-model="this.selectedIp" :disabled="this.selectedPeer === undefined">
|
||||
<option disabled selected :value="undefined">Select a IP...</option>
|
||||
<option
|
||||
v-if="this.selectedPeer !== undefined"
|
||||
v-for="ip in this.cips[this.selectedConfiguration][this.selectedPeer].allowed_ips">
|
||||
{{ip}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="count">
|
||||
<small>Ping Count</small></label>
|
||||
<input class="form-control" type="number"
|
||||
v-model="this.count"
|
||||
min="1" id="count" placeholder="How many times you want to ping?">
|
||||
</div>
|
||||
<button class="btn btn-primary rounded-3 mt-3"
|
||||
:disabled="!this.selectedIp"
|
||||
@click="this.execute()">
|
||||
<i class="bi bi-person-walking me-2"></i>Go!
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-8">
|
||||
<TransitionGroup name="ping">
|
||||
<div v-if="!this.pingResult" key="pingPlaceholder">
|
||||
<div class="pingPlaceholder bg-body-secondary rounded-3 mb-3"
|
||||
:class="{'animate__animated animate__flash animate__slower animate__infinite': this.pinging}"
|
||||
:style="{'animation-delay': `${x*0.15}s`}"
|
||||
v-for="x in 4" ></div>
|
||||
</div>
|
||||
|
||||
<div v-else key="pingResult" class="d-flex flex-column gap-2 w-100">
|
||||
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.15s">
|
||||
<div class="card-body">
|
||||
<p class="mb-0 text-muted"><small>Address</small></p>
|
||||
{{this.pingResult.address}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.3s">
|
||||
<div class="card-body">
|
||||
<p class="mb-0 text-muted"><small>Is Alive</small></p>
|
||||
<span :class="[this.pingResult.is_alive ? 'text-success':'text-danger']">
|
||||
<i class="bi me-1"
|
||||
:class="[this.pingResult.is_alive ? 'bi-check-circle-fill' : 'bi-x-circle-fill']"></i>
|
||||
{{this.pingResult.is_alive ? "Yes": "No"}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.45s">
|
||||
<div class="card-body">
|
||||
<p class="mb-0 text-muted"><small>Average / Min / Max Round Trip Time</small></p>
|
||||
<samp>{{this.pingResult.avg_rtt}}ms /
|
||||
{{this.pingResult.min_rtt}}ms /
|
||||
{{this.pingResult.max_rtt}}ms
|
||||
</samp>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.6s">
|
||||
<div class="card-body">
|
||||
<p class="mb-0 text-muted"><small>Sent / Received / Lost Package</small></p>
|
||||
<samp>{{this.pingResult.package_sent}} /
|
||||
{{this.pingResult.package_received}} /
|
||||
{{this.pingResult.package_loss}}
|
||||
</samp>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pingPlaceholder{
|
||||
width: 100%;
|
||||
height: 79.98px;
|
||||
}
|
||||
.ping-move, /* apply transition to moving elements */
|
||||
.ping-enter-active,
|
||||
.ping-leave-active {
|
||||
transition: all 0.4s cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
.ping-leave-active{
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ping-enter-from,
|
||||
.ping-leave-to {
|
||||
opacity: 0;
|
||||
//transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* ensure leaving items are taken out of layout flow so that moving
|
||||
animations can be calculated correctly. */
|
||||
.ping-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
87
src/static/app/src/views/settings.vue
Normal file
87
src/static/app/src/views/settings.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script>
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import PeersDefaultSettingsInput from "@/components/settingsComponent/peersDefaultSettingsInput.vue";
|
||||
import {ipV46RegexCheck} from "@/utilities/ipCheck.js";
|
||||
import AccountSettingsInputUsername from "@/components/settingsComponent/accountSettingsInputUsername.vue";
|
||||
import AccountSettingsInputPassword from "@/components/settingsComponent/accountSettingsInputPassword.vue";
|
||||
import DashboardSettingsInputWireguardConfigurationPath
|
||||
from "@/components/settingsComponent/dashboardSettingsInputWireguardConfigurationPath.vue";
|
||||
import DashboardTheme from "@/components/settingsComponent/dashboardTheme.vue";
|
||||
import DashboardSettingsInputIPAddressAndPort
|
||||
from "@/components/settingsComponent/dashboardSettingsInputIPAddressAndPort.vue";
|
||||
import DashboardAPIKeys from "@/components/settingsComponent/dashboardAPIKeys.vue";
|
||||
|
||||
export default {
|
||||
name: "settings",
|
||||
methods: {ipV46RegexCheck},
|
||||
components: {
|
||||
DashboardAPIKeys,
|
||||
DashboardSettingsInputIPAddressAndPort,
|
||||
DashboardTheme,
|
||||
DashboardSettingsInputWireguardConfigurationPath,
|
||||
AccountSettingsInputPassword, AccountSettingsInputUsername, PeersDefaultSettingsInput},
|
||||
setup(){
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
return {dashboardConfigurationStore}
|
||||
},
|
||||
watch: {
|
||||
// 'dashboardConfigurationStore.Configuration': {
|
||||
// deep: true,
|
||||
// handler(){
|
||||
// this.dashboardConfigurationStore.updateConfiguration();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-5">
|
||||
<div class="container">
|
||||
<h3 class="mb-3 text-body">Settings</h3>
|
||||
<DashboardTheme></DashboardTheme>
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<p class="card-header">Peers Default Settings</p>
|
||||
<div class="card-body">
|
||||
<PeersDefaultSettingsInput targetData="peer_global_dns" title="DNS"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="peer_endpoint_allowed_ip" title="Peer Endpoint Allowed IPs"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="peer_mtu" title="MTU (Max Transmission Unit)"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="remote_endpoint" title="Peer Remote Endpoint"
|
||||
:warning="true" warningText="This will be change globally, and will be apply to all peer's QR code and configuration file."
|
||||
></PeersDefaultSettingsInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<p class="card-header">WireGuard Configurations Settings</p>
|
||||
<div class="card-body">
|
||||
<DashboardSettingsInputWireguardConfigurationPath
|
||||
targetData="wg_conf_path"
|
||||
title="Configurations Directory"
|
||||
:warning="true"
|
||||
warning-text="Remember to remove <code>/</code> at the end of your path. e.g <code>/etc/wireguard</code>"
|
||||
>
|
||||
</DashboardSettingsInputWireguardConfigurationPath>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<p class="card-header">Account Settings</p>
|
||||
<div class="card-body">
|
||||
<AccountSettingsInputUsername targetData="username"
|
||||
title="Username"
|
||||
></AccountSettingsInputUsername>
|
||||
<hr>
|
||||
<AccountSettingsInputPassword
|
||||
targetData="password">
|
||||
</AccountSettingsInputPassword>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardAPIKeys></DashboardAPIKeys>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
130
src/static/app/src/views/setup.vue
Normal file
130
src/static/app/src/views/setup.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import QRCode from 'qrcode'
|
||||
import Totp from "@/components/setupComponent/totp.vue";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
export default {
|
||||
name: "setup",
|
||||
components: {Totp},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
return {store}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
setup: {
|
||||
username: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: "",
|
||||
enable_totp: false,
|
||||
verified_totp: false
|
||||
},
|
||||
loading: false,
|
||||
errorMessage: "",
|
||||
done: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
goodToSubmit(){
|
||||
return this.setup.username
|
||||
&& this.setup.newPassword.length >= 8
|
||||
&& this.setup.repeatNewPassword.length >= 8
|
||||
&& this.setup.newPassword === this.setup.repeatNewPassword
|
||||
&& ((this.setup.enable_totp && this.setup.verified_totp) || !this.setup.enable_totp)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit(){
|
||||
this.loading = true
|
||||
fetchPost("/api/Welcome_Finish", this.setup, (res) => {
|
||||
if (res.status){
|
||||
this.done = true;
|
||||
setTimeout(() => {
|
||||
this.$router.push('/')
|
||||
}, 500)
|
||||
}else{
|
||||
document.querySelectorAll("#createAccount input").forEach(x => x.classList.add("is-invalid"))
|
||||
this.errorMessage = res.message;
|
||||
document.querySelector(".login-container-fluid")
|
||||
.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
|
||||
:data-bs-theme="this.store.Configuration.Server.dashboard_theme">
|
||||
<div class="mx-auto text-body" style="width: 500px">
|
||||
<span class="dashboardLogo display-4">Nice to meet you!</span>
|
||||
<p class="mb-5">Please fill in the following fields to finish setup 😊</p>
|
||||
<div>
|
||||
<h3>Create an account</h3>
|
||||
<div class="alert alert-danger" v-if="this.errorMessage">
|
||||
{{this.errorMessage}}
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div id="createAccount" class="d-flex flex-column gap-2">
|
||||
<div class="form-group text-body">
|
||||
<label for="username" class="mb-1 text-muted">
|
||||
<small>Pick an username you like</small></label>
|
||||
<input type="text"
|
||||
v-model="this.setup.username"
|
||||
class="form-control" id="username" name="username" placeholder="Maybe something like 'wiredragon'?" required>
|
||||
</div>
|
||||
<div class="form-group text-body">
|
||||
<label for="password" class="mb-1 text-muted">
|
||||
<small>Create a password (at least 8 characters)</small></label>
|
||||
<input type="password"
|
||||
v-model="this.setup.newPassword"
|
||||
class="form-control" id="password" name="password" placeholder="Make sure is strong enough" required>
|
||||
</div>
|
||||
<div class="form-group text-body">
|
||||
<label for="confirmPassword" class="mb-1 text-muted">
|
||||
<small>Confirm password</small></label>
|
||||
<input type="password"
|
||||
v-model="this.setup.repeatNewPassword"
|
||||
class="form-control" id="confirmPassword" name="confirmPassword" placeholder="and you can remember it :)" required>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="enable_totp"
|
||||
v-model="this.setup.enable_totp">
|
||||
<label class="form-check-label"
|
||||
for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label>
|
||||
</div>
|
||||
<Suspense>
|
||||
<Transition name="fade">
|
||||
<Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp>
|
||||
</Transition>
|
||||
</Suspense>
|
||||
|
||||
<button class="btn btn-dark btn-lg mb-5 d-flex btn-brand shadow align-items-center"
|
||||
ref="signInBtn"
|
||||
:disabled="!this.goodToSubmit || this.loading || this.done" @click="this.submit()">
|
||||
<span class="d-flex align-items-center w-100" v-if="!this.loading && !this.done">
|
||||
Finish<i class="bi bi-chevron-right ms-auto"></i></span>
|
||||
<span class="d-flex align-items-center w-100" v-else-if="this.done">
|
||||
Welcome to WGDashboard!</span>
|
||||
<span class="d-flex align-items-center w-100" v-else>
|
||||
Saving...<span class="spinner-border ms-auto spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
133
src/static/app/src/views/signin.vue
Normal file
133
src/static/app/src/views/signin.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script>
|
||||
import {fetchGet, fetchPost} from "../utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
export default {
|
||||
name: "signin",
|
||||
async setup(){
|
||||
const store = DashboardConfigurationStore()
|
||||
let theme = ""
|
||||
let totpEnabled = false;
|
||||
await fetchGet("/api/getDashboardTheme", {}, (res) => {
|
||||
theme = res.data
|
||||
});
|
||||
await fetchGet("/api/isTotpEnabled", {}, (res) => {
|
||||
totpEnabled = res.data
|
||||
});
|
||||
return {store, theme, totpEnabled}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
username: "",
|
||||
password: "",
|
||||
totp: "",
|
||||
loginError: false,
|
||||
loginErrorMessage: "",
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async auth(){
|
||||
if (this.username && this.password && ((this.totpEnabled && this.totp) || !this.totpEnabled)){
|
||||
this.loading = true
|
||||
await fetchPost("/api/authenticate", {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
totp: this.totp
|
||||
}, (response) => {
|
||||
if (response.status){
|
||||
this.loginError = false;
|
||||
this.$refs["signInBtn"].classList.add("signedIn")
|
||||
if (response.message){
|
||||
this.$router.push('/welcome')
|
||||
}else{
|
||||
if (this.store.Redirect !== undefined){
|
||||
this.$router.push(this.store.Redirect)
|
||||
}else{
|
||||
this.$router.push('/')
|
||||
}
|
||||
}
|
||||
}else{
|
||||
this.loginError = true;
|
||||
this.loginErrorMessage = response.message;
|
||||
document.querySelectorAll("input[required]").forEach(x => {
|
||||
x.classList.remove("is-valid")
|
||||
x.classList.add("is-invalid")
|
||||
});
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
})
|
||||
}else{
|
||||
document.querySelectorAll("input[required]").forEach(x => {
|
||||
if (x.value.length === 0){
|
||||
x.classList.remove("is-valid")
|
||||
x.classList.add("is-invalid")
|
||||
}else{
|
||||
x.classList.remove("is-invalid")
|
||||
x.classList.add("is-valid")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid login-container-fluid d-flex main flex-column" :data-bs-theme="this.theme">
|
||||
<div class="login-box m-auto" style="width: 500px;">
|
||||
<h4 class="mb-0 text-body">Welcome to</h4>
|
||||
<span class="dashboardLogo display-3">WGDashboard</span>
|
||||
<div class="m-auto">
|
||||
<div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError">
|
||||
{{this.loginErrorMessage}}
|
||||
</div>
|
||||
<form @submit="(e) => {e.preventDefault(); this.auth();}">
|
||||
<div class="form-group text-body">
|
||||
<label for="username" class="text-left" style="font-size: 1rem">
|
||||
<i class="bi bi-person-circle"></i></label>
|
||||
<input type="text" v-model="username" class="form-control" id="username" name="username"
|
||||
autocomplete="on"
|
||||
placeholder="Username" required>
|
||||
</div>
|
||||
<div class="form-group text-body">
|
||||
<label for="password" class="text-left" style="font-size: 1rem"><i class="bi bi-key-fill"></i></label>
|
||||
<input type="password"
|
||||
v-model="password" class="form-control" id="password" name="password"
|
||||
autocomplete="on"
|
||||
placeholder="Password" required>
|
||||
</div>
|
||||
<div class="form-group text-body" v-if="totpEnabled">
|
||||
<label for="totp" class="text-left" style="font-size: 1rem"><i class="bi bi-lock-fill"></i></label>
|
||||
<input class="form-control totp"
|
||||
required
|
||||
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
|
||||
placeholder="OTP from your authenticator"
|
||||
v-model="this.totp"
|
||||
>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand shadow signInBtn" ref="signInBtn">
|
||||
<span v-if="!this.loading" class="d-flex w-100">
|
||||
Sign In<i class="ms-auto bi bi-chevron-right"></i>
|
||||
</span>
|
||||
<span v-else class="d-flex w-100 align-items-center">
|
||||
Signing In...
|
||||
<span class="spinner-border ms-auto spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted pb-3 d-block w-100 text-center">
|
||||
WGDashboard v4.0 | Developed with ❤️ by
|
||||
<a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
136
src/static/app/src/views/traceroute.vue
Normal file
136
src/static/app/src/views/traceroute.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script>
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
|
||||
export default {
|
||||
name: "traceroute",
|
||||
data(){
|
||||
return {
|
||||
tracing: false,
|
||||
ipAddress: undefined,
|
||||
tracerouteResult: undefined
|
||||
}
|
||||
},
|
||||
setup(){
|
||||
const store = WireguardConfigurationsStore();
|
||||
return {store}
|
||||
},
|
||||
methods: {
|
||||
execute(){
|
||||
if (this.ipAddress){
|
||||
this.tracing = true;
|
||||
this.tracerouteResult = undefined
|
||||
fetchGet("/api/traceroute/execute", {
|
||||
ipAddress: this.ipAddress,
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.tracerouteResult = res.data
|
||||
}else{
|
||||
this.store.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
this.tracing = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-5 text-body">
|
||||
<div class="container">
|
||||
<h3 class="mb-3 text-body">Traceroute</h3>
|
||||
<div class="row">
|
||||
<div class="col-sm-4 d-flex gap-2 flex-column">
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="ipAddress">
|
||||
<small>IP Address</small></label>
|
||||
<input
|
||||
id="ipAddress"
|
||||
class="form-control"
|
||||
v-model="this.ipAddress"
|
||||
type="text" placeholder="Enter an IP Address you want to trace :)">
|
||||
</div>
|
||||
<button class="btn btn-primary rounded-3 mt-3"
|
||||
:disabled="!this.store.regexCheckIP(this.ipAddress) || this.tracing"
|
||||
@click="this.execute()">
|
||||
<i class="bi bi-bullseye me-2"></i> {{this.tracing ? "Tracing...":"Trace It!"}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-8 position-relative">
|
||||
<TransitionGroup name="ping">
|
||||
<div v-if="!this.tracerouteResult" key="pingPlaceholder">
|
||||
<div class="pingPlaceholder bg-body-secondary rounded-3 mb-3"
|
||||
:class="{'animate__animated animate__flash animate__slower animate__infinite': this.tracing}"
|
||||
:style="{'animation-delay': `${x*0.05}s`}"
|
||||
v-for="x in 10" ></div>
|
||||
</div>
|
||||
<div v-else key="table" class="w-100">
|
||||
<table class="table table-borderless rounded-3 w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Hop</th>
|
||||
<th scope="col">IP Address</th>
|
||||
<th scope="col">Average / Min / Max Round Trip Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(hop, key) in this.tracerouteResult"
|
||||
class="animate__fadeInUp animate__animated"
|
||||
:style="{'animation-delay': `${key * 0.05}s`}"
|
||||
>
|
||||
<td>{{hop.hop}}</td>
|
||||
<td>{{hop.ip}}</td>
|
||||
<td>{{hop.avg_rtt}} / {{hop.min_rtt}} / {{hop.max_rtt}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pingPlaceholder{
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
.ping-move, /* apply transition to moving elements */
|
||||
.ping-enter-active,
|
||||
.ping-leave-active {
|
||||
transition: all 0.4s cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
.ping-leave-active{
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ping-enter-from,
|
||||
.ping-leave-to {
|
||||
opacity: 0;
|
||||
//transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* ensure leaving items are taken out of layout flow so that moving
|
||||
animations can be calculated correctly. */
|
||||
.ping-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
table th, table td{
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
table tbody{
|
||||
border-top: 1em solid transparent;
|
||||
}
|
||||
|
||||
.table > :not(caption) > * > *{
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
32
src/static/app/vite.config.js
Normal file
32
src/static/app/vite.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import {proxy} from "./proxy.js";
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "/static/app/dist",
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server:{
|
||||
proxy: {
|
||||
'/api': proxy
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
7
src/static/css/bootstrap.min.css
vendored
7
src/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
1
src/static/css/dashboard.min.css
vendored
1
src/static/css/dashboard.min.css
vendored
File diff suppressed because one or more lines are too long
7045
src/static/js/bootstrap.bundle.js
vendored
7045
src/static/js/bootstrap.bundle.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
src/static/js/bootstrap.bundle.min.js
vendored
7
src/static/js/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
1
src/static/js/configuration.min.js
vendored
1
src/static/js/configuration.min.js
vendored
File diff suppressed because one or more lines are too long
929
src/static/js/configurationTool.js
Normal file
929
src/static/js/configurationTool.js
Normal file
@@ -0,0 +1,929 @@
|
||||
let $body = $("body");
|
||||
let available_ips = [];
|
||||
let $add_peer = document.getElementById("save_peer");
|
||||
|
||||
$("#configuration_delete").on("click", function(){
|
||||
configurations.configurationDeleteModal().toggle();
|
||||
});
|
||||
|
||||
function ajaxPostJSON(url, data, doneFunc){
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: "POST",
|
||||
data: JSON.stringify(data),
|
||||
headers: {"Content-Type": "application/json"}
|
||||
}).done(function (res) {
|
||||
doneFunc(res);
|
||||
});
|
||||
}
|
||||
|
||||
function ajaxGetJSON(url, doneFunc){
|
||||
$.ajax({
|
||||
url: url,
|
||||
headers: {"Content-Type": "application/json"}
|
||||
}).done(function (res) {
|
||||
doneFunc(res);
|
||||
});
|
||||
}
|
||||
|
||||
$("#sure_delete_configuration").on("click", function () {
|
||||
configurations.removeConfigurationInterval();
|
||||
let ele = $(this)
|
||||
ele.attr("disabled", "disabled");
|
||||
function done(res){
|
||||
if (res.status){
|
||||
$('#configuration_delete_modal button[data-dismiss="modal"]').remove();
|
||||
ele.text("Delete Successful! Redirecting in 5 seconds.");
|
||||
setTimeout(function(){
|
||||
window.location.replace('/');
|
||||
}, 5000)
|
||||
}else{
|
||||
$("#remove_configuration_alert").removeClass("d-none").text(res.reason);
|
||||
}
|
||||
}
|
||||
ajaxPostJSON("/api/deleteConfiguration", {"name": configurations.getConfigurationName()}, done);
|
||||
});
|
||||
|
||||
function loadPeerDataUsageChartDone(res){
|
||||
if (res.status === true){
|
||||
let t = new Date();
|
||||
let string = `${t.getDate()}/${t.getMonth()}/${t.getFullYear()} ${t.getHours() > 10 ? t.getHours():`0${t.getHours()}`}:${t.getMinutes() > 10 ? t.getMinutes():`0${t.getMinutes()}`}:${t.getSeconds() > 10 ? t.getSeconds():`0${t.getSeconds()}`}`;
|
||||
$(".peerDataUsageUpdateTime").html(`Updated on: ${string}`);
|
||||
|
||||
configurations.peerDataUsageChartObj().data.labels = [];
|
||||
configurations.peerDataUsageChartObj().data.datasets[0].data = [];
|
||||
configurations.peerDataUsageChartObj().data.datasets[1].data = [];
|
||||
console.log(res);
|
||||
let data = res.data;
|
||||
if (data.length > 0){
|
||||
configurations.peerDataUsageChartObj().data.labels.push(data[data.length - 1].time);
|
||||
configurations.peerDataUsageChartObj().data.datasets[0].data.push(0);
|
||||
configurations.peerDataUsageChartObj().data.datasets[1].data.push(0);
|
||||
|
||||
configurations.peerDataUsageChartObj().data.datasets[0].lastData = data[data.length - 1].total_sent
|
||||
configurations.peerDataUsageChartObj().data.datasets[1].lastData = data[data.length - 1].total_receive
|
||||
|
||||
|
||||
for(let i = data.length - 2; i >= 0; i--){
|
||||
let sent = data[i].total_sent - configurations.peerDataUsageChartObj().data.datasets[0].lastData;
|
||||
let receive = data[i].total_receive - configurations.peerDataUsageChartObj().data.datasets[1].lastData;
|
||||
configurations.peerDataUsageChartObj().data.datasets[0].data.push(sent);
|
||||
configurations.peerDataUsageChartObj().data.datasets[1].data.push(receive);
|
||||
configurations.peerDataUsageChartObj().data.labels.push(data[i].time);
|
||||
configurations.peerDataUsageChartObj().data.datasets[0].lastData = data[i].total_sent;
|
||||
configurations.peerDataUsageChartObj().data.datasets[1].lastData = data[i].total_receive;
|
||||
}
|
||||
configurations.peerDataUsageChartObj().update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let peerDataUsageInterval;
|
||||
|
||||
$body.on("click", ".btn-data-usage-peer", function(){
|
||||
configurations.peerDataUsageChartObj().data.peerID = $(this).data("peer-id");
|
||||
configurations.peerDataUsageModal().toggle();
|
||||
peerDataUsageInterval = setInterval(function(){
|
||||
ajaxPostJSON("/api/getPeerDataUsage", {"config": configurations.getConfigurationName(), "peerID": configurations.peerDataUsageChartObj().data.peerID, "interval": window.localStorage.getItem("peerTimePeriod")}, loadPeerDataUsageChartDone);
|
||||
}, 30000);
|
||||
ajaxPostJSON("/api/getPeerDataUsage", {"config": configurations.getConfigurationName(), "peerID": configurations.peerDataUsageChartObj().data.peerID, "interval": window.localStorage.getItem("peerTimePeriod")}, loadPeerDataUsageChartDone);
|
||||
});
|
||||
|
||||
$('#peerDataUsage').on('shown.bs.modal', function() {
|
||||
configurations.peerDataUsageChartObj().resize();
|
||||
}).on('hidden.bs.modal', function() {
|
||||
clearInterval(peerDataUsageInterval);
|
||||
configurations.peerDataUsageChartObj().data.peerID = "";
|
||||
configurations.peerDataUsageChartObj().data.labels = [];
|
||||
configurations.peerDataUsageChartObj().data.datasets[0].data = [];
|
||||
configurations.peerDataUsageChartObj().data.datasets[1].data = [];
|
||||
configurations.peerDataUsageChartObj().update();
|
||||
});
|
||||
|
||||
$(".switchTimePeriod").on("click", function(){
|
||||
let peerTimePeriod = window.localStorage.peerTimePeriod;
|
||||
$(".switchTimePeriod").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
if ($(this).data('time') !== peerTimePeriod){
|
||||
ajaxPostJSON("/api/getPeerDataUsage", {"config": configurations.getConfigurationName(), "peerID": configurations.peerDataUsageChartObj().data.peerID, "interval": $(this).data('time')}, loadPeerDataUsageChartDone);
|
||||
window.localStorage.peerTimePeriod = $(this).data('time');
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Edit Configuration
|
||||
*/
|
||||
|
||||
$editConfiguration = $("#edit_configuration");
|
||||
$editConfiguration.on("click", function(){
|
||||
configurations.getConfigurationDetails();
|
||||
configurations.configurationEditModal().toggle();
|
||||
});
|
||||
|
||||
$saveConfiguration = $("#editConfigurationBtn");
|
||||
$saveConfiguration.on("click", function(){
|
||||
$(this).html("Saving...")
|
||||
$(this).siblings().hide();
|
||||
$(this).attr("disabled", "disabled");
|
||||
|
||||
let data = {
|
||||
"configurationName": configurations.getConfigurationName(),
|
||||
"ListenPort": $("#editConfigurationListenPort").val(),
|
||||
"PostUp": $("#editConfigurationPostUp").val(),
|
||||
"PostDown": $("#editConfigurationPostDown").val(),
|
||||
"PreDown": $("#editConfigurationPreDown").val(),
|
||||
"PreUp": $("#editConfigurationPreUp").val(),
|
||||
}
|
||||
function done(res){
|
||||
console.log(res);
|
||||
$saveConfiguration.removeAttr("disabled");
|
||||
if (res.status){
|
||||
configurations.configurationEditModal().toggle();
|
||||
configurations.loadPeers("");
|
||||
showToast("Configuration saved");
|
||||
}else{
|
||||
showToast(res.reason);
|
||||
}
|
||||
$saveConfiguration.html("Save");
|
||||
$saveConfiguration.siblings().show();
|
||||
}
|
||||
ajaxPostJSON("/api/saveConfiguration", data, done);
|
||||
})
|
||||
|
||||
/**
|
||||
* ==========
|
||||
* Add peers
|
||||
* ==========
|
||||
*/
|
||||
|
||||
/**
|
||||
* Toggle add peers modal when add button clicked
|
||||
*/
|
||||
document.querySelector(".add_btn").addEventListener("click", () => {
|
||||
configurations.addModal().toggle();
|
||||
});
|
||||
|
||||
/**
|
||||
* When configuration switch got click
|
||||
*/
|
||||
$(".toggle--switch").on("change", function(){
|
||||
console.log('lol')
|
||||
$(this).addClass("waiting").attr("disabled", "disabled");
|
||||
let id = configurations.getConfigurationName();
|
||||
let status = $(this).prop("checked");
|
||||
let ele = $(this);
|
||||
$.ajax({
|
||||
url: `/switch/${id}`
|
||||
}).done(function(res){
|
||||
if (res.status){
|
||||
if (status){
|
||||
showToast(`${id} is running.`)
|
||||
}else{
|
||||
showToast(`${id} is stopped.`)
|
||||
}
|
||||
}else{
|
||||
if (status){
|
||||
ele.prop("checked", false)
|
||||
}else{
|
||||
ele.prop("checked", true)
|
||||
}
|
||||
showToast(res.reason, true);
|
||||
$(".index-alert").removeClass("d-none").text(`Configuration toggle failed. Please check the following error message:\n${res.message}`);
|
||||
}
|
||||
ele.removeClass("waiting");
|
||||
ele.removeAttr("disabled");
|
||||
configurations.loadPeers($('#search_peer_textbox').val())
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate Public key when private got change
|
||||
*/
|
||||
document.querySelector("#private_key").addEventListener("change", (event) => {
|
||||
let publicKey = document.querySelector("#public_key");
|
||||
if (event.target.value.length === 44) {
|
||||
publicKey.value = window.wireguard.generatePublicKey(event.target.value);
|
||||
publicKey.setAttribute("disabled", "disabled");
|
||||
} else {
|
||||
publicKey.attributes.removeNamedItem("disabled");
|
||||
publicKey.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when add modal is show and hide
|
||||
*/
|
||||
$('#add_modal').on('show.bs.modal', function() {
|
||||
configurations.generateKeyPair();
|
||||
configurations.getAvailableIps();
|
||||
}).on('hide.bs.modal', function() {
|
||||
$("#allowed_ips_indicator").html('');
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when user clicked the regenerate button
|
||||
*/
|
||||
$("#re_generate_key").on("click", function() {
|
||||
$("#public_key").attr("disabled", "disabled");
|
||||
$("#re_generate_key i").addClass("rotating");
|
||||
configurations.generateKeyPair();
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when user is editing in allowed ips textbox
|
||||
*/
|
||||
$("#allowed_ips").on("keyup", function() {
|
||||
let s = configurations.cleanIp($(this).val());
|
||||
s = s.split(",");
|
||||
if (available_ips.includes(s[s.length - 1])) {
|
||||
$("#allowed_ips_indicator").removeClass().addClass("text-success")
|
||||
.html('<i class="bi bi-check-circle-fill"></i>');
|
||||
} else {
|
||||
$("#allowed_ips_indicator").removeClass().addClass("text-warning")
|
||||
.html('<i class="bi bi-exclamation-circle-fill"></i>');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Change peer name when user typing in peer name textbox
|
||||
*/
|
||||
$("#peer_name_textbox").on("keyup", function() {
|
||||
$(".peer_name").html($(this).val());
|
||||
});
|
||||
|
||||
/**
|
||||
* When Add Peer button got clicked
|
||||
*/
|
||||
$add_peer.addEventListener("click", function() {
|
||||
let $bulk_add = $("#bulk_add");
|
||||
if ($bulk_add.prop("checked")) {
|
||||
if (!$("#new_add_amount").hasClass("is-invalid")) {
|
||||
configurations.addPeersByBulk();
|
||||
}
|
||||
} else {
|
||||
let $public_key = $("#public_key");
|
||||
let $private_key = $("#private_key");
|
||||
let $allowed_ips = $("#allowed_ips");
|
||||
$allowed_ips.val(configurations.cleanIp($allowed_ips.val()));
|
||||
let $new_add_DNS = $("#new_add_DNS");
|
||||
$new_add_DNS.val(configurations.cleanIp($new_add_DNS.val()));
|
||||
let $new_add_endpoint_allowed_ip = $("#new_add_endpoint_allowed_ip");
|
||||
$new_add_endpoint_allowed_ip.val(configurations.cleanIp($new_add_endpoint_allowed_ip.val()));
|
||||
let $new_add_name = $("#new_add_name");
|
||||
let $new_add_MTU = $("#new_add_MTU");
|
||||
let $new_add_keep_alive = $("#new_add_keep_alive");
|
||||
let $enable_preshare_key = $("#enable_preshare_key");
|
||||
$add_peer.setAttribute("disabled", "disabled");
|
||||
$add_peer.innerHTML = "Adding...";
|
||||
if ($allowed_ips.val() !== "" && $public_key.val() !== "" && $new_add_DNS.val() !== "" && $new_add_endpoint_allowed_ip.val() !== "") {
|
||||
let conf = configurations.getConfigurationName();
|
||||
let data_list = [$private_key, $allowed_ips, $new_add_name, $new_add_DNS, $new_add_endpoint_allowed_ip, $new_add_MTU, $new_add_keep_alive];
|
||||
data_list.forEach((ele) => ele.attr("disabled", "disabled"));
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: "/add_peer/" + conf,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: JSON.stringify({
|
||||
"private_key": $private_key.val(),
|
||||
"public_key": $public_key.val(),
|
||||
"allowed_ips": $allowed_ips.val(),
|
||||
"name": $new_add_name.val(),
|
||||
"DNS": $new_add_DNS.val(),
|
||||
"endpoint_allowed_ip": $new_add_endpoint_allowed_ip.val(),
|
||||
"MTU": $new_add_MTU.val(),
|
||||
"keep_alive": $new_add_keep_alive.val(),
|
||||
"enable_preshared_key": $enable_preshare_key.prop("checked"),
|
||||
"preshared_key": $enable_preshare_key.val()
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response !== "true") {
|
||||
$("#add_peer_alert").html(response).removeClass("d-none");
|
||||
data_list.forEach((ele) => ele.removeAttr("disabled"));
|
||||
$add_peer.removeAttribute("disabled");
|
||||
$add_peer.innerHTML = "Save";
|
||||
} else {
|
||||
configurations.loadPeers("");
|
||||
data_list.forEach((ele) => ele.removeAttr("disabled"));
|
||||
$("#add_peer_form").trigger("reset");
|
||||
$add_peer.removeAttribute("disabled");
|
||||
$add_peer.innerHTML = "Save";
|
||||
showToast("Add peer successful!");
|
||||
configurations.addModal().toggle();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#add_peer_alert").html("Please fill in all required box.").removeClass("d-none");
|
||||
$add_peer.removeAttribute("disabled");
|
||||
$add_peer.innerHTML = "Add";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when user is typing the amount of peers they want to add, and will check if the amount is less than 1 or
|
||||
* is larger than the amount of available ips
|
||||
*/
|
||||
$("#new_add_amount").on("keyup", function() {
|
||||
let $bulk_amount_validation = $("#bulk_amount_validation");
|
||||
// $(this).removeClass("is-valid").addClass("is-invalid");
|
||||
if ($(this).val().length > 0) {
|
||||
if (isNaN($(this).val())) {
|
||||
$(this).removeClass("is-valid").addClass("is-invalid");
|
||||
$bulk_amount_validation.html("Please enter a valid integer");
|
||||
} else if ($(this).val() > available_ips.length) {
|
||||
$(this).removeClass("is-valid").addClass("is-invalid");
|
||||
$bulk_amount_validation.html(`Cannot create more than ${available_ips.length} peers.`);
|
||||
} else if ($(this).val() < 1) {
|
||||
$(this).removeClass("is-valid").addClass("is-invalid");
|
||||
$bulk_amount_validation.html("Please enter at least 1 or more.");
|
||||
} else {
|
||||
$(this).removeClass("is-invalid").addClass("is-valid");
|
||||
}
|
||||
} else {
|
||||
$(this).removeClass("is-invalid").removeClass("is-valid");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when user toggled add peers by bulk
|
||||
*/
|
||||
$("#bulk_add").on("change", function() {
|
||||
let hide = $(".non-bulk");
|
||||
let amount = $("#new_add_amount");
|
||||
if ($(this).prop("checked") === true) {
|
||||
for (let i = 0; i < hide.length; i++) {
|
||||
$(hide[i]).attr("disabled", "disabled");
|
||||
}
|
||||
amount.removeAttr("disabled");
|
||||
} else {
|
||||
for (let i = 0; i < hide.length; i++) {
|
||||
if ($(hide[i]).attr('id') !== "public_key") {
|
||||
$(hide[i]).removeAttr("disabled");
|
||||
}
|
||||
}
|
||||
amount.attr("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* =======================
|
||||
* Available IP Related
|
||||
* =======================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle when available ip modal show and hide
|
||||
*/
|
||||
$("#available_ip_modal").on("show.bs.modal", () => {
|
||||
document.querySelector('#add_modal').classList.add("ip_modal_open");
|
||||
}).on("hidden.bs.modal", () => {
|
||||
document.querySelector('#add_modal').classList.remove("ip_modal_open");
|
||||
let ips = [];
|
||||
let $selected_ip_list = document.querySelector("#selected_ip_list");
|
||||
for (let i = 0; i < $selected_ip_list.childElementCount; i++) {
|
||||
ips.push($selected_ip_list.children[i].dataset.ip);
|
||||
}
|
||||
ips.forEach((ele) => configurations.triggerIp(ele));
|
||||
});
|
||||
|
||||
/**
|
||||
* When IP Badge got click
|
||||
*/
|
||||
$body.on("click", ".available-ip-badge", function() {
|
||||
$(".available-ip-item[data-ip='" + $(this).data("ip") + "']").removeClass("active");
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
/**
|
||||
* When available ip item got click
|
||||
*/
|
||||
$body.on("click", ".available-ip-item", function() {
|
||||
configurations.triggerIp($(this).data("ip"));
|
||||
});
|
||||
|
||||
/**
|
||||
* When search IP button got clicked
|
||||
*/
|
||||
$("#search_available_ip").on("click", function() {
|
||||
configurations.ipModal().toggle();
|
||||
let $allowed_ips = document.querySelector("#allowed_ips");
|
||||
if ($allowed_ips.value.length > 0) {
|
||||
let s = $allowed_ips.value.split(",");
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
s[i] = s[i].trim();
|
||||
configurations.triggerIp(s[i]);
|
||||
}
|
||||
}
|
||||
}).tooltip();
|
||||
|
||||
/**
|
||||
* When confirm IP is clicked
|
||||
*/
|
||||
$("#confirm_ip").on("click", () => {
|
||||
configurations.ipModal().toggle();
|
||||
let ips = [];
|
||||
let $selected_ip_list = $("#selected_ip_list");
|
||||
$selected_ip_list.children().each(function() {
|
||||
ips.push($(this).data("ip"));
|
||||
});
|
||||
$("#allowed_ips").val(ips.join(", "));
|
||||
ips.forEach((ele) => configurations.triggerIp(ele));
|
||||
});
|
||||
|
||||
/**
|
||||
* =======
|
||||
* QR Code
|
||||
* =======
|
||||
*/
|
||||
|
||||
/**
|
||||
* When the QR-code button got clicked on each peer
|
||||
*/
|
||||
$body.on("click", ".btn-qrcode-peer", function() {
|
||||
let src = $(this).data('imgsrc');
|
||||
$.ajax({
|
||||
"url": src,
|
||||
"method": "GET"
|
||||
}).done(function(res) {
|
||||
$("#qrcode_img").attr('src', res);
|
||||
configurations.qrcodeModal().toggle();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* ===========
|
||||
* Delete Peer
|
||||
* ===========
|
||||
*/
|
||||
|
||||
/**
|
||||
* When the delete button got clicked on each peer
|
||||
*/
|
||||
$body.on("click", ".btn-delete-peer", function() {
|
||||
let peer_id = $(this).data('peer-id')
|
||||
$("#delete_peer").data("peer-id", peer_id);
|
||||
configurations.deleteModal().toggle();
|
||||
});
|
||||
|
||||
$body.on("click", ".btn-lock-peer", function() {
|
||||
configurations.toggleAccess($(this).data('peer-id'), configurations.getConfigurationName());
|
||||
if ($(this).hasClass("lock")) {
|
||||
console.log($(this).data("peer-name"))
|
||||
showToast(`Enabled ${$(this).children().data("peer-name")}`)
|
||||
$(this).removeClass("lock")
|
||||
$(this).children().tooltip('hide').attr('data-original-title', 'Peer enabled. Click to disable peer.').tooltip('show');
|
||||
} else {
|
||||
// Currently unlocked
|
||||
showToast(`Disabled ${$(this).children().data("peer-name")}`)
|
||||
$(this).addClass("lock");
|
||||
$(this).children().tooltip('hide').attr('data-original-title', 'Peer disabled. Click to enable peer.').tooltip('show');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* When the confirm delete button clicked
|
||||
*/
|
||||
$("#delete_peer").on("click", function() {
|
||||
$(this).attr("disabled", "disabled");
|
||||
$(this).html("Deleting...");
|
||||
let config = configurations.getConfigurationName();
|
||||
let peer_ids = [$(this).data("peer-id")];
|
||||
configurations.deletePeers(config, peer_ids);
|
||||
});
|
||||
|
||||
/**
|
||||
* =============
|
||||
* Peer Settings
|
||||
* =============
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle when setting button got clicked for each peer
|
||||
*/
|
||||
$body.on("click", ".btn-setting-peer", function() {
|
||||
// configurations.startProgressBar();
|
||||
let peer_id = $(this).data("peer-id");
|
||||
$("#save_peer_setting").attr("peer_id", peer_id);
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: "/get_peer_data/" + configurations.getConfigurationName(),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: JSON.stringify({ "id": peer_id }),
|
||||
success: function(response) {
|
||||
let peer_name = ((response.name === "") ? "Untitled" : response.name);
|
||||
$("#setting_modal .peer_name").html(peer_name);
|
||||
$("#setting_modal #peer_name_textbox").val(response.name);
|
||||
$("#setting_modal #peer_private_key_textbox").val(response.private_key);
|
||||
$("#setting_modal #peer_DNS_textbox").val(response.DNS);
|
||||
$("#setting_modal #peer_allowed_ip_textbox").val(response.allowed_ip);
|
||||
$("#setting_modal #peer_endpoint_allowed_ips").val(response.endpoint_allowed_ip);
|
||||
$("#setting_modal #peer_mtu").val(response.mtu);
|
||||
$("#setting_modal #peer_keep_alive").val(response.keep_alive);
|
||||
$("#setting_modal #peer_preshared_key_textbox").val(response.preshared_key);
|
||||
configurations.settingModal().toggle();
|
||||
configurations.endProgressBar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when setting modal is closing
|
||||
*/
|
||||
$('#setting_modal').on('hidden.bs.modal', function() {
|
||||
$("#setting_peer_alert").addClass("d-none");
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when private key text box in setting modal got changed
|
||||
*/
|
||||
$("#peer_private_key_textbox").on("change", function() {
|
||||
let $save_peer_setting = $("#save_peer_setting");
|
||||
if ($(this).val().length > 0) {
|
||||
$.ajax({
|
||||
"url": "/check_key_match/" + configurations.getConfigurationName(),
|
||||
"method": "POST",
|
||||
"headers": { "Content-Type": "application/json" },
|
||||
"data": JSON.stringify({
|
||||
"private_key": $("#peer_private_key_textbox").val(),
|
||||
"public_key": $save_peer_setting.attr("peer_id")
|
||||
})
|
||||
}).done(function(res) {
|
||||
if (res.status === "failed") {
|
||||
$("#setting_peer_alert").html(res.status).removeClass("d-none");
|
||||
} else {
|
||||
$("#setting_peer_alert").addClass("d-none");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* When save peer setting button got clicked
|
||||
*/
|
||||
$("#save_peer_setting").on("click", function() {
|
||||
$(this).attr("disabled", "disabled");
|
||||
$(this).html("Saving...");
|
||||
let $peer_DNS_textbox = $("#peer_DNS_textbox");
|
||||
let $peer_allowed_ip_textbox = $("#peer_allowed_ip_textbox");
|
||||
let $peer_endpoint_allowed_ips = $("#peer_endpoint_allowed_ips");
|
||||
let $peer_name_textbox = $("#peer_name_textbox");
|
||||
let $peer_private_key_textbox = $("#peer_private_key_textbox");
|
||||
let $peer_preshared_key_textbox = $("#peer_preshared_key_textbox");
|
||||
let $peer_mtu = $("#peer_mtu");
|
||||
let $peer_keep_alive = $("#peer_keep_alive");
|
||||
|
||||
if ($peer_DNS_textbox.val() !== "" &&
|
||||
$peer_allowed_ip_textbox.val() !== "" && $peer_endpoint_allowed_ips.val() !== "") {
|
||||
let peer_id = $(this).attr("peer_id");
|
||||
let conf_id = $(this).attr("conf_id");
|
||||
let data_list = [$peer_name_textbox, $peer_DNS_textbox, $peer_private_key_textbox, $peer_preshared_key_textbox, $peer_allowed_ip_textbox, $peer_endpoint_allowed_ips, $peer_mtu, $peer_keep_alive];
|
||||
data_list.forEach((ele) => ele.attr("disabled", "disabled"));
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: "/save_peer_setting/" + conf_id,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: JSON.stringify({
|
||||
id: peer_id,
|
||||
name: $peer_name_textbox.val(),
|
||||
DNS: $peer_DNS_textbox.val(),
|
||||
private_key: $peer_private_key_textbox.val(),
|
||||
allowed_ip: $peer_allowed_ip_textbox.val(),
|
||||
endpoint_allowed_ip: $peer_endpoint_allowed_ips.val(),
|
||||
MTU: $peer_mtu.val(),
|
||||
keep_alive: $peer_keep_alive.val(),
|
||||
preshared_key: $peer_preshared_key_textbox.val()
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.status === "failed") {
|
||||
$("#setting_peer_alert").html(response.msg).removeClass("d-none");
|
||||
} else {
|
||||
configurations.settingModal().toggle();
|
||||
configurations.loadPeers($('#search_peer_textbox').val());
|
||||
$('#alertToast').toast('show');
|
||||
$('#alertToast .toast-body').html("Peer Saved!");
|
||||
}
|
||||
$("#save_peer_setting").removeAttr("disabled").html("Save");
|
||||
data_list.forEach((ele) => ele.removeAttr("disabled"));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#setting_peer_alert").html("Please fill in all required box.").removeClass("d-none");
|
||||
$("#save_peer_setting").removeAttr("disabled").html("Save");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggle show or hide for the private key textbox in the setting modal
|
||||
*/
|
||||
$(".peer_private_key_textbox_switch").on("click", function() {
|
||||
let $peer_private_key_textbox = $("#peer_private_key_textbox");
|
||||
let mode = (($peer_private_key_textbox.attr('type') === 'password') ? "text" : "password");
|
||||
let icon = (($peer_private_key_textbox.attr('type') === 'password') ? "bi bi-eye-slash-fill" : "bi bi-eye-fill");
|
||||
$peer_private_key_textbox.attr('type', mode);
|
||||
$(".peer_private_key_textbox_switch i").removeClass().addClass(icon);
|
||||
});
|
||||
|
||||
/**
|
||||
* ===========
|
||||
* Search Peer
|
||||
* ===========
|
||||
*/
|
||||
|
||||
let typingTimer; // Timeout object
|
||||
let doneTypingInterval = 200; // Timeout interval
|
||||
|
||||
/**
|
||||
* Handle when the user keyup and keydown on the search textbox
|
||||
*/
|
||||
$('#search_peer_textbox').on('keyup', function() {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(() => {
|
||||
configurations.loadPeers($(this).val());
|
||||
}, doneTypingInterval);
|
||||
}).on('keydown', function() {
|
||||
clearTimeout(typingTimer);
|
||||
});
|
||||
|
||||
/**
|
||||
* Manage Peers
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle when sort peers changed
|
||||
*/
|
||||
$body.on("change", "#sort_by_dropdown", function() {
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
data: JSON.stringify({ 'sort': $("#sort_by_dropdown option:selected").val() }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
url: "/update_dashboard_sort",
|
||||
success: function() {
|
||||
configurations.loadPeers($('#search_peer_textbox').val());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle copy public key
|
||||
*/
|
||||
$body.on("mouseenter", ".key", function() {
|
||||
let label = $(this).parent().siblings().children()[1];
|
||||
label.style.opacity = "100";
|
||||
}).on("mouseout", ".key", function() {
|
||||
let label = $(this).parent().siblings().children()[1];
|
||||
label.style.opacity = "0";
|
||||
setTimeout(function() {
|
||||
label.innerHTML = "CLICK TO COPY";
|
||||
}, 200);
|
||||
}).on("click", ".key", function() {
|
||||
let label = $(this).parent().siblings().children()[1];
|
||||
configurations.copyToClipboard($(this));
|
||||
label.innerHTML = "COPIED!";
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when interval button got clicked
|
||||
*/
|
||||
$body.on("click", ".update_interval", function() {
|
||||
$(".interval-btn-group button").removeClass("active");
|
||||
let _new = $(this);
|
||||
_new.addClass("active");
|
||||
let interval = $(this).data("refresh-interval");
|
||||
if ([5000, 10000, 30000, 60000].includes(interval)) {
|
||||
configurations.updateRefreshInterval(interval);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// $.ajax({
|
||||
// method:"POST",
|
||||
// data: "interval="+$(this).data("refresh-interval"),
|
||||
// url: "/update_dashboard_refresh_interval",
|
||||
// success: function (res){
|
||||
// configurations.updateRefreshInterval(res, interval);
|
||||
// }
|
||||
// });
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when refresh button got clicked
|
||||
*/
|
||||
$body.on("click", ".refresh", function() {
|
||||
configurations.loadPeers($('#search_peer_textbox').val());
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle when display mode button got clicked
|
||||
*/
|
||||
$body.on("click", ".display_mode", function() {
|
||||
$(".display-btn-group button").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
window.localStorage.setItem("displayMode", $(this).data("display-mode"));
|
||||
configurations.updateDisplayMode();
|
||||
if ($(this).data("display-mode") === "list") {
|
||||
Array($(".peer_list").children()).forEach(function(child) {
|
||||
$(child).removeClass().addClass("col-12");
|
||||
});
|
||||
showToast("Displaying as List");
|
||||
} else {
|
||||
Array($(".peer_list").children()).forEach(function(child) {
|
||||
$(child).removeClass().addClass("col-sm-6 col-lg-4");
|
||||
});
|
||||
showToast("Displaying as Grids");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* =================
|
||||
* Configuration Menu
|
||||
* =================
|
||||
*/
|
||||
let $setting_btn_menu = $(".setting_btn_menu");
|
||||
$setting_btn_menu.css("top", ($setting_btn_menu.height() + 54) * (-1));
|
||||
let $setting_btn = $(".setting_btn");
|
||||
|
||||
/**
|
||||
* When the menu button got clicked
|
||||
*/
|
||||
$setting_btn.on("click", function() {
|
||||
if ($setting_btn_menu.hasClass("show")) {
|
||||
$setting_btn_menu.removeClass("showing");
|
||||
setTimeout(function() {
|
||||
$setting_btn_menu.removeClass("show");
|
||||
}, 201);
|
||||
} else {
|
||||
$setting_btn_menu.addClass("show");
|
||||
setTimeout(function() {
|
||||
$setting_btn_menu.addClass("showing");
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Whenever the user clicked, if it is outside the menu and the menu is opened, hide the menu
|
||||
*/
|
||||
$("html").on("click", function(r) {
|
||||
if (document.querySelector(".setting_btn") !== r.target) {
|
||||
if (!document.querySelector(".setting_btn").contains(r.target)) {
|
||||
if (!document.querySelector(".setting_btn_menu").contains(r.target)) {
|
||||
$setting_btn_menu.removeClass("showing");
|
||||
setTimeout(function() {
|
||||
$setting_btn_menu.removeClass("show");
|
||||
}, 310);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* ====================
|
||||
* Delete Peers by Bulk
|
||||
* ====================
|
||||
*/
|
||||
|
||||
/**
|
||||
* When delete peers by bulk clicked
|
||||
*/
|
||||
$("#delete_peers_by_bulk_btn").on("click", () => {
|
||||
let $delete_bulk_modal_list = $("#delete_bulk_modal .list-group");
|
||||
$delete_bulk_modal_list.html('');
|
||||
peers.forEach((peer) => {
|
||||
let name;
|
||||
if (peer.name === "") { name = "Untitled Peer"; } else { name = peer.name; }
|
||||
$delete_bulk_modal_list.append('<a class="list-group-item list-group-item-action delete-bulk-peer-item" style="cursor: pointer" data-id="' +
|
||||
peer.id + '" data-name="' + name + '">' + name + '<br><code>' + peer.id + '</code></a>');
|
||||
});
|
||||
configurations.deleteBulkModal().toggle();
|
||||
});
|
||||
|
||||
/**
|
||||
* When the item or tag of delete peers by bulk got clicked
|
||||
*/
|
||||
$body.on("click", ".delete-bulk-peer-item", function() {
|
||||
configurations.toggleDeleteByBulkIP($(this));
|
||||
}).on("click", ".delete-peer-bulk-badge", function() {
|
||||
configurations.toggleDeleteByBulkIP($(".delete-bulk-peer-item[data-id='" + $(this).data("id") + "']"));
|
||||
});
|
||||
|
||||
let $selected_peer_list = document.getElementById("selected_peer_list");
|
||||
|
||||
/**
|
||||
* The change observer to observe when user choose 1 or more peers to delete
|
||||
* @type {MutationObserver}
|
||||
*/
|
||||
let changeObserver = new MutationObserver(function() {
|
||||
if ($selected_peer_list.hasChildNodes()) {
|
||||
$("#confirm_delete_bulk_peers").removeAttr("disabled");
|
||||
} else {
|
||||
$("#confirm_delete_bulk_peers").attr("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
changeObserver.observe($selected_peer_list, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true
|
||||
});
|
||||
|
||||
let confirm_delete_bulk_peers_interval;
|
||||
|
||||
/**
|
||||
* When the user clicked the delete button in the delete peers by bulk
|
||||
*/
|
||||
$("#confirm_delete_bulk_peers").on("click", function() {
|
||||
let btn = $(this);
|
||||
if (confirm_delete_bulk_peers_interval !== undefined) {
|
||||
clearInterval(confirm_delete_bulk_peers_interval);
|
||||
confirm_delete_bulk_peers_interval = undefined;
|
||||
btn.html("Delete");
|
||||
} else {
|
||||
let timer = 5;
|
||||
btn.html(`Deleting in ${timer} secs... Click to cancel`);
|
||||
confirm_delete_bulk_peers_interval = setInterval(function() {
|
||||
timer -= 1;
|
||||
btn.html(`Deleting in ${timer} secs... Click to cancel`);
|
||||
if (timer === 0) {
|
||||
btn.html(`Deleting...`);
|
||||
btn.attr("disabled", "disabled");
|
||||
let ips = [];
|
||||
$selected_peer_list.childNodes.forEach((ele) => ips.push(ele.dataset.id));
|
||||
configurations.deletePeers(configurations.getConfigurationName(), ips);
|
||||
clearInterval(confirm_delete_bulk_peers_interval);
|
||||
confirm_delete_bulk_peers_interval = undefined;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Select all peers to delete
|
||||
*/
|
||||
$("#select_all_delete_bulk_peers").on("click", function() {
|
||||
$(".delete-bulk-peer-item").each(function() {
|
||||
if (!$(this).hasClass("active")) {
|
||||
configurations.toggleDeleteByBulkIP($(this));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* When delete peers by bulk window is hidden
|
||||
*/
|
||||
$(configurations.deleteBulkModal()._element).on("hidden.bs.modal", function() {
|
||||
$(".delete-bulk-peer-item").each(function() {
|
||||
if ($(this).hasClass("active")) {
|
||||
configurations.toggleDeleteByBulkIP($(this));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* ==============
|
||||
* Download Peers
|
||||
* ==============
|
||||
*/
|
||||
|
||||
/**
|
||||
* When the download peers button got clicked
|
||||
*/
|
||||
$body.on("click", ".btn-download-peer", function(e) {
|
||||
e.preventDefault();
|
||||
let link = $(this).attr("href");
|
||||
$.ajax({
|
||||
"url": link,
|
||||
"method": "GET",
|
||||
success: function(res) {
|
||||
configurations.downloadOneConfig(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* When the download all peers got clicked
|
||||
*/
|
||||
$("#download_all_peers").on("click", function() {
|
||||
$.ajax({
|
||||
"url": `/download_all/${configurations.getConfigurationName()}`,
|
||||
"method": "GET",
|
||||
success: function(res) {
|
||||
if (res.peers.length > 0) {
|
||||
window.wireguard.generateZipFiles(res);
|
||||
showToast("Peers' zip file download successful!");
|
||||
} else {
|
||||
showToast("Oops! There are no peer can be download.");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
1
src/static/js/configurationTool.min.js
vendored
Normal file
1
src/static/js/configurationTool.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
243
src/static/js/index.js
Normal file
243
src/static/js/index.js
Normal file
@@ -0,0 +1,243 @@
|
||||
let emptyInputFeedback = "Can't leave empty";
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
let $add_configuration = $("#add_configuration");
|
||||
|
||||
let addConfigurationModal = $("#addConfigurationModal");
|
||||
$(".bottomNavHome").addClass("active");
|
||||
|
||||
|
||||
addConfigurationModal.modal({
|
||||
keyboard: false,
|
||||
backdrop: 'static',
|
||||
show: false
|
||||
});
|
||||
|
||||
addConfigurationModal.on("hidden.bs.modal", function(){
|
||||
$("#add_configuration_form").trigger("reset");
|
||||
$("#add_configuration_form input").removeClass("is-valid").removeClass("is-invalid");
|
||||
$(".addConfigurationAvailableIPs").text("N/A");
|
||||
});
|
||||
|
||||
$(".toggle--switch").on("change", function(){
|
||||
$(this).addClass("waiting").attr("disabled", "disabled");
|
||||
let id = $(this).data("conf-id");
|
||||
let status = $(this).prop("checked");
|
||||
let ele = $(this);
|
||||
let label = $(this).siblings("label");
|
||||
$.ajax({
|
||||
url: `/switch/${id}`
|
||||
}).done(function(res){
|
||||
let dot = $(`div[data-conf-id="${id}"] .dot`);
|
||||
if (res.status){
|
||||
if (status){
|
||||
dot.removeClass("dot-stopped").addClass("dot-running");
|
||||
dot.siblings().text("Running");
|
||||
showToast(`${id} is running.`);
|
||||
}else{
|
||||
dot.removeClass("dot-running").addClass("dot-stopped");
|
||||
showToast(`${id} is stopped.`);
|
||||
}
|
||||
}else{
|
||||
ele.parents().children(".card-message").html(`<pre class="index-alert">Configuration toggle failed. Please check the following error message:<br><code>${res.message}</code></pre>`)
|
||||
showToast(`${id} toggled failed.`, true);
|
||||
if (status){
|
||||
ele.prop("checked", false)
|
||||
}else{
|
||||
ele.prop("checked", true)
|
||||
}
|
||||
}
|
||||
ele.removeClass("waiting").removeAttr("disabled");
|
||||
})
|
||||
});
|
||||
|
||||
$(".sb-home-url").addClass("active");
|
||||
|
||||
$(".card-body").on("click", function(handle){
|
||||
if ($(handle.target).attr("class") !== "toggleLabel" && $(handle.target).attr("class") !== "toggle--switch") {
|
||||
let c = $(".card");
|
||||
for (let i of c){
|
||||
if (i != $(this).parent()[0]){
|
||||
$(i).css("transition", "ease-in-out 0.3s").css("opacity", "0.5")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
window.open($(this).find("a").attr("href"), "_self");
|
||||
}
|
||||
});
|
||||
|
||||
function genKeyPair(){
|
||||
let keyPair = window.wireguard.generateKeypair();
|
||||
$("#addConfigurationPrivateKey").val(keyPair.privateKey).data("checked", true);
|
||||
}
|
||||
|
||||
$("#reGeneratePrivateKey").on("click", function() {
|
||||
genKeyPair();
|
||||
});
|
||||
|
||||
$("#toggleAddConfiguration").on("click", function(){
|
||||
addConfigurationModal.modal('toggle');
|
||||
genKeyPair()
|
||||
});
|
||||
|
||||
$("#addConfigurationPrivateKey").on("change", function() {
|
||||
$privateKey = $(this);
|
||||
$privateKeyFeedback = $("#addConfigurationPrivateKeyFeedback");
|
||||
if ($privateKey.val().length != 44){
|
||||
invalidInput($privateKey, $privateKeyFeedback, "Invalid length");
|
||||
}else{
|
||||
validInput($privateKey);
|
||||
}
|
||||
});
|
||||
|
||||
function ajaxPostJSON(url, data, doneFunc){
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: "POST",
|
||||
data: JSON.stringify(data),
|
||||
headers: {"Content-Type": "application/json"}
|
||||
}).done(function (res) {
|
||||
doneFunc(res);
|
||||
});
|
||||
}
|
||||
|
||||
function validInput(input){
|
||||
input.removeClass("is-invalid").addClass("is-valid").removeAttr("disabled").data("checked", true);
|
||||
}
|
||||
function invalidInput(input, feedback, text){
|
||||
input.removeClass("is-valid").addClass("is-invalid").removeAttr("disabled").data("checked", false);
|
||||
feedback.addClass("invalid-feedback").text(text);
|
||||
}
|
||||
|
||||
function checkPort($this){
|
||||
let port = $this;
|
||||
port.attr("disabled", "disabled");
|
||||
let portFeedback = $("#addConfigurationListenPortFeedback");
|
||||
if (port.val().length == 0){
|
||||
invalidInput(port, portFeedback, emptyInputFeedback)
|
||||
}else{
|
||||
function done(res){
|
||||
if(res.status){
|
||||
validInput(port);
|
||||
}else{
|
||||
invalidInput(port, portFeedback, res.reason)
|
||||
}
|
||||
}
|
||||
ajaxPostJSON('/api/addConfigurationPortCheck', {"port": port.val()}, done);
|
||||
}
|
||||
}
|
||||
$("#addConfigurationListenPort").on("change", function(){
|
||||
checkPort($(this));
|
||||
})
|
||||
|
||||
function checkAddress($this){
|
||||
let address = $this;
|
||||
address.attr("disabled", "disabled");
|
||||
let availableIPs = $(".addConfigurationAvailableIPs");
|
||||
let addressFeedback = $("#addConfigurationAddressFeedback");
|
||||
if (address.val().length == 0){
|
||||
invalidInput(address, addressFeedback, emptyInputFeedback);
|
||||
availableIPs.html(`N/A`);
|
||||
}else{
|
||||
function done(res){
|
||||
if (res.status){
|
||||
availableIPs.html(`<strong>${res.data}</strong>`);
|
||||
validInput(address);
|
||||
}else{
|
||||
invalidInput(address, addressFeedback, res.reason);
|
||||
availableIPs.html(`N/A`);
|
||||
}
|
||||
}
|
||||
ajaxPostJSON("/api/addConfigurationAddressCheck", {"address": address.val()}, done)
|
||||
}
|
||||
}
|
||||
$("#addConfigurationAddress").on("change", function(){
|
||||
checkAddress($(this));
|
||||
});
|
||||
|
||||
|
||||
function checkName($this){
|
||||
let name = $this;
|
||||
let nameFeedback = $("#addConfigurationNameFeedback");
|
||||
name.val(name.val().replace(/\s/g,'')).attr("disabled", "disabled");
|
||||
if (name.val().length === 0){
|
||||
invalidInput(name, nameFeedback, emptyInputFeedback)
|
||||
}else{
|
||||
function done(res){
|
||||
if (res.status){
|
||||
validInput(name);
|
||||
}else{
|
||||
invalidInput(name, nameFeedback, res.reason);
|
||||
}
|
||||
}
|
||||
ajaxPostJSON("/api/addConfigurationNameCheck", {"name": name.val()}, done);
|
||||
}
|
||||
}
|
||||
$("#addConfigurationName").on("change", function(){
|
||||
checkName($(this));
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$("#addConfigurationBtn").on("click", function(){
|
||||
let btn = $(this);
|
||||
let input = $("#add_configuration_form input");
|
||||
let filled = true;
|
||||
for (let i = 0; i < input.length; i++){
|
||||
let $i = $(input[i]);
|
||||
if ($i.attr("required") != undefined){
|
||||
if ($i.val().length == 0 && $i.attr("name") !== "addConfigurationPrivateKey"){
|
||||
invalidInput($i, $i.siblings(".input-feedback"), emptyInputFeedback);
|
||||
filled = false;
|
||||
}
|
||||
if ($i.val().length != 44 && $i.attr("name") == "addConfigurationPrivateKey"){
|
||||
invalidInput($i, $i.siblings(".input-feedback"), "Invalid length");
|
||||
filled = false;
|
||||
}
|
||||
if (!$i.data("checked")){
|
||||
filled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filled){
|
||||
$("#addConfigurationModal .modal-footer .btn").hide();
|
||||
$(".addConfigurationStatus").removeClass("d-none");
|
||||
let data = {};
|
||||
let q = [];
|
||||
for (let i = 0; i < input.length; i++){
|
||||
let $i = $(input[i]);
|
||||
data[$i.attr("name")] = $i.val();
|
||||
q.push($i.attr("name"));
|
||||
}
|
||||
let done = (res) => {
|
||||
let name = res.data;
|
||||
$(".addConfigurationAddStatus").removeClass("text-primary").addClass("text-success").html(`<i class="bi bi-check-circle-fill"></i> ${name} added successfully.`);
|
||||
if (res.status){
|
||||
setTimeout(() => {
|
||||
$(".addConfigurationToggleStatus").removeClass("waiting").html(`<div class="spinner-border spinner-border-sm" role="status"></div> Toggle Configuration`)
|
||||
$.ajax({
|
||||
url: `/switch/${name}`
|
||||
}).done(function(res){
|
||||
if (res.status){
|
||||
$(".addConfigurationToggleStatus").removeClass("text-primary").addClass("text-success").html(`<i class="bi bi-check-circle-fill"></i> Toggle Successfully. Refresh in 5 seconds.`);
|
||||
setTimeout(() => {
|
||||
$(".addConfigurationToggleStatus").text("Refeshing...")
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}else{
|
||||
$(".addConfigurationToggleStatus").removeClass("text-primary").addClass("text-danger").html(`<i class="bi bi-x-circle-fill"></i> ${name} toggle failed.`)
|
||||
$("#addCconfigurationAlertMessage").removeClass("d-none").html(`${name} toggle failed. Please check the following error message:<br>${res.message}`);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}else{
|
||||
$(".addConfigurationStatus").removeClass("text-primary").addClass("text-danger").html(`<i class="bi bi-x-circle-fill"></i> ${name} adding failed.`)
|
||||
$("#addCconfigurationAlert").removeClass("d-none").children(".alert-body").text(res.reason);
|
||||
}
|
||||
};
|
||||
ajaxPostJSON("/api/addConfiguration", data, done);
|
||||
}
|
||||
});
|
1
src/static/js/index.min.js
vendored
Normal file
1
src/static/js/index.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
src/static/js/jquery.min.js
vendored
2
src/static/js/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
38
src/static/js/pwa.js
Normal file
38
src/static/js/pwa.js
Normal file
@@ -0,0 +1,38 @@
|
||||
let wrapper = $(".bottomNavWrapper");
|
||||
$(".bottomNavConfigs").on("click", function(){
|
||||
let subNav = $(this).children(".subNav");
|
||||
subNav.removeClass("animate__fadeOutDown").addClass("active animate__fadeInUp");
|
||||
wrapper.fadeIn();
|
||||
});
|
||||
|
||||
$(".bottomNavHome").on("click", function(){
|
||||
window.location.replace('/')
|
||||
});
|
||||
|
||||
$(".bottomNavSettings").on("click", function(){
|
||||
window.location.replace('/settings')
|
||||
})
|
||||
|
||||
|
||||
function hideBottomSubNav(){
|
||||
$(".bottomNavButton .subNav").removeClass("animate__fadeInUp").addClass("animate__fadeOutDown");
|
||||
wrapper.fadeOut();
|
||||
setTimeout(function(){
|
||||
$(".bottomNavButton .subNav").removeClass("active");
|
||||
},350)
|
||||
}
|
||||
|
||||
wrapper.on("click", function(){
|
||||
hideBottomSubNav();
|
||||
});
|
||||
|
||||
|
||||
$(".bottomNavMore").on("click", function(){
|
||||
let subNav = $(this).children(".subNav");
|
||||
subNav.removeClass("animate__fadeOutDown").addClass("active animate__fadeInUp");
|
||||
wrapper.fadeIn();
|
||||
});
|
||||
|
||||
// $(".bottomNavButton .nav-conf-link").on("click", function(){
|
||||
// hideBottomSubNav();
|
||||
// })
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user