1
0
mirror of https://github.com/donaldzou/WGDashboard.git synced 2024-11-22 15:20:09 +01:00

Merge pull request #303 from donaldzou/v4

Merging the v4 branch to main.
This commit is contained in:
Donald Zou 2024-08-17 00:56:29 -04:00 committed by GitHub
commit a9f977c3b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
131 changed files with 17383 additions and 11890 deletions

35
.gitignore vendored
View File

@ -15,4 +15,37 @@ log/**
release/* release/*
src/db/wgdashboard.db src/db/wgdashboard.db
.jshintrc .jshintrc
node_modules/ 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
.vite/*

649
README.md
View File

@ -21,125 +21,272 @@ Hi Everyone, I've decided to promote the long time working `v3.1` to `v4`! This
<hr> <hr>
<p align="center"> <p align="center">
<img alt="WGDashboard" src="img/logo.png" width="128"> <img alt="WGDashboard" src="./src/static/img/logo.png" width="128">
</p> </p>
<h1 align="center">WGDashboard</h1> <h1 align="center">WGDashboard</h1>
<p align="center"> <p align="center">
<img src="http://ForTheBadge.com/images/badges/made-with-python.svg"> <img src="https://forthebadge.com/images/badges/made-with-python.svg">
<img src="https://forthebadge.com/images/badges/made-with-javascript.svg">
<img src="https://forthebadge.com/images/badges/license-mit.svg">
</p>
<p align="center">
<img src="https://forthebadge.com/images/badges/built-with-love.svg">
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/donaldzou/wireguard-dashboard/releases/latest"><img src="https://img.shields.io/github/v/release/donaldzou/wireguard-dashboard"></a> <a href="https://github.com/donaldzou/wireguard-dashboard/releases/latest"><img src="https://img.shields.io/github/v/release/donaldzou/wireguard-dashboard"></a>
<a href="https://wakatime.com/badge/github/donaldzou/WGDashboard"><img src="https://wakatime.com/badge/github/donaldzou/WGDashboard.svg" alt="wakatime"></a> <a href="https://wakatime.com/badge/github/donaldzou/WGDashboard"><img src="https://wakatime.com/badge/github/donaldzou/WGDashboard.svg" alt="wakatime"></a>
<a href="https://hits.seeyoufarm.com"><img src="https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fdonaldzou%2FWGDashboard&count_bg=%2379C83D&title_bg=%23555555&icon=github.svg&icon_color=%23E7E7E7&title=Visitor&edge_flat=false"/></a> <a href="https://hits.seeyoufarm.com"><img src="https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fdonaldzou%2FWGDashboard&count_bg=%2379C83D&title_bg=%23555555&icon=github.svg&icon_color=%23E7E7E7&title=Visitor&edge_flat=false"/></a>
</p> </p>
<p align="center">Monitoring WireGuard is not convinient, need to login into server and type <code>wg show</code>. That's why this platform is being created, to view all configurations and manage them in a easier way.</p> <p align="center">Monitoring WireGuard is not convenient, need to remote access to server and type <code>wg show</code>. That's why this project is being created, to view all configurations and manage them in a easy way.</p>
<p align="center"><small>Note: This project is not affiliate to the official WireGuard Project ;)</small></p> <p align="center">With all these awesome features, while keeping it <b>simple</b>, <b>easy to install and use</b></p>
## 📣 What's New: v3.0 <p align="center"><b><i>This project is not affiliate to the official WireGuard Project</i></b></p>
- 🎉 **New Features** ## 📣 What's New: v4.0
- **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** ### 🎉 New Features
- [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:* - **Updated dashboard design**: Re-designed some of the section with more modern style and layout, the UI is faster and more responsive, it also uses less memory. But overall is still the same dashboard you're familiarized.
- **Docker Solution**: We now have 2 docker solutions! Thanks to @DaanSelen & @shuricksumy for providing them. For more information, please see the [Docker](#-docker-solutions) section below.
- **Peer Job Scheduler**: Now you can schedule jobs for each peer to either **restrict** or **delete** the peer if the peer's total / upload / download data usage exceeded a limit, or you can set a specific datetime to restrict or delete the peer.
- **Share Peer's QR Code with Public Link**: You can share a peer's QR code and `.conf` file without the need to loging in.
- **WGDashboard's REST API**: You can now request all the api endpoint used in the dashboard. For more details please review the [API Documentation](./docs/api-documents.md).
- **Logging**: Dashboard will now log all activity on the dashboard and API requests.
- **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.
> If you have any other brilliant ideas for this project, please shout it in here [#129](https://github.com/donaldzou/WGDashboard/issues/129) :heart: ### 🧐 Other Changes
- **Deprecated jQuery from the project, and migrated and rewrote the whole front-end with Vue.js. This allows the dashboard is future proofed, and potential cross server access with a desktop app.**
- Rewrote the backend into a REST API structure
- Improved SQL query efficient
- Removed all templates, except for `index.html` where it will load the Vue.js app.
- Parsing names in `.conf`
- Minimized the need to read `.conf`, only when any `.conf` is modified
**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 ;)** ### 🥘 New Experimental Features
- **Cross-Server Access**: Now you can access other servers that installed `v4` of WGDashboard through API key.
- **Desktop App**: Thanks to **Cross-Server Access**, you can now download an ElectronJS based desktop app of WGDashboard, and use that to access WGDashboard on different servers.
- > For more information, please scroll down to [🥘 Experimental Functions](#-experimental-functions)
> I can't thank enough for all of you who wait for this release, and for those who are new to this project, welcome :)
> Also, huge thanks to who contributed to this major release:
> @bolgovrussia, @eduardorosabales, @Profik, @airgapper, @tokon2000, @bkeenke, @kontorskiy777, @bugsse, @Johnnykson, @DaanSelen, @shuricksumy and many others!
<hr> <hr>
## Table of Content ## 📋 Table of Content
<!-- TOC -->
- [💡 Features](#-features) * [📣 What's New: v4.0](#-whats-new-v40)
- [📝 Requirement](#-requirement) * [🎉 New Features](#-new-features)
- [🛠 Install](#-install) * [🧐 Other Changes](#-other-changes)
- [🪜 Usage](#-usage) * [🥘 New Experimental Features](#-new-experimental-features)
- [Start/Stop/Restart WGDashboard](#startstoprestart-wgdashboard) * [📋 Table of Content](#-table-of-content)
- [Autostart WGDashboard on boot](#autostart-wgdashboard-on-boot--v22) * [💡 Features](#-features)
- [✂️ Dashboard Configuration](#%EF%B8%8F-dashboard-configuration) * [📝 Requirements](#-requirements)
- [Dashboard Configuration file](#dashboard-configuration-file) * [Supported Operating Systems](#supported-operating-systems)
- [Generating QR code and peer configuration file (.conf)](#generating-qr-code-and-peer-configuration-file-conf) * [Existing WireGuard Configurations](#existing-wireguard-configurations)
- [❓ How to update the dashboard?](#-how-to-update-the-dashboard) * [🛠 Install](#-install)
- [🥘 Experimental Functions](#-experimental-functions) * [Install Commands](#install-commands)
- [🔍 Screenshot](#-screenshot) * [Ubuntu 20.04 LTS](#ubuntu-2004-lts)
- [⏰ Changelog](#--changelog) * [Ubuntu 22.04 LTS & Ubuntu 24.02 LTS](#ubuntu-2204-lts--ubuntu-2402-lts)
- [🛒 Dependencies](#-dependencies) * [Debian 12.6](#debian-126)
- [✨ Contributors](#-contributors) * [Debian 11.10](#debian-1110)
* [Red Hat Enterprise Linux 9.4 & CentOS 9-Stream](#red-hat-enterprise-linux-94--centos-9-stream)
* [Fedora 40 & Fedora 39 & Fedora 38](#fedora-40--fedora-39--fedora-38)
* [Manual Installation](#manual-installation)
* [🪜 Usage](#-usage)
* [Start/Stop/Restart WGDashboard](#startstoprestart-wgdashboard)
* [Autostart WGDashboard on boot (>= v2.2)](#autostart-wgdashboard-on-boot--v22)
* [✂️ Dashboard Configuration](#-dashboard-configuration)
* [Dashboard Configuration file](#dashboard-configuration-file)
* [Generating QR code and peer configuration file (.conf)](#generating-qr-code-and-peer-configuration-file-conf)
* [❓ How to update the dashboard?](#-how-to-update-the-dashboard)
* [**Please note for users who are using `v3 - v3.0.6` want to update to `v4.0`**](#please-note-for-users-who-are-using-v3---v306-want-to-update-to-v40)
* [**Please note for users who are using `v2.3.1` or below**](#please-note-for-users-who-are-using-v231-or-below)
* [🐬 Docker Solutions](#-docker-solutions)
* [Solution 1 from @DaanSelen](#solution-1-from-daanselen)
* [Solution 2 from @shuricksumy](#solution-2-from-shuricksumy)
* [📖 WGDashboard REST API Documentation & How to use API Key](#-wgdashboard-rest-api-documentation--how-to-use-api-key)
* [🥘 Experimental Features](#-experimental-features)
* [Cross-Server Access](#cross-server-access)
* [Desktop App](#desktop-app)
* [🔍 Screenshot](#-screenshot)
* [🕰️ Changelogs](#-changelogs)
<!-- TOC -->
## 💡 Features ## 💡 Features
- **No need to re-configure existing WireGuard configuration! It can search for existed configuration files.** - Automatically look for existing WireGuard configuration under `/etc/wireguard`
- Easy to use interface, provided username and password protection to the dashboard - Easy to use interface, provided credential and TOTP protection to the dashboard
- Add peers and edit (Allowed IPs, DNS, Private Key...) - Manage peers and configuration
- View peers and configuration real time details (Data Usage, Latest Handshakes...) - Add Peers or by bulk with auto-generated information
- Share your peer configuration with QR code or file download - Edit peer information
- Testing tool: Ping and Traceroute to your peer's ip - Delete peers with ease
- **And more functions are coming up!** - Restrict peers
- Generate QR Code and `.conf` file for peers, share it through a public link
- Schedule jobs to delete / restrict peer when conditions are met
- View real time peer status
- Testing tool: Ping and Traceroute to your peer
## 📝 Requirement ## 📝 Requirements
- Recommend the following OS, tested by our beloved users: 1. Supported operating systems. Please view the list below.
- [x] Ubuntu 18.04.1 LTS - 20.04.1 LTS [@Me] 2. WireGuard & WireGuard-Tools (`wg-quick`)
- [x] Debian GNU/Linux 10 (buster) [❤️ @[robchez](https://github.com/robchez)] 3. Python 3.10 / 3.11 / 3.12
- [x] AlmaLinux 8.4 (Electric Cheetah) [❤️ @[barry-smithjr](https://github.com/)] 4. `git`, `net-tools`, `sudo` (_This should only apply to RHEL 9 & 8, interestingly it doesn't have it preinstalled)_
- [x] CentOS 7 [❤️ @[PrzemekSkw](https://github.com/PrzemekSkw)]
> **If you have tested on other OS and it works perfectly please provide it to me in [#31](https://github.com/donaldzou/wireguard-dashboard/issues/31). Thank you!** ### Supported Operating Systems
> [!NOTE]
> All operating systems below are tested by myself. All are ARM64 ran in UTM Virtual Machine.
- **WireGuard** and **WireGuard-Tools (`wg-quick`)** are installed. | Ubuntu | Debian | Red Hat Enterprise Linux | CentOS | Fedora |
|-----------|--------|--------------------------|----------|--------|
| 20.04 LTS | 12.6 | 9.4 | 9-Stream | 40 |
| 22.04 LTS | 11.10 | | | 39 |
| 24.02 LTS | | | | 38 |
> Don't know how? Check this <a href="https://www.wireguard.com/install/">official documentation</a> > [!TIP]
- Net Tools (`net-tools`) is installed. > If you installed WGDashboard on other systems without any issues, please let me know. Thank you!
- You can verify this by checking if `ifconfig` return a list of network interface. Such as `eth0`
- Configuration files under **`/etc/wireguard`**, but please note the following sample
```ini ### Existing WireGuard Configurations
[Interface]
...
SaveConfig = true
# Need to include this line to allow WireGuard Tool to save your configuration,
# or if you just want it to monitor your WireGuard Interface and don't need to
# make any changes with the dashboard, you can set it to false.
[Peer] > [!NOTE]
PublicKey = abcd1234 > This only applies to existing WireGuard Configuration under `/etc/wireguard`
AllowedIPs = 1.2.3.4/32
# Must have for each peer
```
- Python 3.7+ & Pip3 ```ini
[Interface]
...
SaveConfig = true
# Need to include this line to allow WireGuard Tool to save your configuration,
# or if you just want it to monitor your WireGuard Interface and don't need to
# make any changes with the dashboard, you can set it to false.
- Browser support CSS3 and ES6 [Peer]
#Name# = Donald's iPhone
PublicKey = abcd1234
AllowedIPs = 1.2.3.4/32
```
> [!TIP]
> With `v4`, WGDashboard will look for entry with `#Name# = abc...` in each peer and use that for the name.
## 🛠 Install ## 🛠 Install
### Install Commands
These commands are tested by myself in each OS. It contains commands to install WireGuard, Git, Net Tools, and even Python on some OS.
> [!WARNING]
> Please make sure you understand these commands before you run them.
#### Ubuntu 20.04 LTS
```shell
sudo add-apt-repository ppa:deadsnakes/ppa -y && \
sudo apt-get update -y && \
sudo apt-get install python3.10 python3.10-distutils wireguard-tools net-tools --no-install-recommends -y && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p
```
#### Ubuntu 22.04 LTS & Ubuntu 24.02 LTS
```shell
sudo apt-get update -y && \
sudo apt install wireguard-tools net-tools --no-install-recommends -y && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf
```
#### Debian 12.6
```shell
apt-get install sudo git iptables -y && \
sudo apt-get update && \
sudo apt install wireguard-tools net-tools && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf
```
#### Debian 11.10
> [!WARNING]
> This commands will download Python 3.10's source code and build from it, since Debian 11.10 doesn't comes with Python 3.10
```shell
apt-get install sudo -y && \
sudo apt-get update && \
sudo apt install -y git iptables build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev wget libbz2-dev wireguard-tools net-tools && \
wget https://www.python.org/ftp/python/3.10.0/Python-3.10.0.tgz && \
tar -xvf Python-3.10.0.tgz && \
cd Python-3.10.0 && \
sudo ./configure --enable-optimizations && \
sudo make && \
sudo make altinstall && \
cd .. && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf
```
#### Red Hat Enterprise Linux 9.4 & CentOS 9-Stream
```shell
sudo yum install wireguard-tools net-tools git python3.11 -y && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf && \
firewall-cmd --add-port=10086/tcp --permanent && \
firewall-cmd --add-port=51820/udp --permanent && \
firewall-cmd --reload
```
#### Fedora 40 & Fedora 39 & Fedora 38
```shell
sudo yum install wireguard-tools net-tools git -y && \
git clone https://github.com/donaldzou/WGDashboard.git && \
cd ./WGDashboard/src && \
chmod +x ./wgd.sh && \
./wgd.sh install && \
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && \
sudo sysctl -p /etc/sysctl.conf && \
firewall-cmd --add-port=10086/tcp --permanent && \
firewall-cmd --add-port=51820/udp --permanent && \
firewall-cmd --reload
```
### Manual Installation
> [!NOTE]
> To ensure a smooth installation process, please make sure Python 3.10/3.11/3.12, `git`, `wireguard-tools` and `net-tools` are installed :)
1. Download WGDashboard 1. Download WGDashboard
```shell ```shell
git clone -b v3.0.6.2 https://github.com/donaldzou/WGDashboard.git wgdashboard git clone https://github.com/donaldzou/WGDashboard.git wgdashboard
2. Open the WGDashboard folder 2. Open the WGDashboard folder
@ -150,7 +297,7 @@ Hi Everyone, I've decided to promote the long time working `v3.1` to `v4`! This
3. Install WGDashboard 3. Install WGDashboard
```shell ```shell
sudo chmod u+x wgd.sh sudo chmod u+x wgd.sh && \
sudo ./wgd.sh install sudo ./wgd.sh install
``` ```
@ -163,17 +310,15 @@ Hi Everyone, I've decided to promote the long time working `v3.1` to `v4`! This
5. Run WGDashboard 5. Run WGDashboard
```shell ```shell
./wgd.sh start sudo ./wgd.sh start
``` ```
**Note**:
> For [`pivpn`](https://github.com/pivpn/pivpn) user, please use `sudo ./wgd.sh start` to run if your current account does not have the permission to run `wg show` and `wg-quick`.
6. Access dashboard 6. Access dashboard
Access your server with port `10086` (e.g. http://your_server_ip:10086), using username `admin` and password `admin`. See below how to change port and ip that the dashboard is running with. Access your server with port `10086` (e.g. http://your_server_ip:10086), using username `admin` and password `admin`. See below how to change port and ip that the dashboard is running with.
## 🪜 Usage ## 🪜 Usage
#### Start/Stop/Restart WGDashboard #### Start/Stop/Restart WGDashboard
@ -220,40 +365,33 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
```ini ```ini
[Unit] [Unit]
After=network.service After=syslog.target network-online.target
Wants=wg-quick.target
ConditionPathIsDirectory=/etc/wireguard
[Service] [Service]
WorkingDirectory=<your dashboard directory full path here> Type=forking
ExecStart=/usr/bin/python3 <your dashboard directory full path here>/dashboard.py PIDFile=<absolute_path_of_wgdashboard_src>/gunicorn.pid
WorkingDirectory=<absolute_path_of_wgdashboard_src>
ExecStart=<absolute_path_of_wgdashboard_src>/wgd.sh start
ExecStop=<absolute_path_of_wgdashboard_src>/wgd.sh stop
ExecReload=<absolute_path_of_wgdashboard_src>/wgd.sh restart
TimeoutSec=120
PrivateTmp=yes
Restart=always Restart=always
[Install] [Install]
WantedBy=default.target WantedBy=multi-user.target
``` ```
Now, we need to replace both `<your dashboard directory full path here>` to the one you just copied from step 2. After doing this, the file will become something like this, your file might be different: Now, we need to replace all `<absolute_path_of_wgdashboard_src>` to the one you just copied from step 2. After doing this, the file will become something like this, your file might be different:
```ini
[Unit]
After=netword.service
[Service]
WorkingDirectory=/root/wgdashboard/src
ExecStart=/usr/bin/python3 /root/wgdashboard/src/dashboard.py
Restart=always
[Install]
WantedBy=default.target
```
**Be aware that after the value of `WorkingDirectory`, it does not have a `/` (slash).** And then save the file after you edited it **Be aware that after the value of `WorkingDirectory`, it does not have a `/` (slash).** And then save the file after you edited it
4. Copy the service file to systemd folder 4. Copy the service file to systemd folder
```bash ```bash
$ cp wg-dashboard.service /etc/systemd/system/wg-dashboard.service $ sudo cp wg-dashboard.service /etc/systemd/system/wg-dashboard.service
``` ```
To make sure you copy the file successfully, you can use this command `cat /etc/systemd/system/wg-dashboard.service` to see if it will output the file you just edited. To make sure you copy the file successfully, you can use this command `cat /etc/systemd/system/wg-dashboard.service` to see if it will output the file you just edited.
@ -272,28 +410,31 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
```bash ```bash
$ sudo systemctl status wg-dashboard.service $ sudo systemctl status wg-dashboard.service
``` ```
And you should see something like this And you should see something like this
```shell ```shell
● wg-dashboard.service ● wg-dashboard.service
Loaded: loaded (/etc/systemd/system/wg-dashboard.service; enabled; vendor preset: enabled) Loaded: loaded (/etc/systemd/system/wg-dashboard.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2021-08-03 22:31:26 UTC; 4s ago Active: active (running) since Wed 2024-08-14 22:21:47 EDT; 55s ago
Main PID: 6602 (python3) Process: 494968 ExecStart=/home/donaldzou/Wireguard-Dashboard/src/wgd.sh start (code=exited, status=0/SUCCESS)
Tasks: 1 (limit: 453) Main PID: 495005 (gunicorn)
Memory: 26.1M Tasks: 5 (limit: 4523)
Memory: 36.8M
CPU: 789ms
CGroup: /system.slice/wg-dashboard.service CGroup: /system.slice/wg-dashboard.service
└─6602 /usr/bin/python3 /root/wgdashboard/src/dashboard.py ├─495005 /home/donaldzou/Wireguard-Dashboard/src/venv/bin/python3 ./venv/bin/gunicorn --config ./gunicorn.conf.py
└─495007 /home/donaldzou/Wireguard-Dashboard/src/venv/bin/python3 ./venv/bin/gunicorn --config ./gunicorn.conf.py
Aug 03 22:31:26 ubuntu-wg systemd[1]: Started wg-dashboard.service. Aug 14 22:21:40 wg sudo[494978]: root : PWD=/home/donaldzou/Wireguard-Dashboard/src ; USER=root ; COMMAND=./venv/bin/gunicorn --config ./gunicorn.conf.py
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Serving Flask app "WGDashboard" (lazy loading) Aug 14 22:21:40 wg sudo[494978]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=0)
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Environment: production Aug 14 22:21:40 wg wgd.sh[494979]: [WGDashboard] WGDashboard w/ Gunicorn will be running on 0.0.0.0:10086
Aug 03 22:31:27 ubuntu-wg python3[6602]: WARNING: This is a development server. Do not use it in a production deployment. Aug 14 22:21:40 wg wgd.sh[494979]: [WGDashboard] Access log file is at ./log/access_2024_08_14_22_21_40.log
Aug 03 22:31:27 ubuntu-wg python3[6602]: Use a production WSGI server instead. Aug 14 22:21:40 wg wgd.sh[494979]: [WGDashboard] Error log file is at ./log/error_2024_08_14_22_21_40.log
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Debug mode: off Aug 14 22:21:40 wg sudo[494978]: pam_unix(sudo:session): session closed for user root
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Running on all addresses. Aug 14 22:21:45 wg wgd.sh[494968]: [WGDashboard] Checking if WGDashboard w/ Gunicorn started successfully
Aug 03 22:31:27 ubuntu-wg python3[6602]: WARNING: This is a development server. Do not use it in a production deployment. Aug 14 22:21:47 wg wgd.sh[494968]: [WGDashboard] WGDashboard w/ Gunicorn started successfully
Aug 03 22:31:27 ubuntu-wg python3[6602]: * Running on http://0.0.0.0:10086/ (Press CTRL+C to quit) Aug 14 22:21:47 wg wgd.sh[494968]: ------------------------------------------------------------
Aug 14 22:21:47 wg systemd[1]: Started wg-dashboard.service.
``` ```
If you see `Active:` followed by `active (running) since...` then it means it run correctly. If you see `Active:` followed by `active (running) since...` then it means it run correctly.
@ -315,7 +456,7 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u
Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following configurations: Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following configurations:
| | Description | Default | Edit Available | | | Description | Default | Edit Available |
| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | -------------- | |------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------|----------------|
| **`[Account]`** | *Configuration on account* | | | | **`[Account]`** | *Configuration on account* | | |
| `username` | Dashboard login username | `admin` | Yes | | `username` | Dashboard login username | `admin` | Yes |
| `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes | | `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes |
@ -325,9 +466,10 @@ Since version 2.0, WGDashboard will be using a configuration file called `wg-das
| `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes | | `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes |
| `app_port` | Port the the dashboard will run with | `10086` | Yes | | `app_port` | Port the the dashboard will run with | `10086` | Yes |
| `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** | | `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** |
| `version` | Dashboard Version | `v3.0.6` | **No** | | `version` | Dashboard Version | `v4.0` | **No** |
| `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes | | `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes |
| `dashboard_sort` | How configuration is sorting | `status` | Yes | | `dashboard_sort` | How configuration is sorting | `status` | Yes |
| `dashboard_theme` | Dashboard Theme | `dark` | Yes |
| | | | | | | | | |
| **`[Peers]`** | *Default Settings on a new peer* | | | | **`[Peers]`** | *Default Settings on a new peer* | | |
| `peer_global_dns` | DNS Server | `1.1.1.1` | Yes | | `peer_global_dns` | DNS Server | `1.1.1.1` | Yes |
@ -366,30 +508,12 @@ Endpoint = 0.0.0.0:51820
## ❓ How to update the dashboard? ## ❓ How to update the dashboard?
#### **Please note for user who is using `v2.3.1` or below** #### **Please note for users who are using `v3 - v3.0.6` want to update to `v4.0`**
- Although theoretically updating through `wgd.sh` should work, but I still suggest you to update the dashboard manually.
- For user who is using `v2.3.1` or below, please notice that all data that stored in the current database will **not** transfer to the new database. This is hard decision to move from TinyDB to SQLite. But SQLite does provide a thread-safe access and TinyDB doesn't. I couldn't find a safe way to transfer the data, so you need to do them manually... Sorry about that :pensive: . But I guess this would be a great start for future development :sunglasses:. #### **Please note for users who are using `v2.3.1` or below**
<hr> - For user who is using `v2.3.1` or below, please notice that all data that stored in the current database will **not** transfer to the new database. This is hard decision to move from TinyDB to SQLite. But SQLite does provide a thread-safe access and TinyDB doesn't. I couldn't find a safe way to transfer the data, so you need to do them manually... Sorry about that :pensive:。 But I guess this would be a great start for future development :sunglasses:.
#### Update Method 1 (For `v3.0` or above)
1. Change your directory to `wgdashboard/src`
```bash
cd wgdashboard/src
```
2. Update the dashboard with the following
```bash
./wgd.sh update
chmod +x ./wgd.sh
```
> If this doesn't work, please use the method below. Sorry about that :(
#### Update Method 2
1. Change your directory to `wgdashboard` 1. Change your directory to `wgdashboard`
@ -400,214 +524,65 @@ Endpoint = 0.0.0.0:51820
2. Update the dashboard 2. Update the dashboard
```shell ```shell
git pull https://github.com/donaldzou/WGDashboard.git v3.0.6 --force git pull https://github.com/donaldzou/WGDashboard.git v4.0 --force
``` ```
3. Install 3. Install
```shell ```shell
./wgd.sh install sudo ./wgd.sh install
``` ```
Starting with `v3.0`, you can simply do `sudo ./wgd.sh update` !! (I hope)
## 🐬 Docker Solutions
Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol) Current, we have 2 beloved contributors provided solutions for hosting WGDashboard with Docker
## 🥘 Experimental Functions ### Solution 1 from @DaanSelen
#### Progressive Web App (PWA) for WGDashboard Please visit [Docker-explain.md](./docker/Docker-explain.md)
- With `v3.0`, I've added a `manifest.json` into the dashboard, so user could add their dashboard as a PWA to their browser or mobile device. ### Solution 2 from @shuricksumy
<img src="img/PWA.gif"/> Please visit [shuricksumy/docker-wgdashboard](https://github.com/shuricksumy/docker-wgdashboard)
> For questions or issues related to Docker, please visit [#272](https://github.com/donaldzou/WGDashboard/issues/272)
## 📖 WGDashboard REST API Documentation & How to use API Key
Please visit the [API Documentation](./docs/api-documents.md)
## 🥘 Experimental Features
### Cross-Server Access
Starting with `v4.0`, you can access WGDashboards on other server through one WGDashboard with API Keys
![Cross Server Example](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/cross-server.gif)
### Desktop App
Since the major changes for `v4.0` is to move the whole front-end code to Vue.js. And with this change, we can take the
advantage of combining ElectronJS and Vue.js to create a Desktop version of WGDashboard. Currently, we provide an Universal macOS app and a Windows app.
To download the app, please visit the [latest release](https://github.com/donaldzou/WGDashboard/releases).
![ElectronJS App Demo](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/electronjs-app.gif)
## 🔍 Screenshot ## 🔍 Screenshot
![Sign In Page](img/SignIn.png) ![Sign In](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/sign-in.png)
![Cross Server](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/cross-server.png)
![Index](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/index.png)
![New Configuration](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/new-configuration.png)
![Settings](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/settings.png)
![Light-Dark Mode](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/light-dark.png)
![Configuration](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/configuration.png)
![Add Peers](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/add-peers.png)
![Ping](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/ping.png)
![Traceroute](https://donaldzou.nyc3.cdn.digitaloceanspaces.com/wgdashboard-images/traceroute.png)
![Index Image](img/HomePage.png) ## 🕰️ Changelogs
![Configuration](img/Configuration.png)
![Add Peer](img/AddPeer.png)
![Edit Peer](img/EditPeer.png)
![Delete Peer](img/DeleteBulk.png)
![Dashboard Setting](img/DashboardSetting.png)
![Ping](img/Ping.png)
![Traceroute](img/Traceroute.png)
## ⏰ Changelog
#### v3.0.6 - Mar 22, 2022
**Fixed Bug**
- When wgdashboard is running behind a proxy server, redirecting could cause using http while proxy is using https [❤️ from #161]
#### v3.0.5 - Jan 31, 2022
**Quick Fix**
- Fixed public key does not match when user used an existing private key
- Sorry for the wrong version number that causing the dashboard ask for update after updating.
#### v3.0.3 - Jan 23, 2022
- Fixed when dashboard configuration file cannot be found after a fresh install. [❤️ from #132 ]
#### v3.0 - 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](https://github.com/donaldzou/WGDashboard#-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:*
> 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](https://github.com/donaldzou/WGDashboard#please-note-for-user-who-is-using-v231-or-below) before updating WGDashboard ;)**
#### v2.3.1 - Sep 8, 2021
- Updated dashboard's name to **WGDashboard**!!
#### v2.3 - Sep 8, 2021
- 🎉 **New Features**
- **Update directly from `wgd.sh`:** Now you can update WGDashboard directly from the bash script.
- **Displaying Peers:** You can switch the display mode between list and table in the configuration page.
- 🪚 **Bug Fixed**
- [Peer DNS Validation Fails #67](issues/67): Added DNS format check. [❤️ @realfian]
- [configparser.NoSectionError: No section: 'Interface' #66](issues/66): Changed permission requirement for `etc/wireguard` from `744` to `755`. [❤️ @ramalmaty]
- [Feature request: Interface not loading when information missing #73](issues/73): Fixed when Configuration Address and Listen Port is missing will crash the dashboard. [❤️ @js32]
- [Remote Peer, MTU and PersistentKeepalives added #70](pull/70): Added MTU, remote peer and Persistent Keepalive. [❤️ @realfian]
- [Fixes DNS check to support search domain #65](pull/65): Added allow input domain into DNS. [❤️@davejlong]
- **🧐 Other Changes**
- Moved Add Peer Button into the right bottom corner.
#### v2.2.1 - Aug 16, 2021
Bug Fixed:
- Added support for full subnet on Allowed IP
- Peer setting Save button
#### v2.2 - Aug 14, 2021
- 🎉 **New Features**
- **Add new peers**: Now you can add peers directly on dashboard, it will generate a pair of private key and public key. You can also set its DNS, endpoint allowed IPs. Both can set a default value in the setting page. [❤️ in [#44](https://github.com/donaldzou/wireguard-dashboard/issues/44)]
- **QR Code:** You can add the private key in peer setting of your existed peer to create a QR code. Or just create a new one, dashboard will now be able to auto generate a private key and public key ;) Don't worry, all keys will be generated on your machine, and **will delete all key files after they got generated**. [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
- **Peer configuration file download:** Same as QR code, you now can download the peer configuration file, so you don't need to manually input all the details on the peer machine! [❤️ in [#40](https://github.com/donaldzou/wireguard-dashboard/issues/40)]
- **Search peers**: You can now search peers by their name.
- **Autostart on boot:** Added a tutorial on how to start the dashboard to on boot! Please read the [tutorial below](#autostart-wireguard-dashboard-on-boot). [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
- **Click to copy**: You can now click and copy all peer's public key and configuration's public key.
- ....
- 🪚 **Bug Fixed**
- When there are comments in the wireguard config file, will cause the dashboard to crash.
- Used regex to search for config files.
- **🧐 Other Changes**
- Moved all external CSS and JavaScript file to local hosting (Except Bootstrap Icon, due to large amount of SVG files).
- Updated Python dependencies
- Flask: `v1.1.2 => v2.0.1`
- Jinja: `v2.10.1 => v3.0.1`
- icmplib: `v2.1.1 => v3.0.1`
- Updated CSS/JS dependencies
- Bootstrap: `v4.5.3 => v4.6.0`
- UI adjustment
- Adjusted how peers will display in larger screens, used to be 1 row per peer, now is 3 peers in 1 row.
#### v2.1 - Jul 2, 2021
- Added **Ping** and **Traceroute** tools!
- Adjusted the calculation of data usage on each peers
- Added refresh interval of the dashboard
- Bug fixed when no configuration on fresh install ([#23](https://github.com/donaldzou/wireguard-dashboard/issues/23))
- Fixed crash when too many peers ([#22](https://github.com/donaldzou/wireguard-dashboard/issues/22))
#### v2.0 - May 5, 2021
- Added login function to dashboard
- ***I'm not using the most ideal way to store the username and password, feel free to provide a better way to do this if you any good idea!***
- Added a config file to the dashboard
- Dashboard config can be change within the **Setting** tab on the side bar
- Adjusted UI
- And much more!
#### v1.1.2 - Apr 3, 2021
- Resolved issue [#3](https://github.com/donaldzou/wireguard-dashboard/issues/3).
#### v1.1.1 - Apr 2, 2021
- Able to add a friendly name to each peer. Thanks [#2](https://github.com/donaldzou/wireguard-dashboard/issues/2) !
#### v1.0 - Dec 27, 2020
- Added the function to remove peers
## 🛒 Dependencies
- CSS/JS
- [Bootstrap](https://getbootstrap.com/docs/4.6/getting-started/introduction/) `v4.6.0`
- [Bootstrap Icon](https://icons.getbootstrap.com) `v1.4.0`
- [jQuery](https://jquery.com) `v3.5.1`
- Python
- [Flask](https://pypi.org/project/Flask/) `v2.0.1`
- [ifcfg](https://pypi.org/project/ifcfg/) `v0.24`
- [psutil](https://pypi.org/project/psutil/) `v5.9.8`
- [icmplib](https://pypi.org/project/icmplib/) `v2.1.1`
- [flask-qrcode](https://pypi.org/project/Flask-QRcode/) `v3.0.0`
## ✨ Contributors
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/antonioag95"><img src="https://avatars.githubusercontent.com/u/30556866?v=4?s=100" width="100px;" alt=""/><br /><sub><b>antonioag95</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=antonioag95" title="Tests">⚠️</a> <a href="https://github.com/donaldzou/WGDashboard/commits?author=antonioag95" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tonjo"><img src="https://avatars.githubusercontent.com/u/4726289?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tonjo</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=tonjo" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/reafian"><img src="https://avatars.githubusercontent.com/u/11992416?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Richard Newton</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=reafian" title="Code">💻</a></td>
<td align="center"><a href="http://www.davejlong.com"><img src="https://avatars.githubusercontent.com/u/175317?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Long</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=davejlong" title="Code">💻</a></td>
<td align="center"><a href="http://www.std-soft.com"><img src="https://avatars.githubusercontent.com/u/5978293?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Markus Neubauer</b></sub></a><br /><a href="https://github.com/donaldzou/WGDashboard/commits?author=marneu" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
Please visit the [Changelogs.md](./docs/changelogs.md)

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

@ -0,0 +1,612 @@
# 📖 API Document for WGDashboard
**Version: v4.0**
**Created by: Donald Zou**
<!-- TOC -->
* [📖 API Document for WGDashboard](#-api-document-for-wgdashboard)
* [🔑 How to use API Key?](#-how-to-use-api-key)
* [Create API Key](#create-api-key)
* [Use API Key](#use-api-key)
* [API Endpoints](#api-endpoints)
* [Handshake to Server](#handshake-to-server)
* [Request](#request)
* [Response](#response)
* [Validate Authentication](#validate-authentication)
* [Request](#request-1)
* [Response](#response-1)
* [Authenticate](#authenticate)
* [Request](#request-2)
* [Body Parameters](#body-parameters)
* [Response](#response-2)
* [Sign Out](#sign-out)
* [Request](#request-3)
* [Response](#response-3)
* [Get WireGuard Configurations](#get-wireguard-configurations)
* [Request](#request-4)
* [Response](#response-4)
* [Add WireGuard Configuration](#add-wireguard-configuration)
* [Request](#request-5)
* [Body Parameters](#body-parameters-1)
* [Response](#response-5)
* [Toggle WireGuard Configuration](#toggle-wireguard-configuration)
* [Request](#request-6)
* [Query String Parameter](#query-string-parameter)
* [Response](#response-6)
* [Get WGDashboard Configuration](#get-wgdashboard-configuration)
* [Request](#request-7)
* [Response](#response-7)
* [Update WGDashboard Configuration Item](#update-wgdashboard-configuration-item)
* [Request](#request-8)
* [Body Parameters](#body-parameters-2)
* [Response](#response-8)
* [Get WGDashboard API Keys](#get-wgdashboard-api-keys)
* [Request](#request-9)
* [Response](#response-9)
* [Add WGDashboard API Key](#add-wgdashboard-api-key)
* [Request](#request-10)
* [Body Parameters](#body-parameters-3)
* [Response](#response-10)
* [Endpoint](#endpoint)
* [Request](#request-11)
* [Response](#response-11)
<!-- TOC -->
<hr>
## 🔑 How to use API Key?
### Create API Key
1. To request an API Key, simply login to your WGDashboard, go to **Settings**, scroll to the very bottom. Click the **switch** on the right to enable API Key.
2. Click the blur **Create** button, set an **expiry date** you want or **never expire**, then click **Done**.
### Use API Key
- Simply add `wg-dashboard-apikey` with the value of your API key into the HTTP Header.
```javascript
fetch('http://server:10086/api/handshake', {
headers: {
'content-type': 'application/json',
'wg-dashboard-apikey': 'insert your api key here'
},
method: "GET"
})
```
## API Endpoints
### Handshake to Server
This endpoint is designed for a simple handshake when using API key to connect. If `status` is `true` that means
#### Request
`GET /api/handshake`
#### Response
`200 - OK`
```json
{
"data": null,
"message": null,
"status": true
}
```
`401 - UNAUTHORIZED`
```json
{
"data": null,
"message": "Unauthorized access.",
"status": false
}
```
> Notice: this `401` response will return at all endpoint if your API Key or session is invalid.
<hr>
### Validate Authentication
This endpoint if needed for non-cross-server access. This will check if the cookie on the client side is still valid on the server side.
#### Request
`GET /api/validateAuthentication`
#### Response
`200 - OK`
Session is still valid
```json
{
"data": null,
"message": null,
"status": true
}
```
Session is invalid
```json
{
"data": null,
"message": "Invalid authentication.",
"status": false
}
```
<hr>
### Authenticate
This endpoint is dedicated for non-cross-server access. It is used to authenticate user's username, password and TOTP
#### Request
`POST /api/authenticate`
##### Body Parameters
```json
{
"username": "admin",
"password": "admin",
"totp": "123456"
}
```
| Parameter | Type |
|------------|--------|
| `username` | string |
| `password` | string |
| `totp` | string |
#### Response
`200 - OK`
If username, password and TOTP matched
```json
{
"data": null,
"message": null,
"status": true
}
```
If username, password or TOTP is not match
```json
{
"data": null,
"message": "Sorry, your username, password or OTP is incorrect.",
"status": false
}
```
<hr>
### Sign Out
To remove the current session on server side
#### Request
`GET /api/signout`
#### Response
`200 - OK`
```json
{
"data": null,
"message": null,
"status": true
}
```
<hr>
### Get WireGuard Configurations
To get all WireGuard configurations in `/etc/wireguard`
#### Request
`GET /api/getWireguardConfigurations`
#### Response
`200 - OK`
```json
{
"data": [
{
"Address": "10.200.200.1/24",
"ConnectedPeers": 0,
"DataUsage": {
"Receive": 0.1582,
"Sent": 2.1012999999999997,
"Total": 2.2595
},
"ListenPort": "51820",
"Name": "wg0",
"PostDown": "iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp0s1 -j MASQUERADE;",
"PostUp": "iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp0s1 -j MASQUERADE;",
"PreDown": "",
"PreUp": "",
"PrivateKey": "8DsSMli3okgUx5frKbFQ0fMW5ZMyqyxOdOW7+g21L18=",
"PublicKey": "GQlGi8QJ93hWY7L2xlJyh+7S6+ekER9xP11T92T0O0Q=",
"SaveConfig": true,
"Status": false
}
],
"message": null,
"status": true
}
```
<hr>
### Add WireGuard Configuration
Add a new WireGuard Configuration
#### Request
`POST /api/addWireguardConfiguration`
##### Body Parameters
```json
{
"ConfigurationName": "wg0",
"Address": "10.0.0.1/24",
"ListenPort": 51820,
"PrivateKey": "eJuuamCgakVs2xUZGHh/g7C6Oy89JGh7eE2jjEGbbFc=",
"PublicKey": "3Ruirgw9qNRwNpBepkiVjjSe82tY+lDZr6WaFC4wO2g=",
"PresharedKey": "GMMLKWdJlgsKVoR26BJPsNbDXyfILL+x1Nd6Ecmn4lg=",
"PreUp": "",
"PreDown": "iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp0s1 -j MASQUERADE;",
"PostUp": "iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp0s1 -j MASQUERADE;",
"PostDown": ""
}
```
| Parameter | Type |
|---------------------|--------|
| `ConfigurationName` | string |
| `Address` | string |
| `ListenPort` | int |
| `PrivateKey` | string |
| `PublicKey` | string |
| `PresharedKey` | string |
| `PreUp` | string |
| `PreDown` | string |
| `PostUp` | string |
| `PostDown` | string |
#### Response
`200 - OK`
If everything is good
```json
{
"data": null,
"message": null,
"status": true
}
```
If the new configuration's `ConfigurationName` is already existed
```json
{
"data": null,
"message": "Already have a configuration with the name \"wg0\"",
"status": false
}
```
If the new configuration's `ListenPort` is used by another configuration
```json
{
"data": null,
"message": "Already have a configuration with the port \"51820\"",
"status": false
}
```
If the new configuration's `Address` is used by another configuration
```json
{
"data": null,
"message": "Already have a configuration with the address \"10.0.0.1/24\"",
"status": false
}
```
<hr>
### Toggle WireGuard Configuration
To turn on/off of a WireGuard Configuration
#### Request
`GET /api/toggleWireguardConfiguration/?configurationName=`
##### Query String Parameter
| Parameter | Type |
|---------------------|--------|
| `configurationName` | string |
#### Response
`200 - OK`
If toggle is successful, server will return the current status in `status`: `true` or `false` indicating if the configuration is up or not.
```json
{
"data": true,
"message": null,
"status": true
}
```
If the `configurationName` provided does not exist
```json
{
"data": null,
"message": "Please provide a valid configuration name",
"status": false
}
```
<hr>
### Get WGDashboard Configuration
Get the WGDashboard Configuration, such as `dashboard_theme`...
#### Request
`GET /api/getDashboardConfiguration`
#### Response
`200 - OK`
```json
{
"data": {
"Account": {
"enable_totp": false,
"password": "some hashed value :(",
"totp_verified": false,
"username": "admin"
},
"Database": {
"type": "sqlite"
},
"Other": {
"welcome_session": false
},
"Peers": {
"peer_display_mode": "grid",
"peer_endpoint_allowed_ip": "0.0.0.0/0",
"peer_global_dns": "1.1.1.1",
"peer_keep_alive": "21",
"peer_mtu": "1420",
"remote_endpoint": "192.168.2.38"
},
"Server": {
"app_ip": "0.0.0.0",
"app_port": "10086",
"app_prefix": "",
"auth_req": true,
"dashboard_api_key": true,
"dashboard_refresh_interval": "5000",
"dashboard_sort": "status",
"dashboard_theme": "dark",
"version": "v4.0",
"wg_conf_path": "/etc/wireguard"
}
},
"message": null,
"status": true
}
```
<hr>
### Update WGDashboard Configuration Item
Update the WGDashboard Configuration one at a time
#### Request
`POST /api/updateDashboardConfigurationItem`
##### Body Parameters
```json
{
"section": "Server",
"key": "dashboard_theme",
"value": "dark"
}
```
| Parameter | Type | |
|-----------|--------|----------------------------------------------------------|
| `section` | string | Each section in the `wg-dashboard.ini` |
| `key` | string | Each key/value pair under each in the `wg-dashboard.ini` |
| `value` | string | Value for this key/value pair |
#### Response
`200 - OK`
If update is success
```json
{
"data": true,
"message": null,
"status": true
}
```
If update failed
```json
{
"data": true,
"message": "Message related to the error will appear here",
"status": false
}
```
<hr>
### Get WGDashboard API Keys
Get a list of active API key in WGDashboard
#### Request
`GET /api/getDashboardAPIKeys`
#### Response
`200 - OK`
If API Key function is enabled and there are active API keys
> If `ExpiredAt` is `null`, that means this API key will never expire
```json
{
"data": [
{
"CreatedAt": "2024-08-15 00:42:31",
"ExpiredAt": null,
"Key": "AXt1x3TZMukmA-eSnAyESy08I14n20boppSsknHOB-Y"
},
{
"CreatedAt": "2024-08-14 22:50:44",
"ExpiredAt": "2024-08-21 22:50:43",
"Key": "ry0Suo0BrypSMzbq0C_TjkEcgrFHHj6UBZGmC2-KI2o"
}
],
"message": null,
"status": true
}
```
If API key function is disabled
```json
{
"data": null,
"message": "Dashboard API Keys function is disabled",
"status": false
}
```
<hr>
### Add WGDashboard API Key
Add a new API Key in WGDashboard
#### Request
`POST /api/newDashboardAPIKey`
##### Body Parameters
```json
{
"neverExpire": false,
"ExpiredAt": "2024-12-31 16:00:00"
}
```
| Parameter | Type | |
|---------------|--------|-----------------------------------------------------------------------------------|
| `neverExpire` | bool | If this is `false`, please specify a date in `ExpiredAt` |
| `ExpiredAt` | string | If `neverExpire` is `true`, this can be omitted. Format is `YYYY-MM-DD hh:mm:ss`. |
#### Response
`200 - OK`
If success, it will return the latest list of API Keys
```json
{
"data": [
{
"CreatedAt": "2024-08-15 00:42:31",
"ExpiredAt": null,
"Key": "AXt1x3TZMukmA-eSnAyESy08I14n20boppSsknHOB-Y"
},
{
"CreatedAt": "2024-08-14 22:50:44",
"ExpiredAt": "2024-12-31 16:50:43",
"Key": "ry0Suo0BrypSMzbq0C_TjkEcgrFHHj6UBZGmC2-KI2o"
}
],
"message": null,
"status": true
}
```
If API key function is disabled
```json
{
"data": null,
"message": "Dashboard API Keys function is disabled",
"status": false
}
```
<hr>
### Endpoint
Description
#### Request
`GET`
#### Response
`200 - OK`
```json
{
"data": true,
"message": null,
"status": true
}
```

105
docs/changelogs.md Normal file
View File

@ -0,0 +1,105 @@
# ⏰ Changelogs of WGDashboard
#### v3.0.0 - v3.0.6.2 - Jan 18, 2022
- 🎉 **New Features**
- **Moved from TinyDB to SQLite**: SQLite provide a better performance and loading speed when getting peers! Also avoided crashing the database due to **race condition**.
- **Added Gunicorn WSGI Server**: This could provide more stable on handling HTTP request, and more flexibility in the future (such as HTTPS support). **BIG THANKS to @pgalonza :heart:**
- **Add Peers by Bulk:** User can add peers by bulk, just simply set the amount and click add.
- **Delete Peers by Bulk**: User can delete peers by bulk, without deleting peers one by one.
- **Download Peers in Zip**: User can download all *downloadable* peers in a zip.
- **Added Pre-shared Key to peers:** Now each peer can add with a pre-shared key to enhance security. Previously added peers can add the pre-shared key through the peer setting button.
- **Redirect Back to Previous Page:** The dashboard will now redirect you back to your previous page if the current session got timed out and you need to sign in again.
- **Added Some [🥘 Experimental Functions](#-experimental-functions)**
- 🪚 **Bug Fixed**
- [IP Sorting range issues #99](https://github.com/donaldzou/WGDashboard/issues/99) [❤️ @barryboom]
- [INvalid character written to tunnel json file #108](https://github.com/donaldzou/WGDashboard/issues/108) [❤️ @ikidd]
- [Add IPv6 #91](https://github.com/donaldzou/WGDashboard/pull/91) [❤️ @pgalonza]
- [Added MTU and PersistentKeepalive to QR code and download files #112](https://github.com/donaldzou/WGDashboard/pull/112) [:heart: @reafian]
- **And many other bugs provided by our beloved users** :heart:
- **🧐 Other Changes**
- **Key generating moved to front-end**: No longer need to use the server's WireGuard to generate keys, thanks to the `wireguard.js` from the [official repository](https://git.zx2c4.com/wireguard-tools/tree/contrib/keygen-html/wireguard.js)!
- **Peer transfer calculation**: each peer will now show all transfer amount (previously was only showing transfer amount from the last configuration start-up).
- **UI adjustment on running peers**: peers will have a new style indicating that it is running.
- **`wgd.sh` finally can update itself**: So now user could update the whole dashboard from `wgd.sh`, with the `update` command.
- **Minified JS and CSS files**: Although only a small changes on the file size, but I think is still a good practice to save a bit of bandwidth ;)
*And many other small changes for performance and bug fixes! :laughing:*
#### v2.3.1 - Sep 8, 2021
- Updated dashboard's name to **WGDashboard**!!
#### v2.3 - Sep 8, 2021
- 🎉 **New Features**
- **Update directly from `wgd.sh`:** Now you can update WGDashboard directly from the bash script.
- **Displaying Peers:** You can switch the display mode between list and table in the configuration page.
- 🪚 **Bug Fixed**
- [Peer DNS Validation Fails #67](issues/67): Added DNS format check. [❤️ @realfian]
- [configparser.NoSectionError: No section: 'Interface' #66](issues/66): Changed permission requirement for `etc/wireguard` from `744` to `755`. [❤️ @ramalmaty]
- [Feature request: Interface not loading when information missing #73](issues/73): Fixed when Configuration Address and Listen Port is missing will crash the dashboard. [❤️ @js32]
- [Remote Peer, MTU and PersistentKeepalives added #70](pull/70): Added MTU, remote peer and Persistent Keepalive. [❤️ @realfian]
- [Fixes DNS check to support search domain #65](pull/65): Added allow input domain into DNS. [❤️@davejlong]
- **🧐 Other Changes**
- Moved Add Peer Button into the right bottom corner.
#### v2.2.1 - Aug 16, 2021
Bug Fixed:
- Added support for full subnet on Allowed IP
- Peer setting Save button
#### v2.2 - Aug 14, 2021
- 🎉 **New Features**
- **Add new peers**: Now you can add peers directly on dashboard, it will generate a pair of private key and public key. You can also set its DNS, endpoint allowed IPs. Both can set a default value in the setting page. [❤️ in [#44](https://github.com/donaldzou/wireguard-dashboard/issues/44)]
- **QR Code:** You can add the private key in peer setting of your existed peer to create a QR code. Or just create a new one, dashboard will now be able to auto generate a private key and public key ;) Don't worry, all keys will be generated on your machine, and **will delete all key files after they got generated**. [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
- **Peer configuration file download:** Same as QR code, you now can download the peer configuration file, so you don't need to manually input all the details on the peer machine! [❤️ in [#40](https://github.com/donaldzou/wireguard-dashboard/issues/40)]
- **Search peers**: You can now search peers by their name.
- **Autostart on boot:** Added a tutorial on how to start the dashboard to on boot! Please read the [tutorial below](#autostart-wireguard-dashboard-on-boot). [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)]
- **Click to copy**: You can now click and copy all peer's public key and configuration's public key.
- ....
- 🪚 **Bug Fixed**
- When there are comments in the wireguard config file, will cause the dashboard to crash.
- Used regex to search for config files.
- **🧐 Other Changes**
- Moved all external CSS and JavaScript file to local hosting (Except Bootstrap Icon, due to large amount of SVG files).
- Updated Python dependencies
- Flask: `v1.1.2 => v2.0.1`
- Jinja: `v2.10.1 => v3.0.1`
- icmplib: `v2.1.1 => v3.0.1`
- Updated CSS/JS dependencies
- Bootstrap: `v4.5.3 => v4.6.0`
- UI adjustment
- Adjusted how peers will display in larger screens, used to be 1 row per peer, now is 3 peers in 1 row.
#### v2.1 - Jul 2, 2021
- Added **Ping** and **Traceroute** tools!
- Adjusted the calculation of data usage on each peers
- Added refresh interval of the dashboard
- Bug fixed when no configuration on fresh install ([#23](https://github.com/donaldzou/wireguard-dashboard/issues/23))
- Fixed crash when too many peers ([#22](https://github.com/donaldzou/wireguard-dashboard/issues/22))
#### v2.0 - May 5, 2021
- Added login function to dashboard
- ***I'm not using the most ideal way to store the username and password, feel free to provide a better way to do this if you any good idea!***
- Added a config file to the dashboard
- Dashboard config can be change within the **Setting** tab on the side bar
- Adjusted UI
- And much more!
#### v1.1.2 - Apr 3, 2021
- Resolved issue [#3](https://github.com/donaldzou/wireguard-dashboard/issues/3).
#### v1.1.1 - Apr 2, 2021
- Able to add a friendly name to each peer. Thanks [#2](https://github.com/donaldzou/wireguard-dashboard/issues/2) !
#### v1.0 - Dec 27, 2020
- Added the function to remove peers

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

17
package-lock.json generated Normal file
View File

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

5
package.json Normal file
View File

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

244
src/api.py Normal file
View File

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

7
src/bulkAddBash.sh Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,26 @@
import multiprocessing
import dashboard import dashboard
from datetime import datetime
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger
app_host, app_port = dashboard.gunicornConfig()
date = datetime.today().strftime('%Y_%m_%d_%H_%M_%S')
def post_worker_init(worker):
dashboard.startThreads()
app_host, app_port = dashboard.get_host_bind()
worker_class = 'gthread' worker_class = 'gthread'
workers = multiprocessing.cpu_count() * 2 + 1 workers = 1
threads = 4 threads = 1
bind = f"{app_host}:{app_port}" bind = f"{app_host}:{app_port}"
daemon = True daemon = True
pidfile = './gunicorn.pid' pidfile = './gunicorn.pid'
wsgi_app = "dashboard:app"
accesslog = f"./log/access_{date}.log"
log_level = "debug"
capture_output = True
errorlog = f"./log/error_{date}.log"
print(f"[WGDashboard] WGDashboard w/ Gunicorn will be running on {bind}", flush=True)
print(f"[WGDashboard] Access log file is at {accesslog}", flush=True)
print(f"[WGDashboard] Error log file is at {errorlog}", flush=True)

View File

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

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

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

Binary file not shown.

Binary file not shown.

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

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

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

View File

Before

Width:  |  Height:  |  Size: 4.5 MiB

After

Width:  |  Height:  |  Size: 4.5 MiB

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

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -0,0 +1,72 @@
<script setup>
import { RouterView } from 'vue-router'
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {computed, watch} from "vue";
const store = DashboardConfigurationStore();
store.initCrossServerConfiguration();
if (window.IS_WGDASHBOARD_DESKTOP){
store.IsElectronApp = true;
store.CrossServerConfiguration.Enable = true;
}
watch(store.CrossServerConfiguration, () => {
store.syncCrossServerConfiguration()
}, {
deep: true
});
const getActiveCrossServer = computed(() => {
if (store.ActiveServerConfiguration){
return store.CrossServerConfiguration.ServerList[store.ActiveServerConfiguration]
}
return undefined
})
</script>
<template>
<nav class="navbar bg-dark sticky-top" data-bs-theme="dark">
<div class="container-fluid d-flex text-body align-items-center">
<span class="navbar-brand mb-0 h1">WGDashboard</span>
<small class="ms-auto text-muted" v-if="getActiveCrossServer !== undefined">
<i class="bi bi-server me-2"></i>{{getActiveCrossServer.host}}
</small>
<a role="button" class="navbarBtn text-body"
@click="store.ShowNavBar = !store.ShowNavBar"
style="line-height: 0; font-size: 2rem">
<i class="bi bi-list"></i></a>
</div>
</nav>
<Suspense>
<RouterView v-slot="{ Component }">
<Transition name="app" mode="out-in">
<Component :is="Component"></Component>
</Transition>
</RouterView>
</Suspense>
</template>
<style scoped>
.app-enter-active,
.app-leave-active {
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.app-enter-from{
transform: translateY(20px);
opacity: 0;
}
.app-leave-to {
transform: translateY(-20px);
opacity: 0;
}
@media screen and (min-width: 768px) {
.navbarBtn{
display: none;
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,228 @@
<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,
showDisplaySettings: false,
showMoreSettings: false
}
},
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 peerSearchContainer">
<RouterLink
to="create"
class="text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm">
<i class="bi bi-plus-lg me-2"></i>Peer
</RouterLink>
<button class="btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm"
@click="this.downloadAllPeer()">
<i class="bi bi-download me-2"></i> Download All
</button>
<div class="flex-grow-1 mt-3 mt-md-0">
<input class="form-control rounded-3 bg-secondary-subtle border-1 border-secondary-subtle shadow-sm w-100"
placeholder="Search..."
id="searchPeers"
@keyup="this.debounce()"
v-model="this.searchString">
</div>
<button
@click="this.showDisplaySettings = true"
class="btn text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
type="button" aria-expanded="false">
<i class="bi bi-filter-circle me-2"></i>
Display
</button>
<button class="btn text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
@click="this.showMoreSettings = true"
type="button" aria-expanded="false">
<i class="bi bi-three-dots"></i>
</button>
<Transition name="zoom">
<div
v-if="this.showDisplaySettings"
class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll displayModal">
<div class="container-md d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal">
<div class="card rounded-3 shadow w-100">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
<h4 class="mb-0 fw-normal">Display
</h4>
<button type="button" class="btn-close ms-auto" @click="this.showDisplaySettings = false"></button>
</div>
<div class="card-body px-4 pb-4 d-flex gap-3 flex-column">
<div>
<p class="text-muted fw-bold mb-2"><small>Sort by</small></p>
<div class="list-group">
<a v-for="(value, key) in this.sort" class="list-group-item list-group-item-action d-flex" role="button" @click="this.updateSort(key)">
<span class="me-auto">{{value}}</span>
<i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_sort === key"></i>
</a>
</div>
</div>
<div>
<p class="text-muted fw-bold mb-2"><small>Refresh interval</small></p>
<div class="list-group">
<a v-for="(value, key) in this.interval"
class="list-group-item list-group-item-action d-flex" role="button"
@click="this.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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Transition>
<Transition name="zoom">
<div
v-if="this.showMoreSettings"
class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll displayModal">
<div class="container-md d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal">
<div class="card rounded-3 shadow w-100">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
<h4 class="mb-0 fw-normal">Configuration Settings
</h4>
<button type="button" class="btn-close ms-auto" @click="this.showMoreSettings = false"></button>
</div>
<div class="card-body px-4 pb-4 d-flex gap-3 flex-column">
<div>
<p class="text-muted fw-bold mb-2"><small>Peer Jobs</small></p>
<div class="list-group">
<a class="list-group-item list-group-item-action d-flex" role="button"
@click="this.$emit('jobsAll')">
Active Jobs
</a>
<a class="list-group-item list-group-item-action d-flex" role="button"
@click="this.$emit('jobLogs')">
Logs
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</template>
<style scoped>
.animation__fadeInDropdown{
animation-name: fadeInDropdown;
animation-duration: 0.2s;
animation-timing-function: cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
@keyframes fadeInDropdown{
0%{
opacity: 0;
filter: blur(3px);
transform: translateY(-60px);
}
100%{
opacity: 1;
filter: blur(0px);
transform: translateY(-40px);
}
}
.displayModal .dashboardModal{
width: 400px !important;
}
@media screen and (max-width: 768px) {
.peerSearchContainer{
flex-direction: column;
}
.peerSettingContainer .dashboardModal{
width: 100% !important;
}
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,172 @@
<script>
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchPost} from "@/utilities/fetch.js";
import dayjs from "dayjs";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import VueDatePicker from '@vuepic/vue-datepicker';
export default {
name: "peerShareLinkModal",
props: {
peer: Object
},
components: {
VueDatePicker
},
data(){
return {
dataCopy: undefined,
loading: false
}
},
setup(){
const store = DashboardConfigurationStore();
return {store}
},
mounted() {
this.dataCopy = JSON.parse(JSON.stringify(this.peer.ShareLink)).at(0);
},
watch: {
'peer.ShareLink': {
deep: true,
handler(newVal, oldVal){
if (oldVal.length !== newVal.length){
this.dataCopy = JSON.parse(JSON.stringify(this.peer.ShareLink)).at(0);
}
}
}
},
methods: {
startSharing(){
this.loading = true;
fetchPost("/api/sharePeer/create", {
Configuration: this.peer.configuration.Name,
Peer: this.peer.id,
ExpireDate: dayjs().add(7, 'd').format("YYYY-MM-DD HH:mm:ss")
}, (res) => {
if (res.status){
this.peer.ShareLink = res.data;
this.dataCopy = res.data.at(0);
this.store.newMessage("Server", "Share link created successfully", "success")
}else{
this.store.newMessage("Server",
"Share link failed to create. Reason: " + res.message, "danger")
}
this.loading = false;
})
},
updateLinkExpireDate(){
fetchPost("/api/sharePeer/update", this.dataCopy, (res) => {
if (res.status){
this.dataCopy = res.data.at(0)
this.peer.ShareLink = res.data;
this.store.newMessage("Server", "Link expire date updated", "success")
}else{
this.store.newMessage("Server",
"Link expire date failed to update. Reason: " + res.message, "danger")
}
this.loading = false
});
},
stopSharing(){
this.loading = true;
this.dataCopy.ExpireDate = dayjs().format("YYYY-MM-DD HH:mm:ss")
this.updateLinkExpireDate()
},
parseTime(modelData){
if(modelData){
this.dataCopy.ExpireDate = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
}else{
this.dataCopy.ExpireDate = undefined
}
this.updateLinkExpireDate()
}
},
computed: {
getUrl(){
const crossServer = this.store.getActiveCrossServer();
if(crossServer){
return `${crossServer.host}/${this.$router.resolve(
{path: "/share", query: {"ShareID": this.dataCopy.ShareID}}).href}`
}
return window.location.origin
+ window.location.pathname
+ this.$router.resolve(
{path: "/share", query: {"ShareID": this.dataCopy.ShareID}}).href;
}
}
}
</script>
<template>
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
<div class="container d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 500px">
<div class="card rounded-3 shadow flex-grow-1">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
<h4 class="mb-0">Share Peer</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div>
<div class="card-body px-4 pb-4" v-if="this.peer.ShareLink">
<div v-if="!this.dataCopy">
<h6 class="mb-3 text-muted">
Currently the peer is not sharing
</h6>
<button
@click="this.startSharing()"
:disabled="this.loading"
class="w-100 btn bg-success-subtle text-success-emphasis border-1 border-success-subtle rounded-3 shadow-sm">
<span :class="{'animate__animated animate__flash animate__infinite animate__slower': this.loading}">
<i class="bi bi-send-fill me-2" ></i>
</span>
{{this.loading ? "Sharing...":"Start Sharing"}}
</button>
</div>
<div v-else>
<div class="d-flex gap-2 mb-4">
<i class="bi bi-link-45deg"></i>
<a :href="this.getUrl"
class="text-decoration-none" target="_blank">
{{ getUrl }}
</a>
</div>
<div class="d-flex flex-column gap-2 mb-3">
<small>
<i class="bi bi-calendar me-2"></i>
Expire Date
</small>
<VueDatePicker
:is24="true"
:min-date="new Date()"
:model-value="this.dataCopy.ExpireDate"
@update:model-value="this.parseTime" time-picker-inline
format="yyyy-MM-dd HH:mm:ss"
preview-format="yyyy-MM-dd HH:mm:ss"
:dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
/>
</div>
<button
@click="this.stopSharing()"
:disabled="this.loading"
class="w-100 btn bg-danger-subtle text-danger-emphasis border-1 border-danger-subtle rounded-3 shadow-sm">
<span :class="{'animate__animated animate__flash animate__infinite animate__slower': this.loading}">
<i class="bi bi-send-slash-fill me-2" ></i>
</span>
{{this.loading ? "Stop Sharing...":"Stop Sharing"}}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

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

View File

@ -0,0 +1,76 @@
<script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import ConfigurationCard from "@/components/configurationListComponents/configurationCard.vue";
export default {
name: "configurationList",
components: {ConfigurationCard},
async setup(){
const wireguardConfigurationsStore = WireguardConfigurationsStore();
return {wireguardConfigurationsStore}
},
data(){
return {
configurationLoaded: false
}
},
async mounted() {
await this.wireguardConfigurationsStore.getConfigurations();
this.configurationLoaded = true;
this.wireguardConfigurationsStore.ConfigurationListInterval = setInterval(() => {
this.wireguardConfigurationsStore.getConfigurations()
}, 10000)
},
beforeUnmount() {
clearInterval(this.wireguardConfigurationsStore.ConfigurationListInterval)
}
}
</script>
<template>
<div class="mt-md-5 mt-3">
<div class="container-md">
<div class="d-flex mb-4 configurationListTitle">
<h3 class="text-body d-flex">
<i class="bi bi-body-text me-2"></i>
<span>WireGuard Configurations</span></h3>
<RouterLink to="/new_configuration" class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto rounded-3">
<i class="bi bi-plus-circle-fill me-2"></i>
Configuration
</RouterLink>
</div>
<Transition name="fade" mode="out-in">
<div v-if="this.configurationLoaded">
<p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0">
You don't have any WireGuard configurations yet. Please check the configuration folder or change it in "Settings". By default the folder is "/etc/wireguard".
</p>
<div class="d-flex gap-3 flex-column mb-3" v-else>
<ConfigurationCard v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name" :c="c"></ConfigurationCard>
</div>
</div>
</Transition>
</div>
</div>
</template>
<style scoped>
@media screen and (max-width: 768px) {
.configurationListTitle{
flex-direction: column;
gap: 0.5rem;
h3 span{
margin-left: auto !important;
}
.btn{
width: 100%;
}
}
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,140 @@
<script>
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchGet} from "@/utilities/fetch.js";
export default {
name: "navbar",
setup(){
const wireguardConfigurationsStore = WireguardConfigurationsStore();
const dashboardConfigurationStore = DashboardConfigurationStore();
return {wireguardConfigurationsStore, dashboardConfigurationStore}
},
data(){
return {
updateAvailable: false,
updateMessage: "Checking for update...",
updateUrl: ""
}
},
mounted() {
fetchGet("/api/getDashboardUpdate", {}, (res) => {
if (res.status){
if (res.data){
this.updateAvailable = true
this.updateUrl = res.data
}
this.updateMessage = res.message
}else{
this.updateMessage = "Failed to check available update"
console.log(`Failed to get update: ${res.message}`)
}
})
}
}
</script>
<template>
<div class="col-md-3 col-lg-2 d-md-block p-3 navbar-container"
:class="{active: this.dashboardConfigurationStore.ShowNavBar}"
:data-bs-theme="dashboardConfigurationStore.Configuration.Server.dashboard_theme"
style="height: calc(-50px + 100vh);">
<nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow overflow-y-scroll" >
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column px-2">
<li class="nav-item">
<RouterLink class="nav-link rounded-3"
to="/" exact-active-class="active">
<i class="bi bi-house me-2"></i>
Home</RouterLink></li>
<li class="nav-item">
<RouterLink class="nav-link rounded-3" to="/settings"
exact-active-class="active">
<i class="bi bi-gear me-2"></i>
Settings</RouterLink></li>
</ul>
<hr class="text-body">
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted text-center">
<i class="bi bi-body-text me-2"></i>
Configurations
</h6>
<ul class="nav flex-column px-2">
<li class="nav-item">
<RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link rounded-3"
active-class="active"
v-for="c in this.wireguardConfigurationsStore.Configurations">
<span class="dot me-2" :class="{active: c.Status}"></span>
{{c.Name}}
</RouterLink>
</li>
</ul>
<hr class="text-body">
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted text-center">
<i class="bi bi-tools me-2"></i>
Tools
</h6>
<ul class="nav flex-column px-2">
<li class="nav-item">
<RouterLink to="/ping" class="nav-link rounded-3" active-class="active">Ping</RouterLink></li>
<li class="nav-item">
<RouterLink to="/traceroute" class="nav-link rounded-3" active-class="active">Traceroute</RouterLink>
</li>
</ul>
<hr class="text-body">
<ul class="nav flex-column px-2">
<li class="nav-item"><a class="nav-link text-danger rounded-3"
@click="this.dashboardConfigurationStore.signOut()"
role="button" style="font-weight: bold">
<i class="bi bi-box-arrow-left me-2"></i>
Sign Out</a>
</li>
<li class="nav-item" style="font-size: 0.8rem">
<a :href="this.updateUrl" v-if="this.updateAvailable" class="text-decoration-none" target="_blank">
<small class="nav-link text-muted rounded-3" >
{{ this.updateMessage }}
</small>
</a>
<small class="nav-link text-muted" v-else>
{{ this.updateMessage }}
</small>
</li>
</ul>
</div>
</nav>
</div>
</template>
<style scoped>
@media screen and (max-width: 768px) {
.navbar-container{
position: absolute;
z-index: 1000;
animation-duration: 0.4s;
animation-fill-mode: both;
display: none;
animation-timing-function: cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.navbar-container.active{
animation-direction: normal;
display: block !important;
animation-name: zoomInFade
}
}
@keyframes zoomInFade {
0%{
opacity: 0;
transform: translateY(60px);
filter: blur(3px);
}
100%{
opacity: 1;
transform: translateY(0px);
filter: blur(0px);
}
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,63 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "accountSettingsMFA",
setup(){
const store = DashboardConfigurationStore();
const uuid = `input_${v4()}`;
return {store, uuid};
},
data(){
return {
status: false
}
},
mounted() {
this.status = this.store.Configuration.Account["enable_totp"]
},
methods: {
async resetMFA(){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Account",
key: "totp_verified",
value: "false"
}, async (res) => {
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Account",
key: "enable_totp",
value: "false"
}, (res) => {
if (res.status){
this.$router.push("/2FASetup")
}
})
})
}
}
}
</script>
<template>
<div>
<div class="d-flex align-items-center">
<strong>Multi-Factor Authentication</strong>
<div class="form-check form-switch ms-3">
<input class="form-check-input" type="checkbox"
v-model="this.status"
role="switch" id="allowMFAKeysSwitch">
</div>
<button class="btn bg-warning-subtle text-warning-emphasis border-1 border-warning-subtle ms-auto rounded-3 shadow-sm"
v-if="this.status" @click="this.resetMFA()">
<i class="bi bi-shield-lock-fill me-2"></i>
{{this.store.Configuration.Account["totp_verified"] ? "Reset" : "Setup" }} MFA
</button>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,130 @@
<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" v-if="!this.store.getActiveCrossServer()">
<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"
v-if="!this.store.getActiveCrossServer()"
>
<i class="bi bi-key me-2"></i> Create
</button>
<div class="card" style="height: 300px" v-if="this.apiKeys.length === 0">
<div class="card-body d-flex text-muted">
<span class="m-auto">
No Dashboard API Key
</span>
</div>
</div>
<div class="d-flex flex-column gap-2 position-relative" v-else style="min-height: 300px">
<TransitionGroup name="apiKey">
<DashboardAPIKey v-for="key in this.apiKeys" :apiKey="key"
:key="key.Key"
@deleted="(nkeys) => this.apiKeys = nkeys"></DashboardAPIKey>
</TransitionGroup>
</div>
<Transition name="zoomReversed">
<NewDashboardAPIKey v-if="this.newDashboardAPIKey"
@created="(data) => this.apiKeys = data"
@close="this.newDashboardAPIKey = false"
></NewDashboardAPIKey>
</Transition>
</div>
</div>
</template>
<style scoped>
.apiKey-move, /* apply transition to moving elements */
.apiKey-enter-active,
.apiKey-leave-active {
transition: all 0.5s ease;
}
.apiKey-enter-from,
.apiKey-leave-to {
opacity: 0;
transform: translateY(30px) scale(0.9);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.apiKey-leave-active {
position: absolute;
width: 100%;
}
</style>

View File

@ -0,0 +1,89 @@
<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 apiKey-card-body" v-if="!this.confirmDelete">
<div class="d-flex align-items-center gap-2">
<small class="text-muted">Key</small>
<span style="word-break: break-all">{{this.apiKey.Key}}</span>
</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"
v-if="!this.store.getActiveCrossServer()"
@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"
v-if="!this.store.getActiveCrossServer()">
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>
@media screen and (max-width: 992px) {
.apiKey-card-body{
flex-direction: column !important;
align-items: start !important;
div.ms-auto{
margin-left: 0 !important;
}
div{
width: 100%;
align-items: start !important;
}
small{
margin-right: auto;
}
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,119 @@
<script>
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import QRCode from "qrcode";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "totp",
async setup(){
const store = DashboardConfigurationStore();
let l = ""
await fetchGet("/api/Welcome_GetTotpLink", {}, (res => {
if (res.status) l = res.data;
}));
return {l, store}
},
mounted() {
if (this.l) {
QRCode.toCanvas(document.getElementById('qrcode'), this.l, function (error) {})
}
},
data(){
return {
totp: "",
totpInvalidMessage: "",
verified: false
}
},
methods: {
validateTotp(){
}
},
watch: {
totp(newVal){
const input = document.querySelector("#totp");
input.classList.remove("is-invalid", "is-valid")
if (newVal.length === 6){
console.log(newVal)
if (/[0-9]{6}/.test(newVal)){
fetchPost("/api/Welcome_VerifyTotpLink", {
totp: newVal
}, (res) => {
if (res.status){
this.verified = true;
input.classList.add("is-valid");
this.$emit("verified")
}else{
input.classList.add("is-invalid")
this.totpInvalidMessage = "TOTP does not match."
}
})
}else{
input.classList.add("is-invalid");
this.totpInvalidMessage = "TOTP can only contain numbers"
}
}
}
}
}
</script>
<template>
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
:data-bs-theme="this.store.Configuration.Server.dashboard_theme">
<div class="m-auto text-body" style="width: 500px">
<div class="d-flex flex-column">
<div>
<h1 class="dashboardLogo display-4">Multi-Factor Authentication</h1>
<p class="mb-2"><small class="text-muted">1. Please scan the following QR Code to generate TOTP</small></p>
<canvas id="qrcode" class="rounded-3 mb-2"></canvas>
<div class="p-3 bg-body-secondary rounded-3 border mb-3">
<p class="text-muted mb-0"><small>Or you can click the link below:</small>
</p><a :href="this.l"><code style="line-break: anywhere">{{this.l}}</code></a>
</div>
<label for="totp" class="mb-2"><small class="text-muted">2. Enter the TOTP generated by your authenticator to verify</small></label>
<div class="form-group mb-2">
<input class="form-control text-center totp"
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
v-model="this.totp"
:disabled="this.verified"
>
<div class="invalid-feedback">
{{this.totpInvalidMessage}}
</div>
<div class="valid-feedback">
TOTP verified!
</div>
</div>
<div class="alert alert-warning rounded-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i> If you ever lost your TOTP and can't login, please follow instruction on
<a href="https://github.com/donaldzou/WGDashboard" target="_blank">readme.md</a> to reset.
</div>
</div>
<hr>
<div class="d-flex gap-3 mt-5 flex-column">
<RouterLink
to="/"
v-if="!this.verified"
class="btn bg-secondary-subtle text-secondary-emphasis
rounded-3
flex-grow-1 btn-lg border-1 border-secondary-subtle shadow d-flex">
I don't need MFA <i class="bi bi-chevron-right ms-auto"></i>
</RouterLink>
<RouterLink
to="/"
v-else class="btn btn-dark btn-lg d-flex btn-brand shadow align-items-center flex-grow-1 rounded-3">
Complete <i class="bi bi-chevron-right ms-auto"></i>
</RouterLink>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,165 @@
<script>
import dayjs from "dayjs";
export default {
name: "RemoteServer",
props: {
server: Object
},
data(){
return{
active: false,
startTime: undefined,
endTime: undefined,
errorMsg: "",
refreshing: false
}
},
methods: {
async handshake(){
this.active = false;
if (this.server.host && this.server.apiKey){
this.refreshing = true;
this.startTime = undefined;
this.endTime = undefined;
this.startTime = dayjs()
await fetch(`${this.server.host}/api/handshake`, {
headers: {
"content-type": "application/json",
"wg-dashboard-apikey": this.server.apiKey
},
method: "GET",
signal: AbortSignal.timeout(5000)
}).then(res => {
if (res.status === 200){
return res.json()
}
throw new Error(res.statusText)
}).then(() => {
this.endTime = dayjs()
this.active = true;
}).catch((res) => {
this.active = false;
this.errorMsg = res;
});
this.refreshing = false;
}
},
async connect(){
await fetch(`${this.server.host}/api/authenticate`, {
headers: {
"content-type": "application/json",
"wg-dashboard-apikey": this.server.apiKey
},
body: JSON.stringify({
host: window.location.hostname
}),
method: "POST",
signal: AbortSignal.timeout(5000),
}).then(res => res.json()).then(res => {
this.$emit("setActiveServer")
this.$router.push('/')
})
}
},
mounted() {
this.handshake()
},
computed: {
getHandshakeTime(){
if (this.startTime && this.endTime){
return `${dayjs().subtract(this.startTime).millisecond()}ms`
}else{
if (this.refreshing){
return `Pinging...`
}
return this.errorMsg ? this.errorMsg : "N/A"
}
}
}
}
</script>
<template>
<div class="card rounded-3">
<div class="card-body">
<div class="d-flex gap-3 w-100 remoteServerContainer">
<div class="d-flex gap-3 align-items-center flex-grow-1">
<i class="bi bi-server"></i>
<input class="form-control form-control-sm"
@blur="this.handshake()"
v-model="this.server.host"
type="url">
</div>
<div class="d-flex gap-3 align-items-center flex-grow-1">
<i class="bi bi-key-fill"></i>
<input class="form-control form-control-sm"
@blur="this.handshake()"
v-model="this.server.apiKey"
type="text">
</div>
<div class="d-flex gap-2 button-group">
<button
@click="this.$emit('delete')"
class="ms-auto btn btn-sm bg-danger-subtle text-danger-emphasis border-1 border-danger-subtle">
<i class="bi bi-trash"></i>
</button>
<button
@click="this.connect()"
:class="{disabled: !this.active}"
class="ms-auto btn btn-sm bg-success-subtle text-success-emphasis border-1 border-success-subtle">
<i class="bi bi-arrow-right-circle"></i>
</button>
</div>
</div>
</div>
<div class="card-footer gap-2 d-flex align-items-center">
<span class="dot ms-0 me-2" :class="[this.active ? 'active':'inactive']"></span>
<small>{{this.getHandshakeTime}}</small>
<div class="spin ms-auto text-primary-emphasis" v-if="this.refreshing">
<i class="bi bi-arrow-clockwise"></i>
</div>
<a role="button"
v-else
@click="this.handshake()"
class="text-primary-emphasis text-decoration-none ms-auto disabled">
<i class="bi bi-arrow-clockwise me"></i>
</a>
</div>
</div>
</template>
<style scoped>
.dot.inactive{
background-color: #dc3545;
box-shadow: 0 0 0 0.2rem #dc354545;
}
.spin{
animation: spin 1s infinite cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
@keyframes spin {
0%{
transform: rotate(0deg);
}
100%{
transform: rotate(360deg);
}
}
@media screen and (max-width: 768px) {
.remoteServerContainer{
flex-direction: column;
}
.remoteServerContainer .button-group button{
width: 100%;
}
}
</style>

View File

@ -0,0 +1,40 @@
<script>
import RemoteServer from "@/components/signInComponents/RemoteServer.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "RemoteServerList",
setup(){
const store = DashboardConfigurationStore();
return {store}
},
components: {RemoteServer}
}
</script>
<template>
<div class="w-100 mt-3">
<div class="d-flex align-items-center mb-3">
<h5 class="mb-0">Server List</h5>
<button
@click="this.store.addCrossServerConfiguration()"
class="btn bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle shadow-sm ms-auto">
<i class="bi bi-plus-circle-fill me-2"></i>Server
</button>
</div>
<div class="w-100 d-flex gap-3 flex-column p-3 border border-1 border-secondary-subtle rounded-3"
style="height: 400px; overflow-y: scroll">
<RemoteServer v-for="(server, key) in this.store.CrossServerConfiguration.ServerList"
@setActiveServer="this.store.setActiveCrossServer(key)"
@delete="this.store.deleteCrossServerConfiguration(key)"
:key="key"
:server="server"></RemoteServer>
<h6 class="text-muted m-auto" v-if="Object.keys(this.store.CrossServerConfiguration.ServerList).length === 0">
Click<i class="bi bi-plus-circle-fill mx-1"></i>to add your server</h6>
</div>
</div>
</template>
<style scoped>
</style>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,89 @@
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
},
CrossServerConfiguration:{
Enable: false,
ServerList: {}
},
ActiveServerConfiguration: undefined,
IsElectronApp: false,
ShowNavBar: false
}),
actions: {
initCrossServerConfiguration(){
const currentConfiguration = localStorage.getItem('CrossServerConfiguration');
if (localStorage.getItem("ActiveCrossServerConfiguration") !== null){
this.ActiveServerConfiguration = localStorage.getItem("ActiveCrossServerConfiguration");
}
if (currentConfiguration === null){
window.localStorage.setItem('CrossServerConfiguration', JSON.stringify(this.CrossServerConfiguration))
}else{
this.CrossServerConfiguration = JSON.parse(currentConfiguration)
}
},
syncCrossServerConfiguration(){
window.localStorage.setItem('CrossServerConfiguration', JSON.stringify(this.CrossServerConfiguration))
},
addCrossServerConfiguration(){
this.CrossServerConfiguration.ServerList[v4().toString()] = {host: "", apiKey: "", active: false}
},
deleteCrossServerConfiguration(key){
delete this.CrossServerConfiguration.ServerList[key];
},
getActiveCrossServer(){
const key = localStorage.getItem('ActiveCrossServerConfiguration');
if (key !== null){
return this.CrossServerConfiguration.ServerList[key]
}
return undefined
},
setActiveCrossServer(key){
this.ActiveServerConfiguration = key;
localStorage.setItem('ActiveCrossServerConfiguration', key)
},
removeActiveCrossServer(){
this.ActiveServerConfiguration = undefined;
localStorage.removeItem('ActiveCrossServerConfiguration')
},
async getConfiguration(){
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.removeActiveCrossServer();
this.$router.go('/signin')
});
},
newMessage(from, content, type){
this.Messages.push({
id: v4(),
from: from,
content: content,
type: type,
show: true
})
}
}
});

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,75 @@
import router from "@/router/index.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
const getHeaders = () => {
let headers = {
"content-type": "application/json"
}
const store = DashboardConfigurationStore();
const apiKey = store.getActiveCrossServer();
if (apiKey){
headers['wg-dashboard-apikey'] = apiKey.apiKey
}
return headers
}
const getUrl = (url) => {
const store = DashboardConfigurationStore();
const apiKey = store.getActiveCrossServer();
if (apiKey){
return `${apiKey.host}${url}`
}
// console.log("URL fetching: ", import.meta.env.MODE === 'development' ? url
// : `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`)
return import.meta.env.MODE === 'development' ? url
: `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`
}
export const fetchGet = async (url, params=undefined, callback=undefined) => {
const urlSearchParams = new URLSearchParams(params);
await fetch(`${getUrl(url)}?${urlSearchParams.toString()}`, {
headers: getHeaders()
})
.then((x) => {
const store = DashboardConfigurationStore();
if (!x.ok){
if (x.status !== 200){
if (x.status === 401){
store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
router.push({path: '/signin'})
})
}
export const fetchPost = async (url, body, callback) => {
await fetch(`${getUrl(url)}`, {
headers: getHeaders(),
method: "POST",
body: JSON.stringify(body)
}).then((x) => {
const store = DashboardConfigurationStore();
if (!x.ok){
if (x.status !== 200){
if (x.status === 401){
store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
router.push({path: '/signin'})
})
}

View File

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

View File

@ -230,8 +230,8 @@
var offset = 0; var offset = 0;
for (var i = 0; i < files.length; ++i) { for (var i = 0; i < files.length; ++i) {
var name = encodeString(files[i].filename); var name = encodeString(files[i].fileName);
var contents = encodeString(files[i].content); var contents = encodeString(files[i].file);
var crc = crc32(contents); var crc = crc32(contents);
putU32(b, 0x04034b50); /* signature */ putU32(b, 0x04034b50); /* signature */
@ -297,12 +297,12 @@
return keyToBase64(generatePublicKey(privateKey)); return keyToBase64(generatePublicKey(privateKey));
}, },
generateZipFiles: function(res){ generateZipFiles: function(res, zipFileName){
var files = res.peers; var files = res.data;
var zipFile = createZipFile(files); var zipFile = createZipFile(files);
var blob = new Blob([zipFile], { type: "application/zip" }); var blob = new Blob([zipFile], { type: "application/zip" });
var a = document.createElement("a"); var a = document.createElement("a");
a.download = res.filename; a.download = zipFileName;
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
a.style.display = "none"; a.style.display = "none";
document.body.appendChild(a); document.body.appendChild(a);

View File

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

View File

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

View File

@ -0,0 +1,295 @@
<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")
this.loading = false;
}
})
}
}
},
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-5">
<div class="container mb-4">
<div class="mb-4 d-flex align-items-center gap-4">
<RouterLink to="/" class="text-decoration-none">
<h3 class="mb-0 text-body">
<i class="bi bi-chevron-left me-4"></i> New Configuration
</h3>
</RouterLink>
</div>
<form class="text-body d-flex flex-column gap-3"
@submit="(e) => {e.preventDefault(); this.saveNewConfiguration();}"
>
<div class="card rounded-3 shadow">
<div class="card-header">Configuration Name</div>
<div class="card-body">
<input type="text" class="form-control" placeholder="ex. wg1" id="ConfigurationName"
v-model="this.newConfiguration.ConfigurationName"
:disabled="this.loading"
required>
<div class="invalid-feedback">
<div v-if="this.error">{{this.errorMessage}}</div>
<div v-else>
Configuration name is invalid. Possible reasons:
<ul class="mb-0">
<li>Configuration name already exist.</li>
<li>Configuration name can only contain 15 lower/uppercase alphabet, numbers, "_"(underscore), "="(equal), "+"(plus), "."(period/dot), "-"(dash/hyphen)</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card rounded-3 shadow">
<div class="card-header">Private Key / Public Key / Pre-Shared Key</div>
<div class="card-body" style="font-family: var(--bs-font-monospace)">
<div class="mb-2">
<label class="text-muted fw-bold mb-1"><small>PRIVATE KEY</small></label>
<div class="input-group">
<input type="text" class="form-control" id="PrivateKey" required
:disabled="this.loading"
v-model="this.newConfiguration.PrivateKey" disabled
>
<button class="btn btn-outline-primary" type="button"
title="Regenerate Private Key"
@click="wireguardGenerateKeypair()"
>
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div>
<label class="text-muted fw-bold mb-1"><small>PUBLIC KEY</small></label>
<input type="text" class="form-control" id="PublicKey"
v-model="this.newConfiguration.PublicKey" disabled
>
</div>
</div>
</div>
<div class="card rounded-3 shadow">
<div class="card-header">Listen Port</div>
<div class="card-body">
<input type="number" class="form-control" placeholder="0-65353" id="ListenPort"
min="1"
max="65353"
v-model="this.newConfiguration.ListenPort"
:disabled="this.loading"
required>
<div class="invalid-feedback">
<div v-if="this.error">{{this.errorMessage}}</div>
<div v-else>
Invalid port
</div>
</div>
</div>
</div>
<div class="card rounded-3 shadow">
<div class="card-header d-flex align-items-center">
IP Address & Range
<span class="badge rounded-pill text-bg-success ms-auto">{{ numberOfAvailableIPs }} Available IPs</span>
</div>
<div class="card-body">
<input type="text" class="form-control"
placeholder="Ex: 10.0.0.1/24" id="Address"
v-model="this.newConfiguration.Address"
:disabled="this.loading"
required>
<div class="invalid-feedback">
<div v-if="this.error">{{this.errorMessage}}</div>
<div v-else>
IP address & range is invalid.
</div>
</div>
</div>
</div>
<hr>
<div class="accordion" id="newConfigurationOptionalAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#newConfigurationOptionalAccordionCollapse">
Optional Settings
</button>
</h2>
<div id="newConfigurationOptionalAccordionCollapse"
class="accordion-collapse collapse" data-bs-parent="#newConfigurationOptionalAccordion">
<div class="accordion-body d-flex flex-column gap-3">
<div class="card rounded-3">
<div class="card-header">PreUp</div>
<div class="card-body">
<input type="text" class="form-control" id="preUp" v-model="this.newConfiguration.PreUp">
</div>
</div>
<div class="card rounded-3">
<div class="card-header">PreDown</div>
<div class="card-body">
<input type="text" class="form-control" id="preDown" v-model="this.newConfiguration.PreDown">
</div>
</div>
<div class="card rounded-3">
<div class="card-header">PostUp</div>
<div class="card-body">
<input type="text" class="form-control" id="postUp" v-model="this.newConfiguration.PostUp">
</div>
</div>
<div class="card rounded-3">
<div class="card-header">PostDown</div>
<div class="card-body">
<input type="text" class="form-control" id="postDown" v-model="this.newConfiguration.PostDown">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- <RouterLink to="/new_configuration" class="btn btn-success rounded-3 shadow ms-auto rounded-3">-->
<!-- <i class="bi bi-save me-2"></i>-->
<!-- Save-->
<!-- </RouterLink>-->
<button class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto"
:disabled="!this.goodToSubmit">
<span v-if="this.success" class="d-flex w-100">
Success! <i class="bi bi-check-circle-fill ms-2"></i>
</span>
<span v-else-if="!this.loading" class="d-flex w-100">
Save Configuration <i class="bi bi-save-fill ms-2"></i>
</span>
<span v-else class="d-flex w-100 align-items-center">
Saving...
<span class="ms-2 spinner-border spinner-border-sm" role="status">
<!-- <span class="visually-hidden">Loading...</span>-->
</span>
</span>
</button>
</form>
</div>
</div>
</template>
<style scoped>
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,63 @@
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(({mode}) => {
if (mode === 'electron'){
return {
emptyOutDir: false,
base: './',
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
outDir: '../../../../WGDashboard-Desktop',
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
}
}
}
return {
base: "/static/app/dist",
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server:{
proxy: {
'/api': proxy
},
host: '0.0.0.0'
},
build: {
outDir: 'dist',
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
}
}
})

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