mirror of
https://github.com/WGDashboard/WGDashboard.git
synced 2026-08-04 15:03:02 +00:00
Compare commits
62 Commits
v4.0.beta1
...
v4.0.beta3
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a34a0eb40 | |||
| e3f82e136a | |||
| 8a7df4ba9f | |||
| e86d1a4c7a | |||
| 5b9d0b60a1 | |||
| 7eff2f0c49 | |||
| 97236bb01d | |||
| 96ccb03eea | |||
| 55f55820c5 | |||
| 955839d513 | |||
| a650e628e5 | |||
| 54142b73fb | |||
| 55e0d2695d | |||
| 2f90ab15dc | |||
| fd3fc66bfc | |||
| a352a94d8a | |||
| 410b81f46f | |||
| aa3711c5cc | |||
| d6b1f97a04 | |||
| b4e8e57a22 | |||
| 9644e6195c | |||
| 764e0c7607 | |||
| 97d640dd40 | |||
| d2915b5b05 | |||
| f274f6fd18 | |||
| f507ac2569 | |||
| 208cbd6d89 | |||
| fa2d7fa3da | |||
| 7463767781 | |||
| 958bc864c9 | |||
| 4484668750 | |||
| d5dea4b87f | |||
| 0fdef6a0a2 | |||
| bd71b6bad8 | |||
| 9b7887b279 | |||
| 3960e43872 | |||
| 201c8f9ec9 | |||
| 8c8374a08c | |||
| 467595afc9 | |||
| acb54f098c | |||
| 5755d13460 | |||
| 2c3500315d | |||
| 47ea60c0cd | |||
| 18b18c1396 | |||
| ff227de5fa | |||
| 6799692811 | |||
| c6173f7f6f | |||
| d0e4dabc44 | |||
| f815dae300 | |||
| b3bd6bb39e | |||
| 71df6409c2 | |||
| e4f9a1e0cf | |||
| ca6a05e393 | |||
| c0d26164dc | |||
| 76fe2a1ba9 | |||
| 8cbdb54402 | |||
| 764ef80a62 | |||
| 0c37d93c01 | |||
| c57a5128e5 | |||
| 6cf4eba20a | |||
| 6825d728c2 | |||
| 6d3091b2a2 |
@@ -48,5 +48,4 @@ coverage
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
proxy.js
|
||||
.vite/*
|
||||
@@ -106,9 +106,7 @@
|
||||
# Must have for each peer
|
||||
```
|
||||
|
||||
- Python 3.7+ & Pip3
|
||||
|
||||
- Browser support CSS3 and ES6
|
||||
- **Python 3.10** for v4.0+, **Python 3.7 - 3.9** for v2.0 - v3.0.6.2
|
||||
|
||||
## 🛠 Install
|
||||
1. Download WGDashboard
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# WG-Dashboard Docker Explanation:
|
||||
|
||||
Author: DaanSelen<br>
|
||||
|
||||
This document delves into how the WG-Dashboard Docker container has been built.<br>
|
||||
Of course there are two stages, one before run-time and one at/after run-time.<br>
|
||||
The `Dockerfile` describes how the container image is made, and the `entrypoint.sh` is executed after running the container. <br>
|
||||
In this example, WireGuard is integrated into the container itself, so it should be a run-and-go.<br>
|
||||
For more details on the source-code specific to this Docker image, refer to the source files, they have lots of comments.
|
||||
|
||||
I have tried to embed some new features such as `isolated_peers` and interface startup on container-start (through `enable_wg0`).
|
||||
|
||||
<img src="https://raw.githubusercontent.com/donaldzou/WGDashboard/main/img/logo.png" alt="WG-Dashboard Logo" title="WG-Dashboard Logo" width="150" height="150" />
|
||||
|
||||
## Getting the container running:
|
||||
|
||||
To get the container running you either pull the image from the repository, at the moment: `repo.nerthus.nl/app/wireguard-dashboard:latest`.<br>
|
||||
From there either use the environment variables describe below as parameters or use the Docker Compose file: `compose.yaml`.
|
||||
|
||||
An example of a simple command to get the container running is show below:<br>
|
||||
|
||||
```shell
|
||||
docker run -d \
|
||||
--name wireguard-dashboard \
|
||||
--restart unless-stopped \
|
||||
-e enable_wg0=true \
|
||||
-e isolated_peers=true \
|
||||
-p 10086:10086/tcp \
|
||||
-p 51820:51820/udp \
|
||||
--cap-add NET_ADMIN \
|
||||
repo.nerthus.nl/app/wireguard-dashboard:latest
|
||||
```
|
||||
<br>
|
||||
If you want to use Compose instead of a raw Docker command, refer to the example in the `compose.yaml` or the one pasted below:
|
||||
<br><br>
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wireguard-dashboard:
|
||||
image: repo.nerthus.nl/app/wireguard-dashboard:latest
|
||||
restart: unless-stopped
|
||||
container_name: wire-dash
|
||||
environment:
|
||||
#- tz=
|
||||
#- global_dns=
|
||||
- enable_wg0=true
|
||||
- isolated_peers=false
|
||||
#- public_ip=
|
||||
ports:
|
||||
- 10086:10086/tcp
|
||||
- 51820:51820/udp
|
||||
volumes:
|
||||
- conf:/etc/wireguard
|
||||
- app:/opt/wireguarddashboard/app
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
volumes:
|
||||
conf:
|
||||
app:
|
||||
|
||||
```
|
||||
|
||||
If you want to customize the yaml, make sure the core stays the same, but for example volume PATHs can be freely changed.<br>
|
||||
This setup is just generic and will use the Docker volumes.
|
||||
|
||||
## Working with the container and environment variables:
|
||||
|
||||
Once the container is running, the installation process is essentially the same as running it on bare-metal.<br>
|
||||
So go to the assign TCP port in this case HTTP, like the default 10086 one in the example and log into the WEB-GUI.<br>
|
||||
|
||||
| Environment variable | Accepted arguments | Default value | Verbose |
|
||||
| -------------- | ------- | ------- | ------- |
|
||||
| tz | Europe/Amsterdam or any confirming timezone notation. | Europe/Amsterdam | Sets the timezone of the Docker container. This is to timesync the container to any other processes which would need it. |
|
||||
| global_dns | Any IPv4 address, such as my personal recommendation: 9.9.9.9 (QUAD9) | 1.1.1.1 | Set the default DNS given to clients once they connect to the WireGuard tunnel (VPN).
|
||||
| enable_wg0 | `true` or `false` | `false` | Enables or disables the starting of the WireGuard interface on container 'boot-up'.
|
||||
| isolated_peers | `true` or `false` | `true` | For security the default is true, and it disables peers to ping or reach eachother, the WireGuard interface IS able to reach the peers (Done through `iptables`).
|
||||
| public_ip | Any IPv4 (public recommended) address, such as the one returned by default | Default uses the return of `curl ifconfig.me` | To reach your VPN from outside your own network, you need WG-Dashboard to know what your public IP-address is, otherwise it will generate faulty config files for clients.
|
||||
|
||||
## Closing remarks:
|
||||
|
||||
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Pull from small Debian stable image.
|
||||
FROM debian:stable-slim
|
||||
LABEL maintainer="dselen@nerthus.nl"
|
||||
|
||||
# Declaring environment variables, change Peernet to an address you like, standard is a 24 bit subnet.
|
||||
ENV wg_net="10.0.0.1"
|
||||
# wg_net is used functionally as an ARG for its environment variable nature, do not change unless you know what you are doing.
|
||||
|
||||
# Following ENV variables are changable on container runtime because /entrypoint.sh handles that. See compose.yaml for more info.
|
||||
ENV tz="Europe/Amsterdam"
|
||||
ENV global_dns="1.1.1.1"
|
||||
ENV enable_wg0="false"
|
||||
ENV isolated_peers="true"
|
||||
ENV public_ip="0.0.0.0"
|
||||
|
||||
# Doing basic system maintenance. Change the timezone to the desired timezone.
|
||||
RUN ln -sf /usr/share/zoneinfo/${tz} /etc/localtime
|
||||
|
||||
# Doing package management operations, such as upgrading
|
||||
RUN apt-get update && apt-get upgrade -y \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
git \
|
||||
iproute2 \
|
||||
iptables \
|
||||
iputils-ping \
|
||||
openresolv \
|
||||
procps \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
traceroute \
|
||||
wireguard \
|
||||
wireguard-tools \
|
||||
&& apt-get remove linux-image-* --autoremove -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Removing the Linux Image package to preserve space on the image, for this reason also deleting apt lists, to be able to install packages: run apt update.
|
||||
|
||||
# Using WGDASH -- like wg_net functionally as a ARG command. But it is needed in entrypoint.sh so it needs to be exported as environment variable.
|
||||
ENV WGDASH=/opt/wireguarddashboard
|
||||
RUN python3 -m venv ${WGDASH}/venv
|
||||
|
||||
# Doing WireGuard Dashboard installation measures. Modify the git clone command to get the preferred version, with a specific branch for example.
|
||||
RUN . ${WGDASH}/venv/bin/activate \
|
||||
&& git clone https://github.com/donaldzou/WGDashboard.git ${WGDASH}/app \
|
||||
&& pip3 install -r ${WGDASH}/app/src/requirements.txt \
|
||||
&& chmod +x ${WGDASH}/app/src/wgd.sh \
|
||||
&& .${WGDASH}/app/src/wgd.sh install
|
||||
|
||||
# Set the volume to be used for persistency.
|
||||
VOLUME /etc/wireguard
|
||||
|
||||
# Generate basic WireGuard interface. Echoing the WireGuard interface config for readability, adjust if you want it for efficiency.
|
||||
# Also setting the pipefail option, verbose: https://github.com/hadolint/hadolint/wiki/DL4006.
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
RUN wg genkey | tee /etc/wireguard/wg0_privatekey \
|
||||
&& echo "[Interface]" > /wg0.conf \
|
||||
&& echo "SaveConfig = true" >> /wg0.conf \
|
||||
&& echo "Address = ${wg_net}/24" >> /wg0.conf \
|
||||
&& echo "PrivateKey = $(cat /etc/wireguard/wg0_privatekey)" >> /wg0.conf \
|
||||
&& echo "PostUp = iptables -t nat -I POSTROUTING 1 -s ${wg_net}/24 -o $(ip -o -4 route show to default | awk '{print $NF}') -j MASQUERADE" >> /wg0.conf \
|
||||
&& echo "PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP" >> /wg0.conf \
|
||||
&& echo "PreDown = iptables -t nat -D POSTROUTING 1" >> /wg0.conf \
|
||||
&& echo "PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP" >> /wg0.conf \
|
||||
&& echo "ListenPort = 51820" >> /wg0.conf \
|
||||
#&& echo "DNS = ${global_dns}" >> /wg0.conf \
|
||||
&& rm /etc/wireguard/wg0_privatekey
|
||||
|
||||
# Defining a way for Docker to check the health of the container. In this case: checking the login URL.
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD curl -f http://localhost:10086/signin || exit 1
|
||||
|
||||
# Copy the basic entrypoint.sh script.
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Exposing the default WireGuard Dashboard port for web access.
|
||||
EXPOSE 10086
|
||||
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
wireguard-dashboard:
|
||||
image: repo.nerthus.nl/app/wireguard-dashboard:latest
|
||||
restart: unless-stopped
|
||||
container_name: wire-dash
|
||||
environment:
|
||||
#- tz= # <--- Set container timezone, default: Europe/Amsterdam.
|
||||
#- global_dns= # <--- Set global DNS address, default: 1.1.1.1.
|
||||
- enable_wg0=true # <--- If true, wg0 will be started on container startup. default: false.
|
||||
- isolated_peers=false # <--- When set to true, it disallows peers to talk to eachother, setting to false, allows it, default: true.
|
||||
#- public_ip= # <--- Set public IP to ensure the correct one is chosen, defaulting to the IP give by ifconfig.me.
|
||||
ports:
|
||||
- 10086:10086/tcp
|
||||
- 51820:51820/udp
|
||||
volumes:
|
||||
- conf:/etc/wireguard
|
||||
- app:/opt/wireguarddashboard/app
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
volumes:
|
||||
conf:
|
||||
app:
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
echo "Starting the WireGuard Dashboard Docker container."
|
||||
|
||||
clean_up() {
|
||||
# Cleaning out previous data such as the .pid file and starting the WireGuard Dashboard. Making sure to use the python venv.
|
||||
echo "Looking for remains of previous instances..."
|
||||
if [ -f "/opt/wireguarddashboard/app/src/gunicorn.pid" ]; then
|
||||
echo "Found old .pid file, removing."
|
||||
rm /opt/wireguarddashboard/app/src/gunicorn.pid
|
||||
else
|
||||
echo "No remains found, continuing."
|
||||
fi
|
||||
}
|
||||
|
||||
start_core() {
|
||||
# This first step is to ensure the wg0.conf file exists, and if not, then its copied over from the ephemeral container storage.
|
||||
if [ ! -f "/etc/wireguard/wg0.conf" ]; then
|
||||
cp "/wg0.conf" "/etc/wireguard/wg0.conf"
|
||||
echo "WireGuard interface file copied over."
|
||||
else
|
||||
echo "WireGuard interface file looks to already be existing."
|
||||
fi
|
||||
|
||||
echo "Activating Python venv and executing the WireGuard Dashboard service."
|
||||
|
||||
. "${WGDASH}"/venv/bin/activate
|
||||
cd "${WGDASH}"/app/src || return # If changing the directory fails (permission or presence error), then bash will exist this function, causing the WireGuard Dashboard to not be succesfully launched.
|
||||
bash wgd.sh start
|
||||
|
||||
# The following section takes care of the firewall rules regarding the 'isolated_peers' feature, which allows or drops packets destined from the wg0 to the wg0 interface.
|
||||
if [ "${isolated_peers,,}" = "false" ]; then
|
||||
echo "Isolated peers disabled, adjusting."
|
||||
|
||||
sed -i '/PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP/d' /etc/wireguard/wg0.conf
|
||||
sed -i '/PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP/d' /etc/wireguard/wg0.conf
|
||||
elif [ "${isolated_peers,,}" = "true" ]; then
|
||||
upblocking=$(grep -c "PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP" /etc/wireguard/wg0.conf)
|
||||
downblocking=$(grep -c "PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP" /etc/wireguard/wg0.conf)
|
||||
if [ "$upblocking" -lt 1 ] && [ "$downblocking" -lt 1 ]; then
|
||||
echo "Isolated peers enabled, adjusting."
|
||||
|
||||
sed -i '/PostUp = iptables -t nat -I POSTROUTING 1 -s/a PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP' /etc/wireguard/wg0.conf
|
||||
sed -i '/PreDown = iptables -t nat -D POSTROUTING 1 -s/a PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP' /etc/wireguard/wg0.conf
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# The following section takes care of
|
||||
if [ "${enable_wg0,,}" = "true" ]; then
|
||||
echo "Preference for wg0 to be turned on found."
|
||||
|
||||
wg-quick up wg0
|
||||
else
|
||||
echo "Preference for wg0 to be turned off found."
|
||||
fi
|
||||
}
|
||||
|
||||
set_envvars() {
|
||||
echo "Setting relevant variables for operation."
|
||||
|
||||
# If the timezone is different, for example in North-America or Asia.
|
||||
if [ "${tz}" != "$(cat /etc/timezone)" ]; then
|
||||
echo "Changing timezone."
|
||||
|
||||
ln -sf /usr/share/zoneinfo/"${tz}" /etc/localtime
|
||||
echo "${tz}" > /etc/timezone
|
||||
fi
|
||||
|
||||
# Changing the DNS used for clients and the dashboard itself.
|
||||
if [ "${global_dns}" != "$(grep "peer_global_dns = " /opt/wireguarddashboard/app/src/wg-dashboard.ini | awk '{print $NF}')" ]; then
|
||||
echo "Changing default dns."
|
||||
|
||||
#sed -i "s/^DNS = .*/DNS = ${global_dns}/" /etc/wireguard/wg0.conf # Uncomment if you want to have DNS on server-level.
|
||||
sed -i "s/^peer_global_dns = .*/peer_global_dns = ${global_dns}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
|
||||
fi
|
||||
|
||||
# Setting the public IP of the WireGuard Dashboard container host. If not defined, it will trying fetching it using a curl to ifconfig.me.
|
||||
if [ "${public_ip}" = "0.0.0.0" ]; then
|
||||
default_ip=$(curl -s ifconfig.me)
|
||||
echo "Trying to fetch the Public-IP using ifconfig.me: ${default_ip}"
|
||||
|
||||
sed -i "s/^remote_endpoint = .*/remote_endpoint = ${default_ip}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
|
||||
elif [ "${public_ip}" != "$(grep "remote_endpoint = " /opt/wireguarddashboard/app/src/wg-dashboard.ini | awk '{print $NF}')" ]; then
|
||||
echo "Setting the Public-IP using given variable: ${public_ip}"
|
||||
|
||||
sed -i "s/^remote_endpoint = .*/remote_endpoint = ${public_ip}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_blocking() {
|
||||
sleep 1s
|
||||
echo "Ensuring container continuation."
|
||||
|
||||
# This function checks if the latest error log is created and tails it for docker logs uses.
|
||||
if find "/opt/wireguarddashboard/app/src/log" -mindepth 1 -maxdepth 1 -type f | read -r; then
|
||||
latestErrLog=$(find /opt/wireguarddashboard/app/src/log -name "error_*.log" | head -n 1)
|
||||
latestAccLog=$(find /opt/wireguarddashboard/app/src/log -name "access_*.log" | head -n 1)
|
||||
tail -f "${latestErrLog}" "${latestAccLog}"
|
||||
fi
|
||||
|
||||
# Blocking command in case of erroring. So the container does not quit.
|
||||
sleep infinity
|
||||
}
|
||||
|
||||
# Execute functions for the WireGuard Dashboard services, then set the environment variables
|
||||
clean_up
|
||||
start_core
|
||||
set_envvars
|
||||
ensure_blocking
|
||||
@@ -0,0 +1,8 @@
|
||||
# API Documents for WGDashboard
|
||||
|
||||
**Version: v4.0**
|
||||
|
||||
**Created by: Donald Zou**
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# if [ -z "$(ls -A /etc/wireguard)" ]; then
|
||||
# mv /wg0.conf /etc/wireguard
|
||||
# echo "Moved conf file to /etc/wireguard"
|
||||
# else
|
||||
# rm wg0.conf
|
||||
# echo "Removed unneeded conf file"
|
||||
# fi
|
||||
|
||||
# wg-quick up wg0
|
||||
chmod u+x /opt/wgdashboard/wgd.sh
|
||||
if [ ! -f "/opt/wgdashboard/wg-dashboard.ini" ]; then
|
||||
/opt/wgdashboard/wgd.sh install
|
||||
fi
|
||||
/opt/wgdashboard/wgd.sh debug
|
||||
+378
-126
@@ -47,7 +47,11 @@ UPDATE = None
|
||||
app = Flask("WGDashboard")
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 5206928
|
||||
app.secret_key = secrets.token_urlsafe(32)
|
||||
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
cors = CORS(app, resources={r"/api/*": {
|
||||
"origins": "*",
|
||||
"methods": "DELETE, POST, GET, OPTIONS",
|
||||
"allow_headers": ["Content-Type", "wg-dashboard-apikey"]
|
||||
}})
|
||||
|
||||
class ModelEncoder(JSONEncoder):
|
||||
def default(self, o: Any) -> Any:
|
||||
@@ -77,7 +81,12 @@ class CustomJsonEncoder(DefaultJSONProvider):
|
||||
super().__init__(app)
|
||||
|
||||
def default(self, o):
|
||||
if isinstance(o, WireguardConfiguration) or isinstance(o, Peer) or isinstance(o, PeerJob) or isinstance(o, Log) or isinstance(o, DashboardAPIKey):
|
||||
if (isinstance(o, WireguardConfiguration)
|
||||
or isinstance(o, Peer)
|
||||
or isinstance(o, PeerJob)
|
||||
or isinstance(o, Log)
|
||||
or isinstance(o, DashboardAPIKey)
|
||||
or isinstance(o, PeerShareLink)):
|
||||
return o.toJson()
|
||||
return super().default(self, o)
|
||||
|
||||
@@ -104,7 +113,36 @@ class Log:
|
||||
def __dict__(self):
|
||||
return self.toJson()
|
||||
|
||||
class Logger:
|
||||
class DashboardLogger:
|
||||
def __init__(self):
|
||||
self.loggerdb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard_log.db'),
|
||||
check_same_thread=False)
|
||||
self.loggerdb.row_factory = sqlite3.Row
|
||||
self.loggerdbCursor = self.loggerdb.cursor()
|
||||
self.__createLogDatabase()
|
||||
self.log(Message="WGDashboard started")
|
||||
def __createLogDatabase(self):
|
||||
existingTable = self.loggerdbCursor.execute("SELECT name from sqlite_master where type='table'").fetchall()
|
||||
existingTable = [t['name'] for t in existingTable]
|
||||
|
||||
if "DashboardLog" not in existingTable:
|
||||
self.loggerdbCursor.execute(
|
||||
"CREATE TABLE DashboardLog (LogID VARCHAR NOT NULL, LogDate DATETIME DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now', 'localtime')), URL VARCHAR, IP VARCHAR, Status VARCHAR, Message VARCHAR, PRIMARY KEY (LogID))")
|
||||
self.loggerdb.commit()
|
||||
|
||||
def log(self, URL: str = "", IP: str = "", Status: str = "true", Message: str = "") -> bool:
|
||||
try:
|
||||
self.loggerdbCursor.execute(
|
||||
"INSERT INTO DashboardLog (LogID, URL, IP, Status, Message) VALUES (?, ?, ?, ?, ?)", (str(uuid.uuid4()), URL, IP, Status, Message,))
|
||||
self.loggerdb.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class PeerJobLogger:
|
||||
def __init__(self):
|
||||
self.loggerdb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard_log.db'),
|
||||
check_same_thread=False)
|
||||
@@ -266,7 +304,6 @@ class PeerJobs:
|
||||
def runJob(self):
|
||||
needToDelete = []
|
||||
for job in self.Jobs:
|
||||
print(job.toJson())
|
||||
c = WireguardConfigurations.get(job.Configuration)
|
||||
if c is not None:
|
||||
f, fp = c.searchPeer(job.Peer)
|
||||
@@ -277,11 +314,8 @@ class PeerJobs:
|
||||
y: float = float(job.Value)
|
||||
else:
|
||||
x: datetime = datetime.now()
|
||||
y: datetime = datetime.strptime(job.Value, "%Y-%m-%dT%H:%M")
|
||||
|
||||
|
||||
y: datetime = datetime.strptime(job.Value, "%Y-%m-%d %H:%M:%S")
|
||||
runAction: bool = self.__runJob_Compare(x, y, job.Operator)
|
||||
print("Running Job:" + str(runAction) + "\n")
|
||||
if runAction:
|
||||
s = False
|
||||
if job.Action == "restrict":
|
||||
@@ -298,7 +332,6 @@ class PeerJobs:
|
||||
JobLogger.log(job.JobID, s["status"],
|
||||
f"Peer {fp.id} from {c.Name} failed {job.Action}ed."
|
||||
)
|
||||
print(f'''[{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Peer Job Schedule: Ran {len(needToDelete)} job(s)''')
|
||||
for j in needToDelete:
|
||||
self.deleteJob(j)
|
||||
|
||||
@@ -312,6 +345,74 @@ class PeerJobs:
|
||||
if operator == "lst":
|
||||
return x < y
|
||||
|
||||
class PeerShareLink:
|
||||
def __init__(self, ShareID:str, Configuration: str, Peer: str, ExpireDate: datetime, ShareDate: datetime):
|
||||
self.ShareID = ShareID
|
||||
self.Peer = Peer
|
||||
self.Configuration = Configuration
|
||||
self.ShareDate = ShareDate
|
||||
self.ExpireDate = ExpireDate
|
||||
|
||||
|
||||
def toJson(self):
|
||||
return {
|
||||
"ShareID": self.ShareID,
|
||||
"Peer": self.Peer,
|
||||
"Configuration": self.Configuration,
|
||||
"ExpireDate": self.ExpireDate
|
||||
}
|
||||
|
||||
class PeerShareLinks:
|
||||
def __init__(self):
|
||||
self.Links: list[PeerShareLink] = []
|
||||
self.PeerShareLinkCursor = sqldb.cursor()
|
||||
existingTables = self.PeerShareLinkCursor.execute("SELECT name FROM sqlite_master WHERE type='table' and name = 'PeerShareLinks'").fetchall()
|
||||
if len(existingTables) == 0:
|
||||
self.PeerShareLinkCursor.execute(
|
||||
"""
|
||||
CREATE TABLE PeerShareLinks (
|
||||
ShareID VARCHAR NOT NULL PRIMARY KEY, Configuration VARCHAR NOT NULL, Peer VARCHAR NOT NULL,
|
||||
ExpireDate DATETIME,
|
||||
SharedDate DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
||||
"""
|
||||
)
|
||||
sqldb.commit()
|
||||
self.__getSharedLinks()
|
||||
# print(self.Links)
|
||||
def __getSharedLinks(self):
|
||||
self.Links.clear()
|
||||
allLinks = self.PeerShareLinkCursor.execute("SELECT * FROM PeerShareLinks WHERE ExpireDate IS NULL OR ExpireDate > datetime('now', 'localtime')").fetchall()
|
||||
for link in allLinks:
|
||||
self.Links.append(PeerShareLink(*link))
|
||||
|
||||
def getLink(self, Configuration: str, Peer: str) -> list[PeerShareLink]:
|
||||
self.__getSharedLinks()
|
||||
return list(filter(lambda x : x.Configuration == Configuration and x.Peer == Peer, self.Links))
|
||||
|
||||
def getLinkByID(self, ShareID: str) -> list[PeerShareLink]:
|
||||
self.__getSharedLinks()
|
||||
return list(filter(lambda x : x.ShareID == ShareID, self.Links))
|
||||
|
||||
def addLink(self, Configuration: str, Peer: str, ExpireDate: datetime = None) -> tuple[bool, str]:
|
||||
try:
|
||||
newShareID = str(uuid.uuid4())
|
||||
if len(self.getLink(Configuration, Peer)) > 0:
|
||||
self.PeerShareLinkCursor.execute("UPDATE PeerShareLinks SET ExpireDate = datetime('now', 'localtime') WHERE Configuration = ? AND Peer = ?", (Configuration, Peer, ))
|
||||
self.PeerShareLinkCursor.execute("INSERT INTO PeerShareLinks (ShareID, Configuration, Peer, ExpireDate) VALUES (?, ?, ?, ?)", (newShareID, Configuration, Peer, ExpireDate, ))
|
||||
sqldb.commit()
|
||||
self.__getSharedLinks()
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
return True, newShareID
|
||||
|
||||
def updateLinkExpireDate(self, ShareID, ExpireDate: datetime = None) -> tuple[bool, str]:
|
||||
|
||||
self.PeerShareLinkCursor.execute("UPDATE PeerShareLinks SET ExpireDate = ? WHERE ShareID = ?;", (ExpireDate, ShareID, ))
|
||||
sqldb.commit()
|
||||
self.__getSharedLinks()
|
||||
return True, ""
|
||||
|
||||
class WireguardConfiguration:
|
||||
class InvalidConfigurationFileException(Exception):
|
||||
def __init__(self, m):
|
||||
@@ -391,12 +492,12 @@ class WireguardConfiguration:
|
||||
self.getPeersList()
|
||||
|
||||
def __createDatabase(self):
|
||||
existingTables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
|
||||
existingTables = sqldb.cursor().execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
|
||||
existingTables = [t['name'] for t in existingTables]
|
||||
if self.Name not in existingTables:
|
||||
cursor.execute(
|
||||
sqldb.cursor().execute(
|
||||
"""
|
||||
CREATE TABLE %s (
|
||||
CREATE TABLE '%s'(
|
||||
id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
|
||||
endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
|
||||
total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
|
||||
@@ -410,9 +511,9 @@ class WireguardConfiguration:
|
||||
sqldb.commit()
|
||||
|
||||
if f'{self.Name}_restrict_access' not in existingTables:
|
||||
cursor.execute(
|
||||
sqldb.cursor().execute(
|
||||
"""
|
||||
CREATE TABLE %s_restrict_access (
|
||||
CREATE TABLE '%s_restrict_access' (
|
||||
id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
|
||||
endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
|
||||
total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
|
||||
@@ -425,9 +526,9 @@ class WireguardConfiguration:
|
||||
)
|
||||
sqldb.commit()
|
||||
if f'{self.Name}_transfer' not in existingTables:
|
||||
cursor.execute(
|
||||
sqldb.cursor().execute(
|
||||
"""
|
||||
CREATE TABLE %s_transfer (
|
||||
CREATE TABLE '%s_transfer' (
|
||||
id VARCHAR NOT NULL, total_receive FLOAT NULL,
|
||||
total_sent FLOAT NULL, total_data FLOAT NULL,
|
||||
cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, time DATETIME
|
||||
@@ -436,9 +537,9 @@ class WireguardConfiguration:
|
||||
)
|
||||
sqldb.commit()
|
||||
if f'{self.Name}_deleted' not in existingTables:
|
||||
cursor.execute(
|
||||
sqldb.cursor().execute(
|
||||
"""
|
||||
CREATE TABLE %s_deleted (
|
||||
CREATE TABLE '%s_deleted' (
|
||||
id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
|
||||
endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
|
||||
total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
|
||||
@@ -451,6 +552,8 @@ class WireguardConfiguration:
|
||||
)
|
||||
sqldb.commit()
|
||||
|
||||
|
||||
|
||||
def __getPublicKey(self) -> str:
|
||||
return _generatePublicKey(self.PrivateKey)[1]
|
||||
|
||||
@@ -460,7 +563,7 @@ class WireguardConfiguration:
|
||||
|
||||
def __getRestrictedPeers(self):
|
||||
self.RestrictedPeers = []
|
||||
restricted = cursor.execute("SELECT * FROM %s_restrict_access" % self.Name).fetchall()
|
||||
restricted = sqldb.cursor().execute("SELECT * FROM '%s_restrict_access'" % self.Name).fetchall()
|
||||
for i in restricted:
|
||||
self.RestrictedPeers.append(Peer(i, self))
|
||||
|
||||
@@ -485,7 +588,7 @@ class WireguardConfiguration:
|
||||
p[pCounter][split[0]] = split[1]
|
||||
for i in p:
|
||||
if "PublicKey" in i.keys():
|
||||
checkIfExist = cursor.execute("SELECT * FROM %s WHERE id = ?" % self.Name,
|
||||
checkIfExist = sqldb.cursor().execute("SELECT * FROM '%s' WHERE id = ?" % self.Name,
|
||||
((i['PublicKey']),)).fetchone()
|
||||
if checkIfExist is None:
|
||||
newPeer = {
|
||||
@@ -511,9 +614,9 @@ class WireguardConfiguration:
|
||||
"remote_endpoint": DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
|
||||
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else ""
|
||||
}
|
||||
cursor.execute(
|
||||
sqldb.cursor().execute(
|
||||
"""
|
||||
INSERT INTO %s
|
||||
INSERT INTO '%s'
|
||||
VALUES (:id, :private_key, :DNS, :endpoint_allowed_ip, :name, :total_receive, :total_sent,
|
||||
:total_data, :endpoint, :status, :latest_handshake, :allowed_ip, :cumu_receive, :cumu_sent,
|
||||
:cumu_data, :mtu, :keepalive, :remote_endpoint, :preshared_key);
|
||||
@@ -522,13 +625,21 @@ class WireguardConfiguration:
|
||||
sqldb.commit()
|
||||
self.Peers.append(Peer(newPeer, self))
|
||||
else:
|
||||
cursor.execute("UPDATE %s SET allowed_ip = ? WHERE id = ?" % self.Name,
|
||||
sqldb.cursor().execute("UPDATE '%s' SET allowed_ip = ? WHERE id = ?" % self.Name,
|
||||
(i.get("AllowedIPs", "N/A"), i['PublicKey'],))
|
||||
sqldb.commit()
|
||||
self.Peers.append(Peer(checkIfExist, self))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def addPeers(self, peers: list):
|
||||
for p in peers:
|
||||
subprocess.check_output(f"wg set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip']}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
subprocess.check_output(
|
||||
f"wg-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
|
||||
self.getPeersList()
|
||||
|
||||
def searchPeer(self, publicKey):
|
||||
for i in self.Peers:
|
||||
if i.id == publicKey:
|
||||
@@ -538,12 +649,15 @@ class WireguardConfiguration:
|
||||
def allowAccessPeers(self, listOfPublicKeys):
|
||||
# numOfAllowedPeers = 0
|
||||
# numOfFailedToAllowPeers = 0
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
|
||||
for i in listOfPublicKeys:
|
||||
p = cursor.execute("SELECT * FROM %s_restrict_access WHERE id = ?" % self.Name, (i,)).fetchone()
|
||||
p = sqldb.cursor().execute("SELECT * FROM '%s_restrict_access' WHERE id = ?" % self.Name, (i,)).fetchone()
|
||||
if p is not None:
|
||||
cursor.execute("INSERT INTO %s SELECT * FROM %s_restrict_access WHERE id = ?"
|
||||
sqldb.cursor().execute("INSERT INTO '%s' SELECT * FROM %s_restrict_access WHERE id = ?"
|
||||
% (self.Name, self.Name,), (p['id'],))
|
||||
cursor.execute("DELETE FROM %s_restrict_access WHERE id = ?"
|
||||
sqldb.cursor().execute("DELETE FROM '%s_restrict_access' WHERE id = ?"
|
||||
% self.Name, (p['id'],))
|
||||
subprocess.check_output(f"wg set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip']}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
@@ -558,17 +672,19 @@ class WireguardConfiguration:
|
||||
def restrictPeers(self, listOfPublicKeys):
|
||||
numOfRestrictedPeers = 0
|
||||
numOfFailedToRestrictPeers = 0
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
for p in listOfPublicKeys:
|
||||
found, pf = self.searchPeer(p)
|
||||
if found:
|
||||
try:
|
||||
subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
cursor.execute("INSERT INTO %s_restrict_access SELECT * FROM %s WHERE id = ?" %
|
||||
sqldb.cursor().execute("INSERT INTO '%s_restrict_access' SELECT * FROM %s WHERE id = ?" %
|
||||
(self.Name, self.Name,), (pf.id,))
|
||||
cursor.execute("UPDATE %s_restrict_access SET status = 'stopped' WHERE id = ?" %
|
||||
sqldb.cursor().execute("UPDATE '%s_restrict_access' SET status = 'stopped' WHERE id = ?" %
|
||||
(self.Name,), (pf.id,))
|
||||
cursor.execute("DELETE FROM %s WHERE id = ?" % self.Name, (pf.id,))
|
||||
sqldb.cursor().execute("DELETE FROM '%s' WHERE id = ?" % self.Name, (pf.id,))
|
||||
numOfRestrictedPeers += 1
|
||||
except Exception as e:
|
||||
numOfFailedToRestrictPeers += 1
|
||||
@@ -587,13 +703,15 @@ class WireguardConfiguration:
|
||||
def deletePeers(self, listOfPublicKeys):
|
||||
numOfDeletedPeers = 0
|
||||
numOfFailedToDeletePeers = 0
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
for p in listOfPublicKeys:
|
||||
found, pf = self.searchPeer(p)
|
||||
if found:
|
||||
try:
|
||||
subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
cursor.execute("DELETE FROM %s WHERE id = ?" % self.Name, (pf.id,))
|
||||
sqldb.cursor().execute("DELETE FROM '%s' WHERE id = ?" % self.Name, (pf.id,))
|
||||
numOfDeletedPeers += 1
|
||||
except Exception as e:
|
||||
numOfFailedToDeletePeers += 1
|
||||
@@ -613,7 +731,7 @@ class WireguardConfiguration:
|
||||
d = i.toJson()
|
||||
sqldb.execute(
|
||||
'''
|
||||
UPDATE %s SET private_key = :private_key,
|
||||
UPDATE '%s' SET private_key = :private_key,
|
||||
DNS = :DNS, endpoint_allowed_ip = :endpoint_allowed_ip, name = :name,
|
||||
total_receive = :total_receive, total_sent = :total_sent, total_data = :total_data,
|
||||
endpoint = :endpoint, status = :status, latest_handshake = :latest_handshake,
|
||||
@@ -632,6 +750,8 @@ class WireguardConfiguration:
|
||||
return False, str(e)
|
||||
|
||||
def getPeersLatestHandshake(self):
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
try:
|
||||
latestHandshake = subprocess.check_output(f"wg show {self.Name} latest-handshakes",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
@@ -648,15 +768,17 @@ class WireguardConfiguration:
|
||||
else:
|
||||
status = "stopped"
|
||||
if int(latestHandshake[count + 1]) > 0:
|
||||
sqldb.execute("UPDATE %s SET latest_handshake = ?, status = ? WHERE id= ?" % self.Name
|
||||
sqldb.execute("UPDATE '%s' SET latest_handshake = ?, status = ? WHERE id= ?" % self.Name
|
||||
, (str(minus).split(".", maxsplit=1)[0], status, latestHandshake[count],))
|
||||
else:
|
||||
sqldb.execute("UPDATE %s SET latest_handshake = 'No Handshake', status = ? WHERE id= ?" % self.Name
|
||||
sqldb.execute("UPDATE '%s' SET latest_handshake = 'No Handshake', status = ? WHERE id= ?" % self.Name
|
||||
, (status, latestHandshake[count],))
|
||||
sqldb.commit()
|
||||
count += 2
|
||||
|
||||
def getPeersTransfer(self):
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
try:
|
||||
data_usage = subprocess.check_output(f"wg show {self.Name} transfer",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
@@ -664,50 +786,40 @@ class WireguardConfiguration:
|
||||
data_usage = [p.split("\t") for p in data_usage]
|
||||
for i in range(len(data_usage)):
|
||||
if len(data_usage[i]) == 3:
|
||||
cur_i = cursor.execute(
|
||||
"SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM %s WHERE id= ? "
|
||||
cur_i = sqldb.cursor().execute(
|
||||
"SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM '%s' WHERE id= ? "
|
||||
% self.Name, (data_usage[i][0],)).fetchone()
|
||||
if cur_i is not None:
|
||||
total_sent = cur_i['total_sent']
|
||||
total_receive = cur_i['total_receive']
|
||||
cur_total_sent = round(int(data_usage[i][2]) / (1024 ** 3), 4)
|
||||
cur_total_receive = round(int(data_usage[i][1]) / (1024 ** 3), 4)
|
||||
cur_total_sent = float(data_usage[i][2]) / (1024 ** 3)
|
||||
cur_total_receive = float(data_usage[i][1]) / (1024 ** 3)
|
||||
cumulative_receive = cur_i['cumu_receive'] + total_receive
|
||||
cumulative_sent = cur_i['cumu_sent'] + total_sent
|
||||
if total_sent <= cur_total_sent and total_receive <= cur_total_receive:
|
||||
total_sent = cur_total_sent
|
||||
total_receive = cur_total_receive
|
||||
else:
|
||||
cursor.execute(
|
||||
"UPDATE %s SET cumu_receive = ?, cumu_sent = ?, cumu_data = ? WHERE id = ?" %
|
||||
self.Name, (round(cumulative_receive, 4), round(cumulative_sent, 4),
|
||||
round(cumulative_sent + cumulative_receive, 4),
|
||||
sqldb.cursor().execute(
|
||||
"UPDATE '%s' SET cumu_receive = ?, cumu_sent = ?, cumu_data = ? WHERE id = ?" %
|
||||
self.Name, (cumulative_receive, cumulative_sent,
|
||||
cumulative_sent + cumulative_receive,
|
||||
data_usage[i][0],))
|
||||
total_sent = 0
|
||||
total_receive = 0
|
||||
|
||||
_, p = self.searchPeer(data_usage[i][0])
|
||||
if p.total_receive != round(total_receive, 4) or p.total_sent != round(total_sent, 4):
|
||||
cursor.execute(
|
||||
"UPDATE %s SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?"
|
||||
% self.Name, (round(total_receive, 4), round(total_sent, 4),
|
||||
round(total_receive + total_sent, 4), data_usage[i][0],))
|
||||
now = datetime.now()
|
||||
now_string = now.strftime("%d/%m/%Y %H:%M:%S")
|
||||
# cursor.execute(f'''
|
||||
# INSERT INTO %s_transfer
|
||||
# (id, total_receive, total_sent, total_data,
|
||||
# cumu_receive, cumu_sent, cumu_data, time)
|
||||
# VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
# ''' % self.Name, (data_usage[i][0], round(total_receive, 4), round(total_sent, 4),
|
||||
# round(total_receive + total_sent, 4), round(cumulative_receive, 4),
|
||||
# round(cumulative_sent, 4),
|
||||
# round(cumulative_sent + cumulative_receive, 4), now_string,))
|
||||
# sqldb.commit()
|
||||
if p.total_receive != total_receive or p.total_sent != total_sent:
|
||||
sqldb.cursor().execute(
|
||||
"UPDATE '%s' SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?"
|
||||
% self.Name, (total_receive, total_sent,
|
||||
total_receive + total_sent, data_usage[i][0],))
|
||||
except Exception as e:
|
||||
print("Error" + str(e))
|
||||
print("Error: " + str(e))
|
||||
|
||||
def getPeersEndpoint(self):
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
try:
|
||||
data_usage = subprocess.check_output(f"wg show {self.Name} endpoints",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
@@ -716,14 +828,13 @@ class WireguardConfiguration:
|
||||
data_usage = data_usage.decode("UTF-8").split()
|
||||
count = 0
|
||||
for _ in range(int(len(data_usage) / 2)):
|
||||
sqldb.execute("UPDATE %s SET endpoint = ? WHERE id = ?" % self.Name
|
||||
sqldb.execute("UPDATE '%s' SET endpoint = ? WHERE id = ?" % self.Name
|
||||
, (data_usage[count + 1], data_usage[count],))
|
||||
sqldb.commit()
|
||||
count += 2
|
||||
|
||||
def toggleConfiguration(self) -> [bool, str]:
|
||||
self.getStatus()
|
||||
print("Status: ", self.getStatus())
|
||||
if self.Status:
|
||||
try:
|
||||
check = subprocess.check_output(f"wg-quick down {self.Name}",
|
||||
@@ -760,7 +871,13 @@ class WireguardConfiguration:
|
||||
"PreDown": self.PreDown,
|
||||
"PostUp": self.PostUp,
|
||||
"PostDown": self.PostDown,
|
||||
"SaveConfig": self.SaveConfig
|
||||
"SaveConfig": self.SaveConfig,
|
||||
"DataUsage": {
|
||||
"Total": sum(list(map(lambda x: x.cumu_data + x.total_data, self.Peers))),
|
||||
"Sent": sum(list(map(lambda x: x.cumu_sent + x.total_sent, self.Peers))),
|
||||
"Receive": sum(list(map(lambda x: x.cumu_receive + x.total_receive, self.Peers)))
|
||||
},
|
||||
"ConnectedPeers": len(list(filter(lambda x: x.status == "running", self.Peers)))
|
||||
}
|
||||
|
||||
class Peer:
|
||||
@@ -786,10 +903,13 @@ class Peer:
|
||||
self.remote_endpoint = tableData["remote_endpoint"]
|
||||
self.preshared_key = tableData["preshared_key"]
|
||||
self.jobs: list[PeerJob] = []
|
||||
self.ShareLink: list[PeerShareLink] = []
|
||||
self.getJobs()
|
||||
self.getShareLink()
|
||||
|
||||
def toJson(self):
|
||||
self.getJobs()
|
||||
self.getShareLink()
|
||||
return self.__dict__
|
||||
|
||||
def __repr__(self):
|
||||
@@ -799,6 +919,8 @@ class Peer:
|
||||
preshared_key: str,
|
||||
dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int,
|
||||
keepalive: int) -> ResponseObject:
|
||||
if not self.configuration.getStatus():
|
||||
self.configuration.toggleConfiguration()
|
||||
|
||||
existingAllowedIps = [item for row in list(
|
||||
map(lambda x: [q.strip() for q in x.split(',')],
|
||||
@@ -843,12 +965,13 @@ class Peer:
|
||||
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
|
||||
return ResponseObject(False,
|
||||
"Update peer failed when saving the configuration.")
|
||||
cursor.execute(
|
||||
'''UPDATE %s SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?,
|
||||
sqldb.cursor().execute(
|
||||
'''UPDATE '%s' SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?,
|
||||
keepalive = ?, preshared_key = ? WHERE id = ?''' % self.configuration.Name,
|
||||
(name, private_key, dns_addresses, endpoint_allowed_ip, mtu,
|
||||
keepalive, preshared_key, self.id,)
|
||||
)
|
||||
sqldb.commit()
|
||||
return ResponseObject()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
return ResponseObject(False, exc.output.decode("UTF-8").strip())
|
||||
@@ -888,7 +1011,24 @@ PersistentKeepalive = {str(self.keepalive)}
|
||||
|
||||
def getJobs(self):
|
||||
self.jobs = AllPeerJobs.searchJob(self.configuration.Name, self.id)
|
||||
# print(AllPeerJobs.searchJob(self.configuration.Name, self.id))
|
||||
|
||||
def getShareLink(self):
|
||||
self.ShareLink = AllPeerShareLinks.getLink(self.configuration.Name, self.id)
|
||||
|
||||
def resetDataUsage(self, type):
|
||||
try:
|
||||
if type == "total":
|
||||
sqldb.cursor().execute("UPDATE '%s' SET total_data = 0, cumu_data = 0, total_receive = 0, cumu_receive = 0, total_sent = 0, cumu_sent = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
|
||||
elif type == "receive":
|
||||
sqldb.cursor().execute("UPDATE '%s' SET total_receive = 0, cumu_receive = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
|
||||
elif type == "sent":
|
||||
sqldb.cursor().execute("UPDATE '%s' SET total_sent = 0, cumu_sent = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# Regex Match
|
||||
def regex_match(regex, text):
|
||||
@@ -922,6 +1062,7 @@ class DashboardConfig:
|
||||
"username": "admin",
|
||||
"password": "admin",
|
||||
"enable_totp": "false",
|
||||
"totp_verified": "false",
|
||||
"totp_key": pyotp.random_base32()
|
||||
},
|
||||
"Server": {
|
||||
@@ -955,15 +1096,16 @@ class DashboardConfig:
|
||||
self.SetConfig(section, key, value, True)
|
||||
self.__createAPIKeyTable()
|
||||
self.DashboardAPIKeys = self.__getAPIKeys()
|
||||
self.APIAccessed = False
|
||||
|
||||
def __createAPIKeyTable(self):
|
||||
existingTable = cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name = 'DashboardAPIKeys'").fetchall()
|
||||
existingTable = sqldb.cursor().execute("SELECT name FROM sqlite_master WHERE type='table' AND name = 'DashboardAPIKeys'").fetchall()
|
||||
if len(existingTable) == 0:
|
||||
cursor.execute("CREATE TABLE DashboardAPIKeys (Key VARCHAR NOT NULL PRIMARY KEY, CreatedAt DATETIME NOT NULL DEFAULT (datetime('now', 'localtime')), ExpiredAt VARCHAR)")
|
||||
sqldb.cursor().execute("CREATE TABLE DashboardAPIKeys (Key VARCHAR NOT NULL PRIMARY KEY, CreatedAt DATETIME NOT NULL DEFAULT (datetime('now', 'localtime')), ExpiredAt VARCHAR)")
|
||||
sqldb.commit()
|
||||
|
||||
def __getAPIKeys(self) -> list[DashboardAPIKey]:
|
||||
keys = cursor.execute("SELECT * FROM DashboardAPIKeys WHERE ExpiredAt IS NULL OR ExpiredAt > datetime('now', 'localtime') ORDER BY CreatedAt DESC").fetchall()
|
||||
keys = sqldb.cursor().execute("SELECT * FROM DashboardAPIKeys WHERE ExpiredAt IS NULL OR ExpiredAt > datetime('now', 'localtime') ORDER BY CreatedAt DESC").fetchall()
|
||||
fKeys = []
|
||||
for k in keys:
|
||||
fKeys.append(DashboardAPIKey(*k))
|
||||
@@ -971,12 +1113,12 @@ class DashboardConfig:
|
||||
|
||||
def createAPIKeys(self, ExpiredAt = None):
|
||||
newKey = secrets.token_urlsafe(32)
|
||||
cursor.execute('INSERT INTO DashboardAPIKeys (Key, ExpiredAt) VALUES (?, ?)', (newKey, ExpiredAt,))
|
||||
sqldb.cursor().execute('INSERT INTO DashboardAPIKeys (Key, ExpiredAt) VALUES (?, ?)', (newKey, ExpiredAt,))
|
||||
sqldb.commit()
|
||||
self.DashboardAPIKeys = self.__getAPIKeys()
|
||||
|
||||
def deleteAPIKey(self, key):
|
||||
cursor.execute("UPDATE DashboardAPIKeys SET ExpiredAt = datetime('now', 'localtime') WHERE Key = ?", (key, ))
|
||||
sqldb.cursor().execute("UPDATE DashboardAPIKeys SET ExpiredAt = datetime('now', 'localtime') WHERE Key = ?", (key, ))
|
||||
sqldb.commit()
|
||||
self.DashboardAPIKeys = self.__getAPIKeys()
|
||||
|
||||
@@ -1202,12 +1344,24 @@ def _getWireguardConfigurationAvailableIP(configName: str) -> tuple[bool, list[s
|
||||
return False, None
|
||||
|
||||
|
||||
|
||||
'''
|
||||
API Routes
|
||||
'''
|
||||
|
||||
@app.before_request
|
||||
def auth_req():
|
||||
if request.method.lower() == 'options':
|
||||
return ResponseObject(True)
|
||||
|
||||
DashboardConfig.APIAccessed = False
|
||||
if "api" in request.path:
|
||||
if str(request.method) == "GET":
|
||||
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=str(request.args))
|
||||
elif str(request.method) == "POST":
|
||||
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Request Args: {str(request.args)} Body:{str(request.get_json())}")
|
||||
|
||||
|
||||
authenticationRequired = DashboardConfig.GetConfig("Server", "auth_req")[1]
|
||||
d = request.headers
|
||||
if authenticationRequired:
|
||||
@@ -1215,7 +1369,9 @@ def auth_req():
|
||||
apiKeyEnabled = DashboardConfig.GetConfig("Server", "dashboard_api_key")[1]
|
||||
if apiKey is not None and len(apiKey) > 0 and apiKeyEnabled:
|
||||
apiKeyExist = len(list(filter(lambda x : x.Key == apiKey, DashboardConfig.DashboardAPIKeys))) == 1
|
||||
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"API Key Access: {('true' if apiKeyExist else 'false')} - Key: {apiKey}")
|
||||
if not apiKeyExist:
|
||||
DashboardConfig.APIAccessed = False
|
||||
response = Flask.make_response(app, {
|
||||
"status": False,
|
||||
"message": "API Key does not exist",
|
||||
@@ -1224,10 +1380,13 @@ def auth_req():
|
||||
response.content_type = "application/json"
|
||||
response.status_code = 401
|
||||
return response
|
||||
DashboardConfig.APIAccessed = True
|
||||
else:
|
||||
DashboardConfig.APIAccessed = False
|
||||
if ('/static/' not in request.path and "username" not in session and "/" != request.path
|
||||
and "validateAuthentication" not in request.path and "authenticate" not in request.path
|
||||
and "getDashboardConfiguration" not in request.path and "getDashboardTheme" not in request.path
|
||||
and "sharePeer/get" not in request.path
|
||||
and "isTotpEnabled" not in request.path
|
||||
):
|
||||
response = Flask.make_response(app, {
|
||||
@@ -1239,6 +1398,10 @@ def auth_req():
|
||||
response.status_code = 401
|
||||
return response
|
||||
|
||||
@app.route('/api/handshake', methods=["GET", "OPTIONS"])
|
||||
def API_ValidateAPIKey():
|
||||
return ResponseObject(True)
|
||||
|
||||
|
||||
@app.route('/api/validateAuthentication', methods=["GET"])
|
||||
def API_ValidateAuthentication():
|
||||
@@ -1251,6 +1414,17 @@ def API_ValidateAuthentication():
|
||||
@app.route('/api/authenticate', methods=['POST'])
|
||||
def API_AuthenticateLogin():
|
||||
data = request.get_json()
|
||||
if DashboardConfig.APIAccessed:
|
||||
|
||||
authToken = hashlib.sha256(f"{request.headers.get('wg-dashboard-apikey')}{datetime.now()}".encode()).hexdigest()
|
||||
session['username'] = authToken
|
||||
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
|
||||
print(data['host'])
|
||||
resp.set_cookie("authToken", authToken, domain=data['host'])
|
||||
session.permanent = True
|
||||
return resp
|
||||
|
||||
|
||||
valid = bcrypt.checkpw(data['password'].encode("utf-8"),
|
||||
DashboardConfig.GetConfig("Account", "password")[1].encode("utf-8"))
|
||||
totpEnabled = DashboardConfig.GetConfig("Account", "enable_totp")[1]
|
||||
@@ -1268,8 +1442,10 @@ def API_AuthenticateLogin():
|
||||
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
|
||||
resp.set_cookie("authToken", authToken)
|
||||
session.permanent = True
|
||||
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login success: {data['username']}")
|
||||
return resp
|
||||
|
||||
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login failed: {data['username']}")
|
||||
if totpEnabled:
|
||||
return ResponseObject(False, "Sorry, your username, password or OTP is incorrect.")
|
||||
else:
|
||||
@@ -1286,8 +1462,6 @@ def API_SignOut():
|
||||
@app.route('/api/getWireguardConfigurations', methods=["GET"])
|
||||
def API_getWireguardConfigurations():
|
||||
# WireguardConfigurations = _getConfigurationList()
|
||||
print("in request::::")
|
||||
print(list(WireguardConfigurations.keys()))
|
||||
return ResponseObject(data=[wc for wc in WireguardConfigurations.values()])
|
||||
|
||||
|
||||
@@ -1390,7 +1564,7 @@ def API_newDashboardAPIKey():
|
||||
if data['neverExpire']:
|
||||
expiredAt = None
|
||||
else:
|
||||
expiredAt = datetime.strptime(data['ExpiredAt'], '%Y-%m-%dT%H:%M:%S')
|
||||
expiredAt = datetime.strptime(data['ExpiredAt'], '%Y-%m-%d %H:%M:%S')
|
||||
DashboardConfig.createAPIKeys(expiredAt)
|
||||
return ResponseObject(True, data=DashboardConfig.DashboardAPIKeys)
|
||||
except Exception as e:
|
||||
@@ -1427,6 +1601,20 @@ def API_updatePeerSettings(configName):
|
||||
allowed_ip, endpoint_allowed_ip, mtu, keepalive)
|
||||
return ResponseObject(False, "Peer does not exist")
|
||||
|
||||
@app.route('/api/resetPeerData/<configName>', methods=['POST'])
|
||||
def API_resetPeerData(configName):
|
||||
data = request.get_json()
|
||||
id = data['id']
|
||||
type = data['type']
|
||||
if len(id) == 0 or configName not in WireguardConfigurations.keys():
|
||||
return ResponseObject(False, "Configuration/Peer does not exist")
|
||||
wgc = WireguardConfigurations.get(configName)
|
||||
foundPeer, peer = wgc.searchPeer(id)
|
||||
if not foundPeer:
|
||||
return ResponseObject(False, "Configuration/Peer does not exist")
|
||||
return ResponseObject(status=peer.resetDataUsage(type))
|
||||
|
||||
|
||||
|
||||
@app.route('/api/deletePeers/<configName>', methods=['POST'])
|
||||
def API_deletePeers(configName: str) -> ResponseObject:
|
||||
@@ -1452,6 +1640,61 @@ def API_restrictPeers(configName: str) -> ResponseObject:
|
||||
return configuration.restrictPeers(peers)
|
||||
return ResponseObject(False, "Configuration does not exist")
|
||||
|
||||
@app.route('/api/sharePeer/create', methods=['POST'])
|
||||
def API_sharePeer_create():
|
||||
data: dict[str, str] = request.get_json()
|
||||
Configuration = data.get('Configuration')
|
||||
Peer = data.get('Peer')
|
||||
ExpireDate = data.get('ExpireDate')
|
||||
if Configuration is None or Peer is None:
|
||||
return ResponseObject(False, "Please specify configuration and peer")
|
||||
activeLink = AllPeerShareLinks.getLink(Configuration, Peer)
|
||||
if len(activeLink) > 0:
|
||||
return ResponseObject(False, "This peer is already sharing, please stop sharing first.")
|
||||
status, message = AllPeerShareLinks.addLink(Configuration, Peer, ExpireDate)
|
||||
if not status:
|
||||
return ResponseObject(status, message)
|
||||
return ResponseObject(data=AllPeerShareLinks.getLinkByID(message))
|
||||
|
||||
@app.route('/api/sharePeer/update', methods=['POST'])
|
||||
def API_sharePeer_update():
|
||||
data: dict[str, str] = request.get_json()
|
||||
ShareID: str = data.get("ShareID")
|
||||
ExpireDate: str = data.get("ExpireDate")
|
||||
print(ShareID)
|
||||
print(ExpireDate)
|
||||
|
||||
if ShareID is None:
|
||||
return ResponseObject(False, "Please specify ShareID")
|
||||
|
||||
if len(AllPeerShareLinks.getLinkByID(ShareID)) == 0:
|
||||
return ResponseObject(False, "ShareID does not exist")
|
||||
|
||||
status, message = AllPeerShareLinks.updateLinkExpireDate(ShareID, ExpireDate)
|
||||
if not status:
|
||||
return ResponseObject(status, message)
|
||||
return ResponseObject(data=AllPeerShareLinks.getLinkByID(ShareID))
|
||||
|
||||
@app.route('/api/sharePeer/get', methods=['GET'])
|
||||
def API_sharePeer_get():
|
||||
data = request.args
|
||||
ShareID = data.get("ShareID")
|
||||
if ShareID is None or len(ShareID) == 0:
|
||||
return ResponseObject(False, "Please provide ShareID")
|
||||
link = AllPeerShareLinks.getLinkByID(ShareID)
|
||||
if len(link) == 0:
|
||||
return ResponseObject(False, "This link is either expired to invalid")
|
||||
l = link[0]
|
||||
if l.Configuration not in WireguardConfigurations.keys():
|
||||
return ResponseObject(False, "The peer you're looking for does not exist")
|
||||
c = WireguardConfigurations.get(l.Configuration)
|
||||
fp, p = c.searchPeer(l.Peer)
|
||||
if not fp:
|
||||
return ResponseObject(False, "The peer you're looking for does not exist")
|
||||
|
||||
return ResponseObject(data=p.downloadPeer())
|
||||
|
||||
|
||||
|
||||
@app.route('/api/allowAccessPeers/<configName>', methods=['POST'])
|
||||
def API_allowAccessPeers(configName: str) -> ResponseObject:
|
||||
@@ -1483,8 +1726,7 @@ def API_addPeers(configName):
|
||||
if (not bulkAdd and (len(public_key) == 0 or len(allowed_ips) == 0)) or len(endpoint_allowed_ip) == 0:
|
||||
return ResponseObject(False, "Please fill in all required box.")
|
||||
if not config.getStatus():
|
||||
return ResponseObject(False,
|
||||
f"{configName} is not running, please turn on the configuration before adding peers.")
|
||||
config.toggleConfiguration()
|
||||
if bulkAdd:
|
||||
if bulkAddAmount < 1:
|
||||
return ResponseObject(False, "Please specify amount of peers you want to add")
|
||||
@@ -1497,28 +1739,24 @@ def API_addPeers(configName):
|
||||
|
||||
keyPairs = []
|
||||
for i in range(bulkAddAmount):
|
||||
key = _generatePrivateKey()[1]
|
||||
keyPairs.append([key, _generatePublicKey(key)[1], _generatePrivateKey()[1], availableIps[1][i],
|
||||
f"{config.Name}_{datetime.now().strftime('%m%d%Y%H%M%S')}_Peer_#_{(i + 1)}"])
|
||||
newPrivateKey = _generatePrivateKey()[1]
|
||||
keyPairs.append({
|
||||
"private_key": newPrivateKey,
|
||||
"id": _generatePublicKey(newPrivateKey)[1],
|
||||
"preshared_key": _generatePrivateKey()[1],
|
||||
"allowed_ip": availableIps[1][i],
|
||||
"name": f"BulkPeer #{(i + 1)}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
})
|
||||
if len(keyPairs) == 0:
|
||||
return ResponseObject(False, "Generating key pairs by bulk failed")
|
||||
config.addPeers(keyPairs)
|
||||
|
||||
for i in range(bulkAddAmount):
|
||||
subprocess.check_output(
|
||||
f"wg set {config.Name} peer {keyPairs[i][1]} allowed-ips {keyPairs[i][3]}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
subprocess.check_output(
|
||||
f"wg-quick save {config.Name}", shell=True, stderr=subprocess.STDOUT)
|
||||
config.getPeersList()
|
||||
|
||||
for i in range(bulkAddAmount):
|
||||
found, peer = config.searchPeer(keyPairs[i][1])
|
||||
for kp in keyPairs:
|
||||
found, peer = config.searchPeer(kp['id'])
|
||||
if found:
|
||||
if not peer.updatePeer(keyPairs[i][4], keyPairs[i][0], preshared_key, dns_addresses,
|
||||
keyPairs[i][3],
|
||||
endpoint_allowed_ip, mtu, keep_alive).status:
|
||||
if not peer.updatePeer(kp['name'], kp['private_key'], kp['preshared_key'], dns_addresses,
|
||||
kp['allowed_ip'], endpoint_allowed_ip, mtu, keep_alive):
|
||||
return ResponseObject(False, "Failed to add peers in bulk")
|
||||
|
||||
return ResponseObject()
|
||||
|
||||
else:
|
||||
@@ -1526,16 +1764,17 @@ def API_addPeers(configName):
|
||||
return ResponseObject(False, f"This peer already exist.")
|
||||
name = data['name']
|
||||
private_key = data['private_key']
|
||||
subprocess.check_output(
|
||||
f"wg set {config.Name} peer {public_key} allowed-ips {''.join(allowed_ips)}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
if len(preshared_key) > 0:
|
||||
subprocess.check_output(
|
||||
f"wg set {config.Name} peer {public_key} preshared-key {preshared_key}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
subprocess.check_output(
|
||||
f"wg-quick save {config.Name}", shell=True, stderr=subprocess.STDOUT)
|
||||
config.getPeersList()
|
||||
config.addPeers([{"id": public_key, "allowed_ip": ''.join(allowed_ips)}])
|
||||
# subprocess.check_output(
|
||||
# f"wg set {config.Name} peer {public_key} allowed-ips {''.join(allowed_ips)}",
|
||||
# shell=True, stderr=subprocess.STDOUT)
|
||||
# if len(preshared_key) > 0:
|
||||
# subprocess.check_output(
|
||||
# f"wg set {config.Name} peer {public_key} preshared-key {preshared_key}",
|
||||
# shell=True, stderr=subprocess.STDOUT)
|
||||
# subprocess.check_output(
|
||||
# f"wg-quick save {config.Name}", shell=True, stderr=subprocess.STDOUT)
|
||||
# config.getPeersList()
|
||||
found, peer = config.searchPeer(public_key)
|
||||
if found:
|
||||
return peer.updatePeer(name, private_key, preshared_key, dns_addresses, ",".join(allowed_ips),
|
||||
@@ -1750,12 +1989,14 @@ Sign Up
|
||||
|
||||
@app.route('/api/isTotpEnabled')
|
||||
def API_isTotpEnabled():
|
||||
return ResponseObject(data=DashboardConfig.GetConfig("Account", "enable_totp")[1])
|
||||
return (
|
||||
ResponseObject(data=DashboardConfig.GetConfig("Account", "enable_totp")[1] and DashboardConfig.GetConfig("Account", "totp_verified")[1]))
|
||||
|
||||
|
||||
@app.route('/api/Welcome_GetTotpLink')
|
||||
def API_Welcome_GetTotpLink():
|
||||
if DashboardConfig.GetConfig("Other", "welcome_session")[1]:
|
||||
if not DashboardConfig.GetConfig("Account", "totp_verified")[1]:
|
||||
DashboardConfig.SetConfig("Account", "totp_key", pyotp.random_base32())
|
||||
return ResponseObject(
|
||||
data=pyotp.totp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).provisioning_uri(
|
||||
issuer_name="WGDashboard"))
|
||||
@@ -1765,11 +2006,11 @@ def API_Welcome_GetTotpLink():
|
||||
@app.route('/api/Welcome_VerifyTotpLink', methods=["POST"])
|
||||
def API_Welcome_VerifyTotpLink():
|
||||
data = request.get_json()
|
||||
if DashboardConfig.GetConfig("Other", "welcome_session")[1]:
|
||||
totp = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now()
|
||||
print(totp)
|
||||
return ResponseObject(totp == data['totp'])
|
||||
return ResponseObject(False)
|
||||
totp = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now()
|
||||
if totp == data['totp']:
|
||||
DashboardConfig.SetConfig("Account", "totp_verified", "true")
|
||||
DashboardConfig.SetConfig("Account", "enable_totp", "true")
|
||||
return ResponseObject(totp == data['totp'])
|
||||
|
||||
|
||||
@app.route('/api/Welcome_Finish', methods=["POST"])
|
||||
@@ -1789,10 +2030,10 @@ def API_Welcome_Finish():
|
||||
"repeatNewPassword": data["repeatNewPassword"],
|
||||
"currentPassword": "admin"
|
||||
})
|
||||
updateEnableTotp, updateEnableTotpErr = DashboardConfig.SetConfig("Account", "enable_totp", data["enable_totp"])
|
||||
# updateEnableTotp, updateEnableTotpErr = DashboardConfig.SetConfig("Account", "enable_totp", data["enable_totp"])
|
||||
|
||||
if not updateUsername or not updatePassword or not updateEnableTotp:
|
||||
return ResponseObject(False, f"{updateUsernameErr},{updatePasswordErr},{updateEnableTotpErr}".strip(","))
|
||||
if not updateUsername or not updatePassword:
|
||||
return ResponseObject(False, f"{updateUsernameErr},{updatePasswordErr}".strip(","))
|
||||
|
||||
DashboardConfig.SetConfig("Other", "welcome_session", False)
|
||||
|
||||
@@ -1810,8 +2051,8 @@ def index():
|
||||
|
||||
def backGroundThread():
|
||||
with app.app_context():
|
||||
print("Waiting 5 sec")
|
||||
time.sleep(5)
|
||||
print(f"[WGDashboard] Background Thread #1 Started", flush=True)
|
||||
time.sleep(10)
|
||||
while True:
|
||||
for c in WireguardConfigurations.values():
|
||||
if c.getStatus():
|
||||
@@ -1819,19 +2060,19 @@ def backGroundThread():
|
||||
c.getPeersTransfer()
|
||||
c.getPeersLatestHandshake()
|
||||
c.getPeersEndpoint()
|
||||
c.getPeersList()
|
||||
except Exception as e:
|
||||
print("Error: " + str(e))
|
||||
print(f"[WGDashboard] Background Thread #1 Error: {str(e)}", flush=True)
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
def peerJobScheduleBackgroundThread():
|
||||
with app.app_context():
|
||||
print(f'''[{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Peer Job Schedule: Waiting for 10 Seconds''')
|
||||
print(f"[WGDashboard] Background Thread #2 Started", flush=True)
|
||||
time.sleep(10)
|
||||
while True:
|
||||
print(f'''[{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Peer Job Schedule: Running''')
|
||||
AllPeerJobs.runJob()
|
||||
time.sleep(10)
|
||||
time.sleep(180)
|
||||
|
||||
|
||||
def gunicornConfig():
|
||||
@@ -1839,28 +2080,39 @@ def gunicornConfig():
|
||||
_, app_port = DashboardConfig.GetConfig("Server", "app_port")
|
||||
return app_ip, app_port
|
||||
|
||||
|
||||
import sys
|
||||
if sys.version_info < (3, 10):
|
||||
from typing_extensions import ParamSpec
|
||||
else:
|
||||
from typing import ParamSpec
|
||||
|
||||
sqldb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'), check_same_thread=False)
|
||||
sqldb.row_factory = sqlite3.Row
|
||||
cursor = sqldb.cursor()
|
||||
DashboardConfig = DashboardConfig()
|
||||
|
||||
AllPeerShareLinks: PeerShareLinks = PeerShareLinks()
|
||||
AllPeerJobs: PeerJobs = PeerJobs()
|
||||
JobLogger: Logger = Logger()
|
||||
JobLogger: PeerJobLogger = PeerJobLogger()
|
||||
DashboardLogger: DashboardLogger = DashboardLogger()
|
||||
_, app_ip = DashboardConfig.GetConfig("Server", "app_ip")
|
||||
_, app_port = DashboardConfig.GetConfig("Server", "app_port")
|
||||
_, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
|
||||
|
||||
WireguardConfigurations: dict[str, WireguardConfiguration] = {}
|
||||
WireguardConfigurations = _getConfigurationList()
|
||||
bgThread = threading.Thread(target=backGroundThread)
|
||||
bgThread.daemon = True
|
||||
bgThread.start()
|
||||
|
||||
scheduleJobThread = threading.Thread(target=peerJobScheduleBackgroundThread)
|
||||
scheduleJobThread.daemon = True
|
||||
scheduleJobThread.start()
|
||||
|
||||
def startThreads():
|
||||
bgThread = threading.Thread(target=backGroundThread)
|
||||
bgThread.daemon = True
|
||||
bgThread.start()
|
||||
|
||||
scheduleJobThread = threading.Thread(target=peerJobScheduleBackgroundThread)
|
||||
scheduleJobThread.daemon = True
|
||||
scheduleJobThread.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
startThreads()
|
||||
app.run(host=app_ip, debug=False, port=app_port)
|
||||
|
||||
+15
-1
@@ -1,8 +1,14 @@
|
||||
import multiprocessing
|
||||
import dashboard
|
||||
from datetime import datetime
|
||||
|
||||
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger
|
||||
app_host, app_port = dashboard.gunicornConfig()
|
||||
date = datetime.today().strftime('%Y_%m_%d_%H_%M_%S')
|
||||
|
||||
|
||||
def post_worker_init(worker):
|
||||
dashboard.startThreads()
|
||||
|
||||
|
||||
worker_class = 'gthread'
|
||||
workers = 1
|
||||
@@ -10,3 +16,11 @@ threads = 1
|
||||
bind = f"{app_host}:{app_port}"
|
||||
daemon = True
|
||||
pidfile = './gunicorn.pid'
|
||||
wsgi_app = "dashboard:app"
|
||||
accesslog = f"./log/access_{date}.log"
|
||||
log_level = "debug"
|
||||
capture_output = True
|
||||
errorlog = f"./log/error_{date}.log"
|
||||
print(f"[WGDashboard] WGDashboard w/ Gunicorn will be running on {bind}", flush=True)
|
||||
print(f"[WGDashboard] Access log file is at {accesslog}", flush=True)
|
||||
print(f"[WGDashboard] Error log file is at {errorlog}", flush=True)
|
||||
|
||||
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+30
-30
File diff suppressed because one or more lines are too long
Generated
+2310
-2
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "0.0.0",
|
||||
"version": "4.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build electron": "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",
|
||||
@@ -16,6 +18,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1,18 +1,42 @@
|
||||
<script setup >
|
||||
<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">
|
||||
<nav class="navbar bg-dark sticky-top border-bottom border-secondary-subtle" 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"><i class="bi bi-list"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
<Suspense>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="fade2" mode="out-in">
|
||||
<Transition name="app" mode="out-in">
|
||||
<Component :is="Component"></Component>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
@@ -22,14 +46,16 @@ const store = DashboardConfigurationStore();
|
||||
<style scoped>
|
||||
.app-enter-active,
|
||||
.app-leave-active {
|
||||
transition: all 0.3s ease-in-out;
|
||||
/*position: absolute;*/
|
||||
/*padding-top: 50px*/
|
||||
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
.app-enter-from{
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.app-enter-from,
|
||||
.app-leave-to {
|
||||
transform: translateX(-30px);
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
+13
-6
@@ -14,7 +14,6 @@ export default {
|
||||
data(){
|
||||
return {
|
||||
allowedIp: [],
|
||||
|
||||
availableIpSearchString: "",
|
||||
customAvailableIp: "",
|
||||
allowedIpFormatError: false
|
||||
@@ -45,7 +44,15 @@ export default {
|
||||
watch: {
|
||||
customAvailableIp(){
|
||||
this.allowedIpFormatError = false;
|
||||
},
|
||||
availableIp(){
|
||||
if (this.availableIp !== undefined && this.availableIp.length > 0){
|
||||
this.addAllowedIp(this.availableIp[0])
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -57,11 +64,11 @@ export default {
|
||||
</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>
|
||||
<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">
|
||||
|
||||
@@ -86,6 +86,7 @@ export default {
|
||||
@setting="this.$emit('setting')"
|
||||
@jobs="this.$emit('jobs')"
|
||||
@refresh="this.$emit('refresh')"
|
||||
@share="this.$emit('share')"
|
||||
:Peer="Peer"
|
||||
v-if="this.subMenuOpened"
|
||||
ref="target"
|
||||
|
||||
@@ -40,6 +40,7 @@ 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,
|
||||
@@ -70,6 +71,7 @@ Chart.register(
|
||||
export default {
|
||||
name: "peerList",
|
||||
components: {
|
||||
PeerShareLinkModal,
|
||||
PeerJobsLogsModal,
|
||||
PeerJobsAllModal, PeerJobs, PeerCreate, PeerQRCode, PeerSettings, PeerSearch, Peer, Line, Bar},
|
||||
setup(){
|
||||
@@ -131,6 +133,10 @@ export default {
|
||||
},
|
||||
peerScheduleJobsLogs: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerShare:{
|
||||
modalOpen: false,
|
||||
selectedPeer: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -141,26 +147,21 @@ export default {
|
||||
'$route': {
|
||||
immediate: true,
|
||||
handler(){
|
||||
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
|
||||
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
|
||||
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
|
||||
|
||||
this.loading = true;
|
||||
let id = this.$route.params.id;
|
||||
this.configurationInfo = [];
|
||||
this.configurationPeers = [];
|
||||
if (id){
|
||||
this.getPeers(id)
|
||||
console.log("Changed..")
|
||||
this.setPeerInterval();
|
||||
}
|
||||
}
|
||||
},
|
||||
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
|
||||
console.log("Changed?")
|
||||
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
|
||||
this.setPeerInterval();
|
||||
},
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(){
|
||||
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
|
||||
@@ -211,9 +212,10 @@ export default {
|
||||
{
|
||||
label: 'Data Sent',
|
||||
data: [...this.historySentData.datasets[0].data,
|
||||
((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000).toFixed(4)],
|
||||
((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000)
|
||||
.toFixed(4)],
|
||||
fill: false,
|
||||
borderColor: ' #198754',
|
||||
borderColor: '#198754',
|
||||
tension: 0
|
||||
}
|
||||
],
|
||||
@@ -231,7 +233,8 @@ export default {
|
||||
{
|
||||
label: 'Data Received',
|
||||
data: [...this.historyReceiveData.datasets[0].data,
|
||||
((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000).toFixed(4)],
|
||||
((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000)
|
||||
.toFixed(4)],
|
||||
fill: false,
|
||||
borderColor: '#0d6efd',
|
||||
tension: 0
|
||||
@@ -248,17 +251,24 @@ export default {
|
||||
this.dashboardConfigurationStore.Peers.RefreshInterval = setInterval(() => {
|
||||
this.getPeers()
|
||||
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
|
||||
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
configurationSummary(){
|
||||
return {
|
||||
const k = {
|
||||
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length,
|
||||
totalUsage: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b) : 0,
|
||||
totalReceive: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b) : 0,
|
||||
totalSent: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b) : 0
|
||||
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
|
||||
@@ -468,7 +478,7 @@ export default {
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>Total Usage</small></p>
|
||||
<strong class="h4">{{configurationSummary.totalUsage.toFixed(4)}} GB</strong>
|
||||
<strong class="h4">{{configurationSummary.totalUsage}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
@@ -479,7 +489,7 @@ export default {
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>Total Received</small></p>
|
||||
<strong class="h4 text-primary">{{configurationSummary.totalReceive.toFixed(4)}} GB</strong>
|
||||
<strong class="h4 text-primary">{{configurationSummary.totalReceive}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
@@ -490,7 +500,7 @@ export default {
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>Total Sent</small></p>
|
||||
<strong class="h4 text-success">{{configurationSummary.totalSent.toFixed(4)}} GB</strong>
|
||||
<strong class="h4 text-success">{{configurationSummary.totalSent}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
@@ -548,7 +558,7 @@ export default {
|
||||
: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)"
|
||||
@@ -595,6 +605,12 @@ export default {
|
||||
>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfigData , (error) => {
|
||||
console.log(this.peerConfigData)
|
||||
if (error) console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
+36
-7
@@ -3,10 +3,12 @@ import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJ
|
||||
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: {ScheduleDropdown},
|
||||
components: {VueDatePicker, ScheduleDropdown},
|
||||
props: {
|
||||
dropdowns: Array[Object],
|
||||
pjob: Object,
|
||||
@@ -94,6 +96,11 @@ export default {
|
||||
})
|
||||
}
|
||||
this.$emit('delete')
|
||||
},
|
||||
parseTime(modelData){
|
||||
if(modelData){
|
||||
this.job.Value = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -128,12 +135,26 @@ export default {
|
||||
:data="this.job.Operator"
|
||||
@update="(value) => this.job.Operator = value"
|
||||
></ScheduleDropdown>
|
||||
<input class="form-control form-control-sm form-control-dark rounded-3 flex-grow-1"
|
||||
:disabled="!edit"
|
||||
type="datetime-local"
|
||||
v-if="this.job.Field === 'date'"
|
||||
v-model="this.job.Value"
|
||||
style="width: auto">
|
||||
|
||||
<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
|
||||
@@ -188,4 +209,12 @@ input:disabled{
|
||||
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>
|
||||
@@ -89,67 +89,59 @@ export default {
|
||||
class="text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm">
|
||||
<i class="bi bi-plus-lg me-2"></i>Peer
|
||||
</RouterLink>
|
||||
|
||||
<!-- <RouterLink-->
|
||||
<!-- to="jobs"-->
|
||||
<!-- class="text-decoration-none btn btn-primary rounded-3 btn-sm">-->
|
||||
<!-- <i class="bi bi-app-indicator me-2"></i>Jobs-->
|
||||
<!-- </RouterLink>-->
|
||||
<button class="btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm"
|
||||
@click="this.downloadAllPeer()">
|
||||
<i class="bi bi-download me-2"></i> Download All
|
||||
</button>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<!-- <label class="d-flex me-2 text-muted" for="searchPeers"><i class="bi bi-search me-1"></i></label>-->
|
||||
<input class="form-control rounded-3 bg-secondary-subtle border-1 border-secondary-subtle shadow-sm"
|
||||
<div class="flex-grow-1">
|
||||
<input class="form-control rounded-3 bg-secondary-subtle border-1 border-secondary-subtle shadow-sm w-100"
|
||||
placeholder="Search..."
|
||||
id="searchPeers"
|
||||
@keyup="this.debounce()"
|
||||
v-model="this.searchString">
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
|
||||
<div class="dropdown dropup">
|
||||
<button class="btn text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
|
||||
type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-filter-circle me-2"></i>
|
||||
Sort
|
||||
Display
|
||||
</button>
|
||||
<ul class="dropdown-menu mt-2 shadow rounded-3">
|
||||
<ul class="dropdown-menu mt-2 shadow rounded-3 animate__animated animation__fadeInDropdown dropdown-menu-end">
|
||||
<li>
|
||||
<small class="dropdown-header">Sort by</small>
|
||||
</li>
|
||||
<li v-for="(value, key) in this.sort">
|
||||
<a class="dropdown-item d-flex" role="button" @click="this.updateSort(key)">
|
||||
<span class="me-auto">{{value}}</span>
|
||||
<a class="dropdown-item d-flex align-items-center" role="button" @click="this.updateSort(key)">
|
||||
<small class="me-auto">{{value}}</small>
|
||||
<i class="bi bi-check text-primary"
|
||||
v-if="store.Configuration.Server.dashboard_sort === key"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
|
||||
type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Refresh Interval
|
||||
</button>
|
||||
<ul class="dropdown-menu shadow mt-2 rounded-3">
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<small class="dropdown-header">Refresh Interval</small>
|
||||
</li>
|
||||
<li v-for="(value, key) in this.interval">
|
||||
<a class="dropdown-item d-flex" role="button" @click="updateRefreshInterval(key)">
|
||||
<span class="me-auto">{{value}}</span>
|
||||
<small class="me-auto">{{value}}</small>
|
||||
<i class="bi bi-check text-primary"
|
||||
v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i>
|
||||
</a></li>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
|
||||
<div class="dropdown dropup">
|
||||
<button class="btn text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
|
||||
type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-three-dots me-2"></i>More
|
||||
<i class="bi bi-three-dots"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu shadow mt-2 rounded-3">
|
||||
<ul class="dropdown-menu shadow mt-2 rounded-3 animate__animated animation__fadeInDropdown">
|
||||
<li>
|
||||
<h6 class="dropdown-header">Peer Jobs</h6>
|
||||
</li>
|
||||
<li>
|
||||
<a role="button" class="dropdown-item" @click="this.$emit('jobsAll')">
|
||||
All Active Jobs
|
||||
Active Jobs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -165,4 +157,23 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
|
||||
.animation__fadeInDropdown{
|
||||
animation-name: fadeInDropdown;
|
||||
animation-duration: 0.2s;
|
||||
animation-timing-function: cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
@keyframes fadeInDropdown{
|
||||
0%{
|
||||
opacity: 0;
|
||||
filter: blur(3px);
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
100%{
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -37,6 +37,21 @@ export default {
|
||||
}
|
||||
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() {
|
||||
@@ -57,15 +72,15 @@ export default {
|
||||
<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">
|
||||
<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>
|
||||
<small class="text-muted">Public Key</small><br>
|
||||
<small><samp>{{this.data.id}}</samp></small>
|
||||
<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">
|
||||
@@ -121,8 +136,7 @@ export default {
|
||||
v-model="this.data.DNS"
|
||||
id="peer_DNS_textbox">
|
||||
</div>
|
||||
<hr>
|
||||
<div class="accordion mt-2" id="peerSettingsAccordion">
|
||||
<div class="accordion mt-3" id="peerSettingsAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button rounded-3 collapsed" type="button"
|
||||
@@ -162,12 +176,37 @@ export default {
|
||||
</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">
|
||||
Reset <i class="bi bi-arrow-clockwise ms-2"></i>
|
||||
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"
|
||||
|
||||
@@ -93,8 +93,23 @@ export default {
|
||||
set for this peer
|
||||
</small>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></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')"
|
||||
@@ -109,20 +124,7 @@ export default {
|
||||
<i class="me-auto bi bi-app-indicator"></i> Schedule Jobs
|
||||
</a>
|
||||
</li>
|
||||
<template v-if="this.Peer.private_key">
|
||||
<li>
|
||||
<a class="dropdown-item d-flex" role="button" @click="this.downloadPeer()">
|
||||
<i class="me-auto bi bi-download"></i> Download
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex" role="button"
|
||||
@click="this.downloadQRCode()"
|
||||
>
|
||||
<i class="me-auto bi bi-qr-code"></i> QR Code
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
|
||||
@@ -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>
|
||||
@@ -8,7 +8,6 @@ export default {
|
||||
components: {ConfigurationCard},
|
||||
async setup(){
|
||||
const wireguardConfigurationsStore = WireguardConfigurationsStore();
|
||||
|
||||
return {wireguardConfigurationsStore}
|
||||
},
|
||||
data(){
|
||||
@@ -19,6 +18,13 @@ export default {
|
||||
async mounted() {
|
||||
await this.wireguardConfigurationsStore.getConfigurations();
|
||||
this.configurationLoaded = true;
|
||||
|
||||
this.wireguardConfigurationsStore.ConfigurationListInterval = setInterval(() => {
|
||||
this.wireguardConfigurationsStore.getConfigurations()
|
||||
}, 10000)
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.wireguardConfigurationsStore.ConfigurationListInterval)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -28,7 +34,9 @@ export default {
|
||||
<div class="mt-5">
|
||||
<div class="container">
|
||||
<div class="d-flex mb-4 ">
|
||||
<h3 class="text-body">WireGuard Configurations</h3>
|
||||
<h3 class="text-body">
|
||||
<i class="bi bi-body-text me-2"></i>
|
||||
WireGuard Configurations</h3>
|
||||
<RouterLink to="/new_configuration" class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto rounded-3">
|
||||
<i class="bi bi-plus-circle-fill me-2"></i>
|
||||
Configuration
|
||||
@@ -40,8 +48,8 @@ export default {
|
||||
<p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0">
|
||||
You don't have any WireGuard configurations yet. Please check the configuration folder or change it in "Settings". By default the folder is "/etc/wireguard".
|
||||
</p>
|
||||
<div class="d-flex gap-3 flex-column" v-else>
|
||||
<ConfigurationCard v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name" :c="c"></ConfigurationCard>
|
||||
<div 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>
|
||||
|
||||
@@ -10,7 +10,6 @@ export default {
|
||||
Status: Boolean,
|
||||
PublicKey: String,
|
||||
PrivateKey: String
|
||||
|
||||
}
|
||||
},
|
||||
data(){
|
||||
@@ -52,27 +51,43 @@ export default {
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</h6>
|
||||
</RouterLink>
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
<small class="me-2 text-muted">
|
||||
<strong>PUBLIC KEY</strong>
|
||||
</small>
|
||||
<small class="mb-0 d-block d-lg-inline-block ">
|
||||
<samp style="line-break: anywhere">{{c.PublicKey}}</samp>
|
||||
</small>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<label class="form-check-label" style="cursor: pointer" :for="'switch' + c.PrivateKey">
|
||||
{{this.configurationToggling ? 'Turning ':''}}
|
||||
{{c.Status ? "On":"Off"}}
|
||||
<span v-if="this.configurationToggling"
|
||||
class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||
</label>
|
||||
<input class="form-check-input"
|
||||
style="cursor: pointer"
|
||||
:disabled="this.configurationToggling"
|
||||
type="checkbox" role="switch" :id="'switch' + c.PrivateKey"
|
||||
@change="this.toggle()"
|
||||
v-model="c.Status"
|
||||
>
|
||||
<div class="card-footer d-flex gap-2 flex-column">
|
||||
<div class="d-flex gap-4">
|
||||
<small >
|
||||
<i class="bi bi-arrow-down-up me-2"></i>{{c.DataUsage.Total > 0 ? c.DataUsage.Total.toFixed(4) : 0}} GB
|
||||
</small>
|
||||
<small class="text-primary-emphasis">
|
||||
<i class="bi bi-arrow-down me-2"></i>{{c.DataUsage.Receive > 0 ? c.DataUsage.Receive.toFixed(4) : 0}} GB
|
||||
</small>
|
||||
<small class="text-success-emphasis">
|
||||
<i class="bi bi-arrow-up me-2"></i>{{c.DataUsage.Sent > 0 ? c.DataUsage.Sent.toFixed(4) : 0}} GB
|
||||
</small>
|
||||
<small class="ms-auto">
|
||||
<span class="dot me-2" :class="{active: c.ConnectedPeers > 0}"></span>{{c.ConnectedPeers}} Peers
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<small class="me-2 text-muted">
|
||||
<strong>Public Key</strong>
|
||||
</small>
|
||||
<small class="mb-0 d-block d-lg-inline-block ">
|
||||
<samp style="line-break: anywhere">{{c.PublicKey}}</samp>
|
||||
</small>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<label class="form-check-label" style="cursor: pointer" :for="'switch' + c.PrivateKey">
|
||||
{{this.configurationToggling ? 'Turning ':''}}
|
||||
{{c.Status ? "On":"Off"}}
|
||||
<span v-if="this.configurationToggling"
|
||||
class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||
</label>
|
||||
<input class="form-check-input"
|
||||
style="cursor: pointer"
|
||||
:disabled="this.configurationToggling"
|
||||
type="checkbox" role="switch" :id="'switch' + c.PrivateKey"
|
||||
@change="this.toggle()"
|
||||
v-model="c.Status"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,44 +14,55 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col-md-3 col-lg-2 d-md-block p-3" style="height: calc(-50px + 100vh);">
|
||||
<div class="col-md-3 col-lg-2 d-md-block p-3 navbar-container bg-body" 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">
|
||||
<ul class="nav flex-column px-2">
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link" to="/" exact-active-class="active">Home</RouterLink></li>
|
||||
<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" to="/settings"
|
||||
exact-active-class="active">Settings</RouterLink></li>
|
||||
<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>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Configurations</span>
|
||||
<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">
|
||||
<ul class="nav flex-column px-2">
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link"
|
||||
<RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link rounded-3"
|
||||
active-class="active"
|
||||
|
||||
v-for="c in this.wireguardConfigurationsStore.Configurations">
|
||||
<samp>{{c.Name}}</samp>
|
||||
{{c.Name}}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Tools</span>
|
||||
<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">
|
||||
<ul class="nav flex-column px-2">
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/ping" class="nav-link" active-class="active">Ping</RouterLink></li>
|
||||
<RouterLink to="/ping" class="nav-link rounded-3" active-class="active">Ping</RouterLink></li>
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/traceroute" class="nav-link" active-class="active">Traceroute</RouterLink>
|
||||
<RouterLink to="/traceroute" class="nav-link rounded-3" active-class="active">Traceroute</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link text-danger" @click="this.dashboardConfigurationStore.signOut()" role="button" style="font-weight: bold">Sign Out</a></li>
|
||||
<hr class="text-body">
|
||||
<ul class="nav flex-column px-2">
|
||||
<li class="nav-item"><a class="nav-link text-danger rounded-3"
|
||||
@click="this.dashboardConfigurationStore.signOut()"
|
||||
role="button" style="font-weight: bold">
|
||||
<i class="bi bi-box-arrow-left me-2"></i>
|
||||
Sign Out</a></li>
|
||||
</ul>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a href="https://github.com/donaldzou/WGDashboard/releases/tag/"><small class="nav-link text-muted"></small></a></li>
|
||||
@@ -64,5 +75,10 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navbar-container{
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {v4} from "uuid";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "accountSettingsMFA",
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
const uuid = `input_${v4()}`;
|
||||
return {store, uuid};
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
status: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.status = this.store.Configuration.Account["enable_totp"]
|
||||
},
|
||||
methods: {
|
||||
async resetMFA(){
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Account",
|
||||
key: "totp_verified",
|
||||
value: "false"
|
||||
}, async (res) => {
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Account",
|
||||
key: "enable_totp",
|
||||
value: "false"
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.$router.push("/2FASetup")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<strong>Multi-Factor Authentication</strong>
|
||||
<div class="form-check form-switch ms-3">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
v-model="this.status"
|
||||
role="switch" id="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>
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<div class="card-header d-flex">
|
||||
API Keys
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<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()"
|
||||
@@ -78,6 +78,7 @@ export default {
|
||||
<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>
|
||||
|
||||
+28
-5
@@ -35,26 +35,31 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="card rounded-3 shadow-sm">
|
||||
<div class="card-body d-flex gap-3 align-items-center" v-if="!this.confirmDelete">
|
||||
<div class="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>{{this.apiKey.Key}}
|
||||
<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" @click="this.confirmDelete = true">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
@@ -62,5 +67,23 @@ export default {
|
||||
</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>
|
||||
+22
-6
@@ -2,13 +2,15 @@
|
||||
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(1, 'd').format("YYYY-MM-DDTHH:mm:ss"),
|
||||
ExpiredAt: dayjs().add(7, 'd').format("YYYY-MM-DD HH:mm:ss"),
|
||||
neverExpire: false
|
||||
},
|
||||
submitting: false
|
||||
@@ -39,6 +41,13 @@ export default {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,16 +58,23 @@ export default {
|
||||
style="background-color: #00000060; backdrop-filter: blur(3px)">
|
||||
<div class="card m-auto rounded-3 mt-5">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
|
||||
Create API Key
|
||||
<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">
|
||||
<input class="form-control" type="datetime-local"
|
||||
@change="this.newKeyData.ExpiredAt = this.fixDate(this.newKeyData.ExpiredAt)"
|
||||
:disabled="this.newKeyData.neverExpire || this.submitting"
|
||||
v-model="this.newKeyData.ExpiredAt">
|
||||
<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"
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<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}
|
||||
return {l, store}
|
||||
},
|
||||
mounted() {
|
||||
if (this.l) {
|
||||
@@ -58,28 +60,58 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<p class="mb-2"><small class="text-muted">1. Please scan the following QR Code to generate TOTP</small></p>
|
||||
<canvas id="qrcode" class="rounded-3 mb-2"></canvas>
|
||||
<div class="p-3 bg-body-secondary rounded-3 border mb-3">
|
||||
<p class="text-muted mb-0"><small>Or you can click the link below:</small>
|
||||
</p><a :href="this.l"><code style="line-break: anywhere">{{this.l}}</code></a>
|
||||
</div>
|
||||
<label for="totp" class="mb-2"><small class="text-muted">2. Enter the TOTP generated by your authenticator to verify</small></label>
|
||||
<div class="form-group">
|
||||
<input class="form-control text-center totp"
|
||||
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
|
||||
v-model="this.totp"
|
||||
:disabled="this.verified"
|
||||
>
|
||||
<div class="invalid-feedback">
|
||||
{{this.totpInvalidMessage}}
|
||||
</div>
|
||||
<div class="valid-feedback">
|
||||
TOTP verified!
|
||||
<div 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>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<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;
|
||||
this.refreshing = true;
|
||||
if (this.server.host && this.server.apiKey){
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
|
||||
@@ -4,20 +4,18 @@ import Index from "@/views/index.vue"
|
||||
import Signin from "@/views/signin.vue";
|
||||
import ConfigurationList from "@/components/configurationList.vue";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
|
||||
import Settings from "@/views/settings.vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import Setup from "@/views/setup.vue";
|
||||
import NewConfiguration from "@/views/newConfiguration.vue";
|
||||
import Configuration from "@/views/configuration.vue";
|
||||
import PeerSettings from "@/components/configurationComponents/peerSettings.vue";
|
||||
import PeerList from "@/components/configurationComponents/peerList.vue";
|
||||
import PeerCreate from "@/components/configurationComponents/peerCreate.vue";
|
||||
import RestrictedPeers from "@/components/configurationComponents/restrictedPeers.vue";
|
||||
import Ping from "@/views/ping.vue";
|
||||
import Traceroute from "@/views/traceroute.vue";
|
||||
import PeerJobs from "@/components/configurationComponents/peerJobs.vue";
|
||||
import Totp from "@/components/setupComponent/totp.vue";
|
||||
import Share from "@/views/share.vue";
|
||||
|
||||
const checkAuth = async () => {
|
||||
let result = false
|
||||
@@ -30,6 +28,7 @@ const checkAuth = async () => {
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
|
||||
{
|
||||
name: "Index",
|
||||
path: '/',
|
||||
@@ -90,7 +89,6 @@ const router = createRouter({
|
||||
path: 'create',
|
||||
component: PeerCreate
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
@@ -105,8 +103,22 @@ const router = createRouter({
|
||||
{
|
||||
path: '/welcome', component: Setup,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
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"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -126,18 +138,28 @@ router.beforeEach(async (to, from, next) => {
|
||||
}
|
||||
|
||||
if (to.meta.requiresAuth){
|
||||
if (cookie.getCookie("authToken") && await checkAuth()){
|
||||
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();
|
||||
}
|
||||
dashboardConfigurationStore.Redirect = undefined;
|
||||
next()
|
||||
}else{
|
||||
dashboardConfigurationStore.Redirect = to;
|
||||
next("/signin")
|
||||
}
|
||||
}else {
|
||||
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,9 +10,53 @@ export const DashboardConfigurationStore = defineStore('DashboardConfigurationSt
|
||||
Peers: {
|
||||
Selecting: false,
|
||||
RefreshInterval: undefined
|
||||
}
|
||||
},
|
||||
CrossServerConfiguration:{
|
||||
Enable: false,
|
||||
ServerList: {}
|
||||
},
|
||||
ActiveServerConfiguration: undefined,
|
||||
IsElectronApp: false
|
||||
}),
|
||||
actions: {
|
||||
initCrossServerConfiguration(){
|
||||
const currentConfiguration = localStorage.getItem('CrossServerConfiguration');
|
||||
if (localStorage.getItem("ActiveCrossServerConfiguration") !== null){
|
||||
this.ActiveServerConfiguration = localStorage.getItem("ActiveCrossServerConfiguration");
|
||||
}
|
||||
if (currentConfiguration === null){
|
||||
localStorage.setItem('CrossServerConfiguration', JSON.stringify(this.CrossServerConfiguration))
|
||||
}else{
|
||||
this.CrossServerConfiguration = JSON.parse(currentConfiguration)
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
syncCrossServerConfiguration(){
|
||||
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
|
||||
@@ -27,6 +71,7 @@ export const DashboardConfigurationStore = defineStore('DashboardConfigurationSt
|
||||
},
|
||||
async signOut(){
|
||||
await fetchGet("/api/signout", {}, (res) => {
|
||||
this.removeActiveCrossServer();
|
||||
this.$router.go('/signin')
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ export const WireguardConfigurationsStore = defineStore('WireguardConfigurations
|
||||
state: () => ({
|
||||
Configurations: undefined,
|
||||
searchString: "",
|
||||
ConfigurationListInterval: undefined,
|
||||
PeerScheduleJobs: {
|
||||
dropdowns: {
|
||||
Field: [
|
||||
|
||||
@@ -1,31 +1,71 @@
|
||||
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}`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
export const fetchGet = async (url, params=undefined, callback=undefined) => {
|
||||
const urlSearchParams = new URLSearchParams(params);
|
||||
await fetch(`${url}?${urlSearchParams.toString()}`, {
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
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'})
|
||||
})
|
||||
.then(x => x.json())
|
||||
.then(x => callback ? callback(x) : undefined)
|
||||
.catch(x => {
|
||||
// let router = useRouter()
|
||||
// if (x.status === 401){
|
||||
// router.push('/signin')
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchPost = async (url, body, callback) => {
|
||||
await fetch(`${url}`, {
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
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'})
|
||||
})
|
||||
.then(x => x.json())
|
||||
.then(x => callback ? callback(x) : undefined)
|
||||
// .catch(() => {
|
||||
// alert("Error occurred! Check console")
|
||||
// });
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
this.errorMessage = res.message;
|
||||
document.querySelector(`#${res.data}`).classList.remove("is-valid")
|
||||
document.querySelector(`#${res.data}`).classList.add("is-invalid")
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -126,15 +126,14 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="mt-5">
|
||||
<div class="container mb-4">
|
||||
<div class="mb-4 d-flex align-items-center gap-4">
|
||||
<RouterLink to="/">
|
||||
<RouterLink to="/" class="text-decoration-none">
|
||||
<h3 class="mb-0 text-body">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
<i class="bi bi-chevron-left me-4"></i> New Configuration
|
||||
</h3>
|
||||
</RouterLink>
|
||||
<h3 class="text-body mb-0">New Configuration</h3>
|
||||
</div>
|
||||
|
||||
<form class="text-body d-flex flex-column gap-3"
|
||||
|
||||
@@ -11,11 +11,13 @@ 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,
|
||||
@@ -49,7 +51,7 @@ export default {
|
||||
<PeersDefaultSettingsInput targetData="peer_mtu" title="MTU (Max Transmission Unit)"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
|
||||
<PeersDefaultSettingsInput targetData="remote_endpoint" title="Peer Remote Endpoint"
|
||||
:warning="true" warningText="This will be change globally, and will be apply to all peer's QR code and configuration file."
|
||||
:warning="true" warningText="This will be changed globally, and will be apply to all peer's QR code and configuration file."
|
||||
></PeersDefaultSettingsInput>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,14 +69,16 @@ export default {
|
||||
</div>
|
||||
<div class="card mb-4 shadow rounded-3">
|
||||
<p class="card-header">Account Settings</p>
|
||||
<div class="card-body">
|
||||
<div class="card-body d-flex gap-4 flex-column">
|
||||
<AccountSettingsInputUsername targetData="username"
|
||||
title="Username"
|
||||
></AccountSettingsInputUsername>
|
||||
<hr>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import QRCode from 'qrcode'
|
||||
import Totp from "@/components/setupComponent/totp.vue";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
export default {
|
||||
name: "setup",
|
||||
components: {Totp},
|
||||
components: {},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
return {store}
|
||||
@@ -16,8 +14,7 @@ export default {
|
||||
username: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: "",
|
||||
enable_totp: false,
|
||||
verified_totp: false
|
||||
enable_totp: true
|
||||
},
|
||||
loading: false,
|
||||
errorMessage: "",
|
||||
@@ -30,7 +27,6 @@ export default {
|
||||
&& this.setup.newPassword.length >= 8
|
||||
&& this.setup.repeatNewPassword.length >= 8
|
||||
&& this.setup.newPassword === this.setup.repeatNewPassword
|
||||
&& ((this.setup.enable_totp && this.setup.verified_totp) || !this.setup.enable_totp)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -39,9 +35,7 @@ export default {
|
||||
fetchPost("/api/Welcome_Finish", this.setup, (res) => {
|
||||
if (res.status){
|
||||
this.done = true;
|
||||
setTimeout(() => {
|
||||
this.$router.push('/')
|
||||
}, 500)
|
||||
this.$router.push('/2FASetup')
|
||||
}else{
|
||||
document.querySelectorAll("#createAccount input").forEach(x => x.classList.add("is-invalid"))
|
||||
this.errorMessage = res.message;
|
||||
@@ -62,7 +56,7 @@ export default {
|
||||
<template>
|
||||
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
|
||||
:data-bs-theme="this.store.Configuration.Server.dashboard_theme">
|
||||
<div class="mx-auto text-body" style="width: 500px">
|
||||
<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>
|
||||
@@ -94,26 +88,23 @@ export default {
|
||||
class="form-control" id="confirmPassword" name="confirmPassword" placeholder="and you can remember it :)" required>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="enable_totp"
|
||||
v-model="this.setup.enable_totp">
|
||||
<label class="form-check-label"
|
||||
for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label>
|
||||
</div>
|
||||
<Suspense>
|
||||
<Transition name="fade">
|
||||
<Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp>
|
||||
</Transition>
|
||||
</Suspense>
|
||||
<!-- <div class="form-check form-switch">-->
|
||||
<!-- <input class="form-check-input" type="checkbox" role="switch" id="enable_totp" -->
|
||||
<!-- v-model="this.setup.enable_totp">-->
|
||||
<!-- <label class="form-check-label" -->
|
||||
<!-- for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label>-->
|
||||
<!-- </div>-->
|
||||
<!-- <Suspense>-->
|
||||
<!-- <Transition name="fade">-->
|
||||
<!-- <Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp>-->
|
||||
<!-- </Transition>-->
|
||||
<!-- </Suspense>-->
|
||||
|
||||
<button class="btn btn-dark btn-lg mb-5 d-flex btn-brand shadow align-items-center"
|
||||
ref="signInBtn"
|
||||
:disabled="!this.goodToSubmit || this.loading || this.done" @click="this.submit()">
|
||||
<span class="d-flex align-items-center w-100" v-if="!this.loading && !this.done">
|
||||
Finish<i class="bi bi-chevron-right ms-auto"></i></span>
|
||||
<span class="d-flex align-items-center w-100" v-else-if="this.done">
|
||||
Welcome to WGDashboard!</span>
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,19 +1,25 @@
|
||||
<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 = ""
|
||||
let theme = "dark"
|
||||
let totpEnabled = false;
|
||||
await fetchGet("/api/getDashboardTheme", {}, (res) => {
|
||||
theme = res.data
|
||||
});
|
||||
await fetchGet("/api/isTotpEnabled", {}, (res) => {
|
||||
totpEnabled = res.data
|
||||
});
|
||||
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(){
|
||||
@@ -26,6 +32,11 @@ export default {
|
||||
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)){
|
||||
@@ -75,15 +86,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid login-container-fluid d-flex main flex-column" :data-bs-theme="this.theme">
|
||||
<div class="login-box m-auto" style="width: 500px;">
|
||||
<h4 class="mb-0 text-body">Welcome to</h4>
|
||||
<span class="dashboardLogo display-3">WGDashboard</span>
|
||||
<div class="m-auto">
|
||||
<div class="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();}">
|
||||
<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>
|
||||
@@ -107,27 +121,51 @@ export default {
|
||||
v-model="this.totp"
|
||||
>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand shadow signInBtn" ref="signInBtn">
|
||||
<span v-if="!this.loading" class="d-flex w-100">
|
||||
Sign In<i class="ms-auto bi bi-chevron-right"></i>
|
||||
</span>
|
||||
<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>
|
||||
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">
|
||||
<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>
|
||||
@@ -5,27 +5,58 @@ import {proxy} from "./proxy.js";
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "/static/app/dist",
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
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]`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
server:{
|
||||
proxy: {
|
||||
'/api': proxy
|
||||
}
|
||||
},
|
||||
build: {
|
||||
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]`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
*{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
|
||||
.dp__input{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -138,8 +146,7 @@
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
padding-left: 30px;
|
||||
background-color: #dfdfdf;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .feather {
|
||||
@@ -149,6 +156,7 @@
|
||||
|
||||
.sidebar .nav-link.active, .bottomNavContainer .nav-link.active {
|
||||
color: #007bff;
|
||||
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .feather,
|
||||
@@ -1120,6 +1128,7 @@ pre.index-alert {
|
||||
background-color: #00000060;
|
||||
z-index: 9999;
|
||||
backdrop-filter: blur(1px);
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.dashboardModal{
|
||||
@@ -1153,4 +1162,6 @@ pre.index-alert {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.messageCentre{
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -1,14 +1,18 @@
|
||||
[Unit]
|
||||
After=syslog.target network-online.target
|
||||
Wants=wg-quick.target
|
||||
ConditionPathIsDirectory=/etc/wireguard
|
||||
|
||||
[Service]
|
||||
Environment="VIRTUAL_ENV={{VIRTUAL_ENV}}"
|
||||
WorkingDirectory={{APP_ROOT}}
|
||||
ExecStart={{VIRTUAL_ENV}}/bin/python3 {{APP_ROOT}}/dashboard.py
|
||||
Type=forking
|
||||
PIDFile=/opt/wgdashboard/src/gunicorn.pid
|
||||
WorkingDirectory=/opt/wgdashboard/src
|
||||
ExecStart=/opt/wgdashboard/src/wgd.sh start
|
||||
ExecStop=/opt/wgdashboard/src/wgd.sh stop
|
||||
ExecReload=/opt/wgdashboard/src/wgd.sh restart
|
||||
TimeoutSec=120
|
||||
PrivateTmp=yes
|
||||
Restart=always
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+22
-10
@@ -165,6 +165,7 @@ _checkWireguard(){
|
||||
install_wgd(){
|
||||
printf "[WGDashboard] Starting to install WGDashboard\n"
|
||||
_checkWireguard
|
||||
sudo chmod -R 755 /etc/wireguard/
|
||||
|
||||
if [ ! -d "log" ]
|
||||
then
|
||||
@@ -179,16 +180,19 @@ install_wgd(){
|
||||
else
|
||||
printf "[WGDashboard] \xE2\x9C\x94 Python is installed\n"
|
||||
fi
|
||||
|
||||
version_pass=$(python3 -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 10) else print("0");')
|
||||
if [ $version_pass == "0" ]
|
||||
then
|
||||
printf "[WGDashboard] WGDashboard required Python 3.10 or above\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_installPythonVenv
|
||||
_installPythonPip
|
||||
|
||||
|
||||
version_pass=$(python3 -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 7) else print("0");')
|
||||
if [ $version_pass == "0" ]
|
||||
then
|
||||
printf "[WGDashboard] WGDashboard required Python 3.7 or above\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [ ! -d "db" ]
|
||||
then
|
||||
@@ -236,10 +240,18 @@ gunicorn_start () {
|
||||
export PATH=$PATH:/usr/local/bin:$HOME/.local/bin
|
||||
fi
|
||||
_check_and_set_venv
|
||||
sudo "$venv_gunicorn" --access-logfile log/access_"$d".log \
|
||||
--log-level 'debug' --capture-output \
|
||||
--error-logfile log/error_"$d".log 'dashboard:app'
|
||||
printf "[WGDashboard] Log files is under ./log\n"
|
||||
sudo "$venv_gunicorn" --config ./gunicorn.conf.py
|
||||
sleep 5
|
||||
checkPIDExist=0
|
||||
while [ $checkPIDExist -eq 0 ]
|
||||
do
|
||||
if test -f './gunicorn.pid'; then
|
||||
checkPIDExist=1
|
||||
printf "[WGDashboard] Checking if WGDashboard w/ Gunicorn started successfully\n"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
printf "[WGDashboard] WGDashboard w/ Gunicorn started successfully\n"
|
||||
printf "%s\n" "$dashes"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user