Compare commits

..

46 Commits

Author SHA1 Message Date
Donald Zou 410b81f46f Some UI adjustment 2024-08-09 22:16:38 -04:00
Donald Zou aa3711c5cc Adjusted search peer UI 2024-08-09 21:30:01 -04:00
Donald Zou d6b1f97a04 Update 2024-08-09 20:46:11 -04:00
Donald Zou b4e8e57a22 Updated UI for nav bar 2024-08-09 17:58:41 -04:00
Donald Zou 9644e6195c Finished adding data usage on configuration list 2024-08-09 17:30:44 -04:00
Donald Zou 764e0c7607 Replaced all cursor without recursive use 2024-08-09 17:29:57 -04:00
Donald Zou 97d640dd40 Update dashboard.py 2024-08-09 16:09:55 -04:00
Donald Zou d2915b5b05 Update dashboard.py 2024-08-09 16:09:23 -04:00
Donald Zou f274f6fd18 Fixed some bugs.. 2024-08-09 16:09:08 -04:00
Donald Zou f507ac2569 Rewrote the add peer function 2024-08-08 23:27:13 -04:00
Donald Zou 208cbd6d89 Peer data reset function is don e 2024-08-08 20:56:14 -04:00
Donald Zou fa2d7fa3da Peer Sharing is done :) 2024-08-07 00:37:05 -04:00
Donald Zou 7463767781 Sharing peer is done, fixed #294
Fixed issue #294 with a Vue.js plugin instead of using the `datatime-local` input tag.

Still need to work on the end-user UI for sharing.
2024-08-06 19:15:00 -04:00
Donald Zou 958bc864c9 The sharing function is almost done 2024-08-06 10:17:14 -04:00
Donald Zou 4484668750 Backend for peer sharing is done 2024-08-05 19:57:17 -04:00
Donald Zou d5dea4b87f Fixed the issue mentioned in #290 2024-08-05 17:55:58 -04:00
Donald Zou 0fdef6a0a2 fixing some Gunicorn bugs.. again.. 2024-08-05 15:39:11 -04:00
Donald Zou bd71b6bad8 Update gunicorn.conf.py 2024-08-04 20:24:31 -04:00
Donald Zou 9b7887b279 Update gunicorn.conf.py 2024-08-04 20:23:41 -04:00
Donald Zou 3960e43872 Update gunicorn.conf.py 2024-08-04 20:22:41 -04:00
Donald Zou 201c8f9ec9 Update dashboard.py 2024-08-04 20:17:29 -04:00
Donald Zou 8c8374a08c Update wgd.sh 2024-08-04 20:16:26 -04:00
Donald Zou 467595afc9 Update wgd.sh 2024-08-04 20:15:46 -04:00
Donald Zou acb54f098c Update gunicorn.conf.py 2024-08-04 20:13:17 -04:00
Donald Zou 5755d13460 Update gunicorn.conf.py 2024-08-04 19:45:28 -04:00
Donald Zou 2c3500315d Update gunicorn.conf.py 2024-08-04 19:36:49 -04:00
Donald Zou 47ea60c0cd Testing v4 2024-08-04 19:35:59 -04:00
Donald Zou 18b18c1396 Update wgd.sh 2024-08-04 19:33:05 -04:00
Donald Zou ff227de5fa Update gunicorn.conf.py 2024-08-04 19:32:16 -04:00
Donald Zou 6799692811 Testing.. 2024-08-04 19:30:48 -04:00
Donald Zou c6173f7f6f Update wgd.sh 2024-08-04 19:22:26 -04:00
Donald Zou d0e4dabc44 Update wgd.sh 2024-08-04 19:22:06 -04:00
Donald Zou f815dae300 Update wgd.sh 2024-08-04 19:21:38 -04:00
Donald Zou b3bd6bb39e Update wgd.sh 2024-08-04 19:20:31 -04:00
Donald Zou 71df6409c2 Update gunicorn.conf.py 2024-08-04 19:17:41 -04:00
Donald Zou e4f9a1e0cf Update gunicorn.conf.py 2024-08-04 19:03:30 -04:00
Donald Zou ca6a05e393 Update gunicorn.conf.py 2024-08-04 18:59:45 -04:00
Donald Zou c0d26164dc Update wgd.sh 2024-08-04 18:48:13 -04:00
Donald Zou 76fe2a1ba9 Update wgd.sh 2024-08-04 18:45:57 -04:00
Donald Zou 8cbdb54402 Still fixing Gunicron 2024-08-04 16:08:05 -04:00
Donald Zou 764ef80a62 Update wgd.sh 2024-08-04 15:44:55 -04:00
Donald Zou 0c37d93c01 Testing if this will fix bash exit before Gunicorn ran... 2024-08-04 15:29:07 -04:00
Donald Zou c57a5128e5 Update wgd.sh 2024-08-04 03:20:44 -04:00
Donald Zou 6cf4eba20a Redirect when received a 401 error 2024-08-04 01:31:31 -04:00
Donald Zou 6825d728c2 Log function is completed 2024-08-03 17:03:39 -04:00
Donald Zou 6d3091b2a2 Push dockerfiles 2024-08-03 15:29:56 -04:00
41 changed files with 1564 additions and 411 deletions
+1 -3
View File
@@ -106,9 +106,7 @@
# Must have for each peer # Must have for each peer
``` ```
- Python 3.7+ & Pip3 - **Python 3.10** for v4.0+, **Python 3.7 - 3.9** for v2.0 - v3.0.6.2
- Browser support CSS3 and ES6
## 🛠 Install ## 🛠 Install
1. Download WGDashboard 1. Download WGDashboard
+82
View File
@@ -0,0 +1,82 @@
# WG-Dashboard Docker Explanation:
Author: DaanSelen<br>
This document delves into how the WG-Dashboard Docker container has been built.<br>
Of course there are two stages, one before run-time and one at/after run-time.<br>
The `Dockerfile` describes how the container image is made, and the `entrypoint.sh` is executed after running the container. <br>
In this example, WireGuard is integrated into the container itself, so it should be a run-and-go.<br>
For more details on the source-code specific to this Docker image, refer to the source files, they have lots of comments.
I have tried to embed some new features such as `isolated_peers` and interface startup on container-start (through `enable_wg0`).
<img src="https://raw.githubusercontent.com/donaldzou/WGDashboard/main/img/logo.png" alt="WG-Dashboard Logo" title="WG-Dashboard Logo" width="150" height="150" />
## Getting the container running:
To get the container running you either pull the image from the repository, at the moment: `repo.nerthus.nl/app/wireguard-dashboard:latest`.<br>
From there either use the environment variables describe below as parameters or use the Docker Compose file: `compose.yaml`.
An example of a simple command to get the container running is show below:<br>
```shell
docker run -d \
--name wireguard-dashboard \
--restart unless-stopped \
-e enable_wg0=true \
-e isolated_peers=true \
-p 10086:10086/tcp \
-p 51820:51820/udp \
--cap-add NET_ADMIN \
repo.nerthus.nl/app/wireguard-dashboard:latest
```
<br>
If you want to use Compose instead of a raw Docker command, refer to the example in the `compose.yaml` or the one pasted below:
<br><br>
```yaml
services:
wireguard-dashboard:
image: repo.nerthus.nl/app/wireguard-dashboard:latest
restart: unless-stopped
container_name: wire-dash
environment:
#- tz=
#- global_dns=
- enable_wg0=true
- isolated_peers=false
#- public_ip=
ports:
- 10086:10086/tcp
- 51820:51820/udp
volumes:
- conf:/etc/wireguard
- app:/opt/wireguarddashboard/app
cap_add:
- NET_ADMIN
volumes:
conf:
app:
```
If you want to customize the yaml, make sure the core stays the same, but for example volume PATHs can be freely changed.<br>
This setup is just generic and will use the Docker volumes.
## Working with the container and environment variables:
Once the container is running, the installation process is essentially the same as running it on bare-metal.<br>
So go to the assign TCP port in this case HTTP, like the default 10086 one in the example and log into the WEB-GUI.<br>
| Environment variable | Accepted arguments | Default value | Verbose |
| -------------- | ------- | ------- | ------- |
| tz | Europe/Amsterdam or any confirming timezone notation. | Europe/Amsterdam | Sets the timezone of the Docker container. This is to timesync the container to any other processes which would need it. |
| global_dns | Any IPv4 address, such as my personal recommendation: 9.9.9.9 (QUAD9) | 1.1.1.1 | Set the default DNS given to clients once they connect to the WireGuard tunnel (VPN).
| enable_wg0 | `true` or `false` | `false` | Enables or disables the starting of the WireGuard interface on container 'boot-up'.
| isolated_peers | `true` or `false` | `true` | For security the default is true, and it disables peers to ping or reach eachother, the WireGuard interface IS able to reach the peers (Done through `iptables`).
| public_ip | Any IPv4 (public recommended) address, such as the one returned by default | Default uses the return of `curl ifconfig.me` | To reach your VPN from outside your own network, you need WG-Dashboard to know what your public IP-address is, otherwise it will generate faulty config files for clients.
## Closing remarks:
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.
+77
View File
@@ -0,0 +1,77 @@
# Pull from small Debian stable image.
FROM debian:stable-slim
LABEL maintainer="dselen@nerthus.nl"
# Declaring environment variables, change Peernet to an address you like, standard is a 24 bit subnet.
ENV wg_net="10.0.0.1"
# wg_net is used functionally as an ARG for its environment variable nature, do not change unless you know what you are doing.
# Following ENV variables are changable on container runtime because /entrypoint.sh handles that. See compose.yaml for more info.
ENV tz="Europe/Amsterdam"
ENV global_dns="1.1.1.1"
ENV enable_wg0="false"
ENV isolated_peers="true"
ENV public_ip="0.0.0.0"
# Doing basic system maintenance. Change the timezone to the desired timezone.
RUN ln -sf /usr/share/zoneinfo/${tz} /etc/localtime
# Doing package management operations, such as upgrading
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y --no-install-recommends curl \
git \
iproute2 \
iptables \
iputils-ping \
openresolv \
procps \
python3 \
python3-pip \
python3-venv \
traceroute \
wireguard \
wireguard-tools \
&& apt-get remove linux-image-* --autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Removing the Linux Image package to preserve space on the image, for this reason also deleting apt lists, to be able to install packages: run apt update.
# Using WGDASH -- like wg_net functionally as a ARG command. But it is needed in entrypoint.sh so it needs to be exported as environment variable.
ENV WGDASH=/opt/wireguarddashboard
RUN python3 -m venv ${WGDASH}/venv
# Doing WireGuard Dashboard installation measures. Modify the git clone command to get the preferred version, with a specific branch for example.
RUN . ${WGDASH}/venv/bin/activate \
&& git clone https://github.com/donaldzou/WGDashboard.git ${WGDASH}/app \
&& pip3 install -r ${WGDASH}/app/src/requirements.txt \
&& chmod +x ${WGDASH}/app/src/wgd.sh \
&& .${WGDASH}/app/src/wgd.sh install
# Set the volume to be used for persistency.
VOLUME /etc/wireguard
# Generate basic WireGuard interface. Echoing the WireGuard interface config for readability, adjust if you want it for efficiency.
# Also setting the pipefail option, verbose: https://github.com/hadolint/hadolint/wiki/DL4006.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN wg genkey | tee /etc/wireguard/wg0_privatekey \
&& echo "[Interface]" > /wg0.conf \
&& echo "SaveConfig = true" >> /wg0.conf \
&& echo "Address = ${wg_net}/24" >> /wg0.conf \
&& echo "PrivateKey = $(cat /etc/wireguard/wg0_privatekey)" >> /wg0.conf \
&& echo "PostUp = iptables -t nat -I POSTROUTING 1 -s ${wg_net}/24 -o $(ip -o -4 route show to default | awk '{print $NF}') -j MASQUERADE" >> /wg0.conf \
&& echo "PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP" >> /wg0.conf \
&& echo "PreDown = iptables -t nat -D POSTROUTING 1" >> /wg0.conf \
&& echo "PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP" >> /wg0.conf \
&& echo "ListenPort = 51820" >> /wg0.conf \
#&& echo "DNS = ${global_dns}" >> /wg0.conf \
&& rm /etc/wireguard/wg0_privatekey
# Defining a way for Docker to check the health of the container. In this case: checking the login URL.
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD curl -f http://localhost:10086/signin || exit 1
# Copy the basic entrypoint.sh script.
COPY entrypoint.sh /entrypoint.sh
# Exposing the default WireGuard Dashboard port for web access.
EXPOSE 10086
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
+23
View File
@@ -0,0 +1,23 @@
services:
wireguard-dashboard:
image: repo.nerthus.nl/app/wireguard-dashboard:latest
restart: unless-stopped
container_name: wire-dash
environment:
#- tz= # <--- Set container timezone, default: Europe/Amsterdam.
#- global_dns= # <--- Set global DNS address, default: 1.1.1.1.
- enable_wg0=true # <--- If true, wg0 will be started on container startup. default: false.
- isolated_peers=false # <--- When set to true, it disallows peers to talk to eachother, setting to false, allows it, default: true.
#- public_ip= # <--- Set public IP to ensure the correct one is chosen, defaulting to the IP give by ifconfig.me.
ports:
- 10086:10086/tcp
- 51820:51820/udp
volumes:
- conf:/etc/wireguard
- app:/opt/wireguarddashboard/app
cap_add:
- NET_ADMIN
volumes:
conf:
app:
+109
View File
@@ -0,0 +1,109 @@
#!/bin/bash
echo "Starting the WireGuard Dashboard Docker container."
clean_up() {
# Cleaning out previous data such as the .pid file and starting the WireGuard Dashboard. Making sure to use the python venv.
echo "Looking for remains of previous instances..."
if [ -f "/opt/wireguarddashboard/app/src/gunicorn.pid" ]; then
echo "Found old .pid file, removing."
rm /opt/wireguarddashboard/app/src/gunicorn.pid
else
echo "No remains found, continuing."
fi
}
start_core() {
# This first step is to ensure the wg0.conf file exists, and if not, then its copied over from the ephemeral container storage.
if [ ! -f "/etc/wireguard/wg0.conf" ]; then
cp "/wg0.conf" "/etc/wireguard/wg0.conf"
echo "WireGuard interface file copied over."
else
echo "WireGuard interface file looks to already be existing."
fi
echo "Activating Python venv and executing the WireGuard Dashboard service."
. "${WGDASH}"/venv/bin/activate
cd "${WGDASH}"/app/src || return # If changing the directory fails (permission or presence error), then bash will exist this function, causing the WireGuard Dashboard to not be succesfully launched.
bash wgd.sh start
# The following section takes care of the firewall rules regarding the 'isolated_peers' feature, which allows or drops packets destined from the wg0 to the wg0 interface.
if [ "${isolated_peers,,}" = "false" ]; then
echo "Isolated peers disabled, adjusting."
sed -i '/PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP/d' /etc/wireguard/wg0.conf
sed -i '/PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP/d' /etc/wireguard/wg0.conf
elif [ "${isolated_peers,,}" = "true" ]; then
upblocking=$(grep -c "PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP" /etc/wireguard/wg0.conf)
downblocking=$(grep -c "PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP" /etc/wireguard/wg0.conf)
if [ "$upblocking" -lt 1 ] && [ "$downblocking" -lt 1 ]; then
echo "Isolated peers enabled, adjusting."
sed -i '/PostUp = iptables -t nat -I POSTROUTING 1 -s/a PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP' /etc/wireguard/wg0.conf
sed -i '/PreDown = iptables -t nat -D POSTROUTING 1 -s/a PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP' /etc/wireguard/wg0.conf
fi
fi
# The following section takes care of
if [ "${enable_wg0,,}" = "true" ]; then
echo "Preference for wg0 to be turned on found."
wg-quick up wg0
else
echo "Preference for wg0 to be turned off found."
fi
}
set_envvars() {
echo "Setting relevant variables for operation."
# If the timezone is different, for example in North-America or Asia.
if [ "${tz}" != "$(cat /etc/timezone)" ]; then
echo "Changing timezone."
ln -sf /usr/share/zoneinfo/"${tz}" /etc/localtime
echo "${tz}" > /etc/timezone
fi
# Changing the DNS used for clients and the dashboard itself.
if [ "${global_dns}" != "$(grep "peer_global_dns = " /opt/wireguarddashboard/app/src/wg-dashboard.ini | awk '{print $NF}')" ]; then
echo "Changing default dns."
#sed -i "s/^DNS = .*/DNS = ${global_dns}/" /etc/wireguard/wg0.conf # Uncomment if you want to have DNS on server-level.
sed -i "s/^peer_global_dns = .*/peer_global_dns = ${global_dns}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
fi
# Setting the public IP of the WireGuard Dashboard container host. If not defined, it will trying fetching it using a curl to ifconfig.me.
if [ "${public_ip}" = "0.0.0.0" ]; then
default_ip=$(curl -s ifconfig.me)
echo "Trying to fetch the Public-IP using ifconfig.me: ${default_ip}"
sed -i "s/^remote_endpoint = .*/remote_endpoint = ${default_ip}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
elif [ "${public_ip}" != "$(grep "remote_endpoint = " /opt/wireguarddashboard/app/src/wg-dashboard.ini | awk '{print $NF}')" ]; then
echo "Setting the Public-IP using given variable: ${public_ip}"
sed -i "s/^remote_endpoint = .*/remote_endpoint = ${public_ip}/" /opt/wireguarddashboard/app/src/wg-dashboard.ini
fi
}
ensure_blocking() {
sleep 1s
echo "Ensuring container continuation."
# This function checks if the latest error log is created and tails it for docker logs uses.
if find "/opt/wireguarddashboard/app/src/log" -mindepth 1 -maxdepth 1 -type f | read -r; then
latestErrLog=$(find /opt/wireguarddashboard/app/src/log -name "error_*.log" | head -n 1)
latestAccLog=$(find /opt/wireguarddashboard/app/src/log -name "access_*.log" | head -n 1)
tail -f "${latestErrLog}" "${latestAccLog}"
fi
# Blocking command in case of erroring. So the container does not quit.
sleep infinity
}
# Execute functions for the WireGuard Dashboard services, then set the environment variables
clean_up
start_core
set_envvars
ensure_blocking
+8
View File
@@ -0,0 +1,8 @@
# API Documents for WGDashboard
**Version: v4.0**
**Created by: Donald Zou**
<hr>
View File
+328 -104
View File
@@ -77,7 +77,12 @@ class CustomJsonEncoder(DefaultJSONProvider):
super().__init__(app) super().__init__(app)
def default(self, o): 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 o.toJson()
return super().default(self, o) return super().default(self, o)
@@ -104,7 +109,36 @@ class Log:
def __dict__(self): def __dict__(self):
return self.toJson() 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): def __init__(self):
self.loggerdb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard_log.db'), self.loggerdb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard_log.db'),
check_same_thread=False) check_same_thread=False)
@@ -266,7 +300,6 @@ class PeerJobs:
def runJob(self): def runJob(self):
needToDelete = [] needToDelete = []
for job in self.Jobs: for job in self.Jobs:
print(job.toJson())
c = WireguardConfigurations.get(job.Configuration) c = WireguardConfigurations.get(job.Configuration)
if c is not None: if c is not None:
f, fp = c.searchPeer(job.Peer) f, fp = c.searchPeer(job.Peer)
@@ -277,11 +310,8 @@ class PeerJobs:
y: float = float(job.Value) y: float = float(job.Value)
else: else:
x: datetime = datetime.now() 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) runAction: bool = self.__runJob_Compare(x, y, job.Operator)
print("Running Job:" + str(runAction) + "\n")
if runAction: if runAction:
s = False s = False
if job.Action == "restrict": if job.Action == "restrict":
@@ -298,7 +328,6 @@ class PeerJobs:
JobLogger.log(job.JobID, s["status"], JobLogger.log(job.JobID, s["status"],
f"Peer {fp.id} from {c.Name} failed {job.Action}ed." 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: for j in needToDelete:
self.deleteJob(j) self.deleteJob(j)
@@ -312,6 +341,74 @@ class PeerJobs:
if operator == "lst": if operator == "lst":
return x < y 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 WireguardConfiguration:
class InvalidConfigurationFileException(Exception): class InvalidConfigurationFileException(Exception):
def __init__(self, m): def __init__(self, m):
@@ -391,10 +488,10 @@ class WireguardConfiguration:
self.getPeersList() self.getPeersList()
def __createDatabase(self): 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] existingTables = [t['name'] for t in existingTables]
if self.Name not 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, id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
@@ -410,7 +507,7 @@ class WireguardConfiguration:
sqldb.commit() sqldb.commit()
if f'{self.Name}_restrict_access' not in existingTables: 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, id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
@@ -425,7 +522,7 @@ class WireguardConfiguration:
) )
sqldb.commit() sqldb.commit()
if f'{self.Name}_transfer' not in existingTables: 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, id VARCHAR NOT NULL, total_receive FLOAT NULL,
@@ -436,7 +533,7 @@ class WireguardConfiguration:
) )
sqldb.commit() sqldb.commit()
if f'{self.Name}_deleted' not in existingTables: 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, id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
@@ -451,6 +548,8 @@ class WireguardConfiguration:
) )
sqldb.commit() sqldb.commit()
def __getPublicKey(self) -> str: def __getPublicKey(self) -> str:
return _generatePublicKey(self.PrivateKey)[1] return _generatePublicKey(self.PrivateKey)[1]
@@ -460,7 +559,7 @@ class WireguardConfiguration:
def __getRestrictedPeers(self): def __getRestrictedPeers(self):
self.RestrictedPeers = [] 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: for i in restricted:
self.RestrictedPeers.append(Peer(i, self)) self.RestrictedPeers.append(Peer(i, self))
@@ -485,7 +584,7 @@ class WireguardConfiguration:
p[pCounter][split[0]] = split[1] p[pCounter][split[0]] = split[1]
for i in p: for i in p:
if "PublicKey" in i.keys(): 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() ((i['PublicKey']),)).fetchone()
if checkIfExist is None: if checkIfExist is None:
newPeer = { newPeer = {
@@ -511,7 +610,7 @@ class WireguardConfiguration:
"remote_endpoint": DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], "remote_endpoint": DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else "" "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, VALUES (:id, :private_key, :DNS, :endpoint_allowed_ip, :name, :total_receive, :total_sent,
@@ -522,13 +621,21 @@ class WireguardConfiguration:
sqldb.commit() sqldb.commit()
self.Peers.append(Peer(newPeer, self)) self.Peers.append(Peer(newPeer, self))
else: 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'],)) (i.get("AllowedIPs", "N/A"), i['PublicKey'],))
sqldb.commit() sqldb.commit()
self.Peers.append(Peer(checkIfExist, self)) self.Peers.append(Peer(checkIfExist, self))
except ValueError: except ValueError:
pass 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): def searchPeer(self, publicKey):
for i in self.Peers: for i in self.Peers:
if i.id == publicKey: if i.id == publicKey:
@@ -538,12 +645,15 @@ class WireguardConfiguration:
def allowAccessPeers(self, listOfPublicKeys): def allowAccessPeers(self, listOfPublicKeys):
# numOfAllowedPeers = 0 # numOfAllowedPeers = 0
# numOfFailedToAllowPeers = 0 # numOfFailedToAllowPeers = 0
if not self.getStatus():
self.toggleConfiguration()
for i in listOfPublicKeys: 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: 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'],)) % (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'],)) % self.Name, (p['id'],))
subprocess.check_output(f"wg set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip']}", subprocess.check_output(f"wg set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip']}",
shell=True, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
@@ -558,17 +668,19 @@ class WireguardConfiguration:
def restrictPeers(self, listOfPublicKeys): def restrictPeers(self, listOfPublicKeys):
numOfRestrictedPeers = 0 numOfRestrictedPeers = 0
numOfFailedToRestrictPeers = 0 numOfFailedToRestrictPeers = 0
if not self.getStatus():
self.toggleConfiguration()
for p in listOfPublicKeys: for p in listOfPublicKeys:
found, pf = self.searchPeer(p) found, pf = self.searchPeer(p)
if found: if found:
try: try:
subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove", subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove",
shell=True, stderr=subprocess.STDOUT) 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,)) (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,)) (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 numOfRestrictedPeers += 1
except Exception as e: except Exception as e:
numOfFailedToRestrictPeers += 1 numOfFailedToRestrictPeers += 1
@@ -587,13 +699,15 @@ class WireguardConfiguration:
def deletePeers(self, listOfPublicKeys): def deletePeers(self, listOfPublicKeys):
numOfDeletedPeers = 0 numOfDeletedPeers = 0
numOfFailedToDeletePeers = 0 numOfFailedToDeletePeers = 0
if not self.getStatus():
self.toggleConfiguration()
for p in listOfPublicKeys: for p in listOfPublicKeys:
found, pf = self.searchPeer(p) found, pf = self.searchPeer(p)
if found: if found:
try: try:
subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove", subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove",
shell=True, stderr=subprocess.STDOUT) 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 numOfDeletedPeers += 1
except Exception as e: except Exception as e:
numOfFailedToDeletePeers += 1 numOfFailedToDeletePeers += 1
@@ -632,6 +746,8 @@ class WireguardConfiguration:
return False, str(e) return False, str(e)
def getPeersLatestHandshake(self): def getPeersLatestHandshake(self):
if not self.getStatus():
self.toggleConfiguration()
try: try:
latestHandshake = subprocess.check_output(f"wg show {self.Name} latest-handshakes", latestHandshake = subprocess.check_output(f"wg show {self.Name} latest-handshakes",
shell=True, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
@@ -657,6 +773,8 @@ class WireguardConfiguration:
count += 2 count += 2
def getPeersTransfer(self): def getPeersTransfer(self):
if not self.getStatus():
self.toggleConfiguration()
try: try:
data_usage = subprocess.check_output(f"wg show {self.Name} transfer", data_usage = subprocess.check_output(f"wg show {self.Name} transfer",
shell=True, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
@@ -664,50 +782,40 @@ class WireguardConfiguration:
data_usage = [p.split("\t") for p in data_usage] data_usage = [p.split("\t") for p in data_usage]
for i in range(len(data_usage)): for i in range(len(data_usage)):
if len(data_usage[i]) == 3: if len(data_usage[i]) == 3:
cur_i = cursor.execute( cur_i = sqldb.cursor().execute(
"SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM %s WHERE id= ? " "SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM %s WHERE id= ? "
% self.Name, (data_usage[i][0],)).fetchone() % self.Name, (data_usage[i][0],)).fetchone()
if cur_i is not None: if cur_i is not None:
total_sent = cur_i['total_sent'] total_sent = cur_i['total_sent']
total_receive = cur_i['total_receive'] total_receive = cur_i['total_receive']
cur_total_sent = round(int(data_usage[i][2]) / (1024 ** 3), 4) cur_total_sent = float(data_usage[i][2]) / (1024 ** 3)
cur_total_receive = round(int(data_usage[i][1]) / (1024 ** 3), 4) cur_total_receive = float(data_usage[i][1]) / (1024 ** 3)
cumulative_receive = cur_i['cumu_receive'] + total_receive cumulative_receive = cur_i['cumu_receive'] + total_receive
cumulative_sent = cur_i['cumu_sent'] + total_sent cumulative_sent = cur_i['cumu_sent'] + total_sent
if total_sent <= cur_total_sent and total_receive <= cur_total_receive: if total_sent <= cur_total_sent and total_receive <= cur_total_receive:
total_sent = cur_total_sent total_sent = cur_total_sent
total_receive = cur_total_receive total_receive = cur_total_receive
else: else:
cursor.execute( sqldb.cursor().execute(
"UPDATE %s SET cumu_receive = ?, cumu_sent = ?, cumu_data = ? WHERE id = ?" % "UPDATE %s SET cumu_receive = ?, cumu_sent = ?, cumu_data = ? WHERE id = ?" %
self.Name, (round(cumulative_receive, 4), round(cumulative_sent, 4), self.Name, (cumulative_receive, cumulative_sent,
round(cumulative_sent + cumulative_receive, 4), cumulative_sent + cumulative_receive,
data_usage[i][0],)) data_usage[i][0],))
total_sent = 0 total_sent = 0
total_receive = 0 total_receive = 0
_, p = self.searchPeer(data_usage[i][0]) _, p = self.searchPeer(data_usage[i][0])
if p.total_receive != round(total_receive, 4) or p.total_sent != round(total_sent, 4): if p.total_receive != total_receive or p.total_sent != total_sent:
cursor.execute( sqldb.cursor().execute(
"UPDATE %s SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?" "UPDATE %s SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?"
% self.Name, (round(total_receive, 4), round(total_sent, 4), % self.Name, (total_receive, total_sent,
round(total_receive + total_sent, 4), data_usage[i][0],)) total_receive + total_sent, 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()
except Exception as e: except Exception as e:
print("Error" + str(e)) print("Error: " + str(e))
def getPeersEndpoint(self): def getPeersEndpoint(self):
if not self.getStatus():
self.toggleConfiguration()
try: try:
data_usage = subprocess.check_output(f"wg show {self.Name} endpoints", data_usage = subprocess.check_output(f"wg show {self.Name} endpoints",
shell=True, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
@@ -723,7 +831,6 @@ class WireguardConfiguration:
def toggleConfiguration(self) -> [bool, str]: def toggleConfiguration(self) -> [bool, str]:
self.getStatus() self.getStatus()
print("Status: ", self.getStatus())
if self.Status: if self.Status:
try: try:
check = subprocess.check_output(f"wg-quick down {self.Name}", check = subprocess.check_output(f"wg-quick down {self.Name}",
@@ -760,7 +867,13 @@ class WireguardConfiguration:
"PreDown": self.PreDown, "PreDown": self.PreDown,
"PostUp": self.PostUp, "PostUp": self.PostUp,
"PostDown": self.PostDown, "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: class Peer:
@@ -786,10 +899,13 @@ class Peer:
self.remote_endpoint = tableData["remote_endpoint"] self.remote_endpoint = tableData["remote_endpoint"]
self.preshared_key = tableData["preshared_key"] self.preshared_key = tableData["preshared_key"]
self.jobs: list[PeerJob] = [] self.jobs: list[PeerJob] = []
self.ShareLink: list[PeerShareLink] = []
self.getJobs() self.getJobs()
self.getShareLink()
def toJson(self): def toJson(self):
self.getJobs() self.getJobs()
self.getShareLink()
return self.__dict__ return self.__dict__
def __repr__(self): def __repr__(self):
@@ -799,6 +915,8 @@ class Peer:
preshared_key: str, preshared_key: str,
dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int, dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int,
keepalive: int) -> ResponseObject: keepalive: int) -> ResponseObject:
if not self.configuration.getStatus():
self.configuration.toggleConfiguration()
existingAllowedIps = [item for row in list( existingAllowedIps = [item for row in list(
map(lambda x: [q.strip() for q in x.split(',')], map(lambda x: [q.strip() for q in x.split(',')],
@@ -843,12 +961,13 @@ class Peer:
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'): if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
return ResponseObject(False, return ResponseObject(False,
"Update peer failed when saving the configuration.") "Update peer failed when saving the configuration.")
cursor.execute( sqldb.cursor().execute(
'''UPDATE %s SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?, '''UPDATE %s SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?,
keepalive = ?, preshared_key = ? WHERE id = ?''' % self.configuration.Name, keepalive = ?, preshared_key = ? WHERE id = ?''' % self.configuration.Name,
(name, private_key, dns_addresses, endpoint_allowed_ip, mtu, (name, private_key, dns_addresses, endpoint_allowed_ip, mtu,
keepalive, preshared_key, self.id,) keepalive, preshared_key, self.id,)
) )
sqldb.commit()
return ResponseObject() return ResponseObject()
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
return ResponseObject(False, exc.output.decode("UTF-8").strip()) return ResponseObject(False, exc.output.decode("UTF-8").strip())
@@ -888,7 +1007,24 @@ PersistentKeepalive = {str(self.keepalive)}
def getJobs(self): def getJobs(self):
self.jobs = AllPeerJobs.searchJob(self.configuration.Name, self.id) 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 # Regex Match
def regex_match(regex, text): def regex_match(regex, text):
@@ -922,6 +1058,7 @@ class DashboardConfig:
"username": "admin", "username": "admin",
"password": "admin", "password": "admin",
"enable_totp": "false", "enable_totp": "false",
"totp_verified": "false",
"totp_key": pyotp.random_base32() "totp_key": pyotp.random_base32()
}, },
"Server": { "Server": {
@@ -957,13 +1094,13 @@ class DashboardConfig:
self.DashboardAPIKeys = self.__getAPIKeys() self.DashboardAPIKeys = self.__getAPIKeys()
def __createAPIKeyTable(self): 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: 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() sqldb.commit()
def __getAPIKeys(self) -> list[DashboardAPIKey]: 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 = [] fKeys = []
for k in keys: for k in keys:
fKeys.append(DashboardAPIKey(*k)) fKeys.append(DashboardAPIKey(*k))
@@ -971,12 +1108,12 @@ class DashboardConfig:
def createAPIKeys(self, ExpiredAt = None): def createAPIKeys(self, ExpiredAt = None):
newKey = secrets.token_urlsafe(32) 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() sqldb.commit()
self.DashboardAPIKeys = self.__getAPIKeys() self.DashboardAPIKeys = self.__getAPIKeys()
def deleteAPIKey(self, key): 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() sqldb.commit()
self.DashboardAPIKeys = self.__getAPIKeys() self.DashboardAPIKeys = self.__getAPIKeys()
@@ -1208,6 +1345,13 @@ API Routes
@app.before_request @app.before_request
def auth_req(): def auth_req():
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] authenticationRequired = DashboardConfig.GetConfig("Server", "auth_req")[1]
d = request.headers d = request.headers
if authenticationRequired: if authenticationRequired:
@@ -1215,6 +1359,7 @@ def auth_req():
apiKeyEnabled = DashboardConfig.GetConfig("Server", "dashboard_api_key")[1] apiKeyEnabled = DashboardConfig.GetConfig("Server", "dashboard_api_key")[1]
if apiKey is not None and len(apiKey) > 0 and apiKeyEnabled: if apiKey is not None and len(apiKey) > 0 and apiKeyEnabled:
apiKeyExist = len(list(filter(lambda x : x.Key == apiKey, DashboardConfig.DashboardAPIKeys))) == 1 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: if not apiKeyExist:
response = Flask.make_response(app, { response = Flask.make_response(app, {
"status": False, "status": False,
@@ -1228,6 +1373,7 @@ def auth_req():
if ('/static/' not in request.path and "username" not in session and "/" != request.path 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 "validateAuthentication" not in request.path and "authenticate" not in request.path
and "getDashboardConfiguration" not in request.path and "getDashboardTheme" 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 and "isTotpEnabled" not in request.path
): ):
response = Flask.make_response(app, { response = Flask.make_response(app, {
@@ -1268,8 +1414,10 @@ def API_AuthenticateLogin():
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1]) resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
resp.set_cookie("authToken", authToken) resp.set_cookie("authToken", authToken)
session.permanent = True session.permanent = True
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login success: {data['username']}")
return resp return resp
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login failed: {data['username']}")
if totpEnabled: if totpEnabled:
return ResponseObject(False, "Sorry, your username, password or OTP is incorrect.") return ResponseObject(False, "Sorry, your username, password or OTP is incorrect.")
else: else:
@@ -1286,8 +1434,6 @@ def API_SignOut():
@app.route('/api/getWireguardConfigurations', methods=["GET"]) @app.route('/api/getWireguardConfigurations', methods=["GET"])
def API_getWireguardConfigurations(): def API_getWireguardConfigurations():
# WireguardConfigurations = _getConfigurationList() # WireguardConfigurations = _getConfigurationList()
print("in request::::")
print(list(WireguardConfigurations.keys()))
return ResponseObject(data=[wc for wc in WireguardConfigurations.values()]) return ResponseObject(data=[wc for wc in WireguardConfigurations.values()])
@@ -1390,7 +1536,7 @@ def API_newDashboardAPIKey():
if data['neverExpire']: if data['neverExpire']:
expiredAt = None expiredAt = None
else: 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) DashboardConfig.createAPIKeys(expiredAt)
return ResponseObject(True, data=DashboardConfig.DashboardAPIKeys) return ResponseObject(True, data=DashboardConfig.DashboardAPIKeys)
except Exception as e: except Exception as e:
@@ -1427,6 +1573,20 @@ def API_updatePeerSettings(configName):
allowed_ip, endpoint_allowed_ip, mtu, keepalive) allowed_ip, endpoint_allowed_ip, mtu, keepalive)
return ResponseObject(False, "Peer does not exist") 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']) @app.route('/api/deletePeers/<configName>', methods=['POST'])
def API_deletePeers(configName: str) -> ResponseObject: def API_deletePeers(configName: str) -> ResponseObject:
@@ -1452,6 +1612,61 @@ def API_restrictPeers(configName: str) -> ResponseObject:
return configuration.restrictPeers(peers) return configuration.restrictPeers(peers)
return ResponseObject(False, "Configuration does not exist") 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']) @app.route('/api/allowAccessPeers/<configName>', methods=['POST'])
def API_allowAccessPeers(configName: str) -> ResponseObject: def API_allowAccessPeers(configName: str) -> ResponseObject:
@@ -1483,8 +1698,7 @@ def API_addPeers(configName):
if (not bulkAdd and (len(public_key) == 0 or len(allowed_ips) == 0)) or len(endpoint_allowed_ip) == 0: 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.") return ResponseObject(False, "Please fill in all required box.")
if not config.getStatus(): if not config.getStatus():
return ResponseObject(False, config.toggleConfiguration()
f"{configName} is not running, please turn on the configuration before adding peers.")
if bulkAdd: if bulkAdd:
if bulkAddAmount < 1: if bulkAddAmount < 1:
return ResponseObject(False, "Please specify amount of peers you want to add") return ResponseObject(False, "Please specify amount of peers you want to add")
@@ -1497,28 +1711,24 @@ def API_addPeers(configName):
keyPairs = [] keyPairs = []
for i in range(bulkAddAmount): for i in range(bulkAddAmount):
key = _generatePrivateKey()[1] newPrivateKey = _generatePrivateKey()[1]
keyPairs.append([key, _generatePublicKey(key)[1], _generatePrivateKey()[1], availableIps[1][i], keyPairs.append({
f"{config.Name}_{datetime.now().strftime('%m%d%Y%H%M%S')}_Peer_#_{(i + 1)}"]) "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: if len(keyPairs) == 0:
return ResponseObject(False, "Generating key pairs by bulk failed") return ResponseObject(False, "Generating key pairs by bulk failed")
config.addPeers(keyPairs)
for i in range(bulkAddAmount): for kp in keyPairs:
subprocess.check_output( found, peer = config.searchPeer(kp['id'])
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])
if found: if found:
if not peer.updatePeer(keyPairs[i][4], keyPairs[i][0], preshared_key, dns_addresses, if not peer.updatePeer(kp['name'], kp['private_key'], kp['preshared_key'], dns_addresses,
keyPairs[i][3], kp['allowed_ip'], endpoint_allowed_ip, mtu, keep_alive):
endpoint_allowed_ip, mtu, keep_alive).status:
return ResponseObject(False, "Failed to add peers in bulk") return ResponseObject(False, "Failed to add peers in bulk")
return ResponseObject() return ResponseObject()
else: else:
@@ -1526,16 +1736,17 @@ def API_addPeers(configName):
return ResponseObject(False, f"This peer already exist.") return ResponseObject(False, f"This peer already exist.")
name = data['name'] name = data['name']
private_key = data['private_key'] private_key = data['private_key']
subprocess.check_output( config.addPeers([{"id": public_key, "allowed_ip": ''.join(allowed_ips)}])
f"wg set {config.Name} peer {public_key} allowed-ips {''.join(allowed_ips)}", # subprocess.check_output(
shell=True, stderr=subprocess.STDOUT) # f"wg set {config.Name} peer {public_key} allowed-ips {''.join(allowed_ips)}",
if len(preshared_key) > 0: # shell=True, stderr=subprocess.STDOUT)
subprocess.check_output( # if len(preshared_key) > 0:
f"wg set {config.Name} peer {public_key} preshared-key {preshared_key}", # subprocess.check_output(
shell=True, stderr=subprocess.STDOUT) # f"wg set {config.Name} peer {public_key} preshared-key {preshared_key}",
subprocess.check_output( # shell=True, stderr=subprocess.STDOUT)
f"wg-quick save {config.Name}", shell=True, stderr=subprocess.STDOUT) # subprocess.check_output(
config.getPeersList() # f"wg-quick save {config.Name}", shell=True, stderr=subprocess.STDOUT)
# config.getPeersList()
found, peer = config.searchPeer(public_key) found, peer = config.searchPeer(public_key)
if found: if found:
return peer.updatePeer(name, private_key, preshared_key, dns_addresses, ",".join(allowed_ips), return peer.updatePeer(name, private_key, preshared_key, dns_addresses, ",".join(allowed_ips),
@@ -1750,12 +1961,14 @@ Sign Up
@app.route('/api/isTotpEnabled') @app.route('/api/isTotpEnabled')
def 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') @app.route('/api/Welcome_GetTotpLink')
def 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( return ResponseObject(
data=pyotp.totp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).provisioning_uri( data=pyotp.totp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).provisioning_uri(
issuer_name="WGDashboard")) issuer_name="WGDashboard"))
@@ -1765,11 +1978,11 @@ def API_Welcome_GetTotpLink():
@app.route('/api/Welcome_VerifyTotpLink', methods=["POST"]) @app.route('/api/Welcome_VerifyTotpLink', methods=["POST"])
def API_Welcome_VerifyTotpLink(): def API_Welcome_VerifyTotpLink():
data = request.get_json() data = request.get_json()
if DashboardConfig.GetConfig("Other", "welcome_session")[1]:
totp = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() totp = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now()
print(totp) if totp == data['totp']:
DashboardConfig.SetConfig("Account", "totp_verified", "true")
DashboardConfig.SetConfig("Account", "enable_totp", "true")
return ResponseObject(totp == data['totp']) return ResponseObject(totp == data['totp'])
return ResponseObject(False)
@app.route('/api/Welcome_Finish', methods=["POST"]) @app.route('/api/Welcome_Finish', methods=["POST"])
@@ -1789,10 +2002,10 @@ def API_Welcome_Finish():
"repeatNewPassword": data["repeatNewPassword"], "repeatNewPassword": data["repeatNewPassword"],
"currentPassword": "admin" "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: if not updateUsername or not updatePassword:
return ResponseObject(False, f"{updateUsernameErr},{updatePasswordErr},{updateEnableTotpErr}".strip(",")) return ResponseObject(False, f"{updateUsernameErr},{updatePasswordErr}".strip(","))
DashboardConfig.SetConfig("Other", "welcome_session", False) DashboardConfig.SetConfig("Other", "welcome_session", False)
@@ -1810,8 +2023,8 @@ def index():
def backGroundThread(): def backGroundThread():
with app.app_context(): with app.app_context():
print("Waiting 5 sec") print(f"[WGDashboard] Background Thread #1 Started", flush=True)
time.sleep(5) time.sleep(10)
while True: while True:
for c in WireguardConfigurations.values(): for c in WireguardConfigurations.values():
if c.getStatus(): if c.getStatus():
@@ -1819,19 +2032,19 @@ def backGroundThread():
c.getPeersTransfer() c.getPeersTransfer()
c.getPeersLatestHandshake() c.getPeersLatestHandshake()
c.getPeersEndpoint() c.getPeersEndpoint()
c.getPeersList()
except Exception as e: except Exception as e:
print("Error: " + str(e)) print(f"[WGDashboard] Background Thread #1 Error: {str(e)}", flush=True)
time.sleep(10) time.sleep(10)
def peerJobScheduleBackgroundThread(): def peerJobScheduleBackgroundThread():
with app.app_context(): 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) time.sleep(10)
while True: while True:
print(f'''[{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Peer Job Schedule: Running''')
AllPeerJobs.runJob() AllPeerJobs.runJob()
time.sleep(10) time.sleep(180)
def gunicornConfig(): def gunicornConfig():
@@ -1839,21 +2052,30 @@ def gunicornConfig():
_, app_port = DashboardConfig.GetConfig("Server", "app_port") _, app_port = DashboardConfig.GetConfig("Server", "app_port")
return app_ip, 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 = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'), check_same_thread=False)
sqldb.row_factory = sqlite3.Row sqldb.row_factory = sqlite3.Row
cursor = sqldb.cursor() cursor = sqldb.cursor()
DashboardConfig = DashboardConfig() DashboardConfig = DashboardConfig()
AllPeerShareLinks: PeerShareLinks = PeerShareLinks()
AllPeerJobs: PeerJobs = PeerJobs() AllPeerJobs: PeerJobs = PeerJobs()
JobLogger: Logger = Logger() JobLogger: PeerJobLogger = PeerJobLogger()
DashboardLogger: DashboardLogger = DashboardLogger()
_, app_ip = DashboardConfig.GetConfig("Server", "app_ip") _, app_ip = DashboardConfig.GetConfig("Server", "app_ip")
_, app_port = DashboardConfig.GetConfig("Server", "app_port") _, app_port = DashboardConfig.GetConfig("Server", "app_port")
_, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path") _, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
WireguardConfigurations: dict[str, WireguardConfiguration] = {} WireguardConfigurations: dict[str, WireguardConfiguration] = {}
WireguardConfigurations = _getConfigurationList() WireguardConfigurations = _getConfigurationList()
def startThreads():
bgThread = threading.Thread(target=backGroundThread) bgThread = threading.Thread(target=backGroundThread)
bgThread.daemon = True bgThread.daemon = True
bgThread.start() bgThread.start()
@@ -1862,5 +2084,7 @@ scheduleJobThread = threading.Thread(target=peerJobScheduleBackgroundThread)
scheduleJobThread.daemon = True scheduleJobThread.daemon = True
scheduleJobThread.start() scheduleJobThread.start()
if __name__ == "__main__": if __name__ == "__main__":
startThreads()
app.run(host=app_ip, debug=False, port=app_port) app.run(host=app_ip, debug=False, port=app_port)
+15 -1
View File
@@ -1,8 +1,14 @@
import multiprocessing
import dashboard import dashboard
from datetime import datetime
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger
app_host, app_port = dashboard.gunicornConfig() 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' worker_class = 'gthread'
workers = 1 workers = 1
@@ -10,3 +16,11 @@ threads = 1
bind = f"{app_host}:{app_port}" bind = f"{app_host}:{app_port}"
daemon = True daemon = True
pidfile = './gunicorn.pid' pidfile = './gunicorn.pid'
wsgi_app = "dashboard:app"
accesslog = f"./log/access_{date}.log"
log_level = "debug"
capture_output = True
errorlog = f"./log/error_{date}.log"
print(f"[WGDashboard] WGDashboard w/ Gunicorn will be running on {bind}", flush=True)
print(f"[WGDashboard] Access log file is at {accesslog}", flush=True)
print(f"[WGDashboard] Error log file is at {errorlog}", flush=True)
File diff suppressed because one or more lines are too long
+30 -30
View File
File diff suppressed because one or more lines are too long
+24
View File
@@ -8,6 +8,7 @@
"name": "app", "name": "app",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@vuepic/vue-datepicker": "^9.0.1",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"@vueuse/shared": "^10.9.0", "@vueuse/shared": "^10.9.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
@@ -721,6 +722,20 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz",
"integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==" "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA=="
}, },
"node_modules/@vuepic/vue-datepicker": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-9.0.1.tgz",
"integrity": "sha512-5sSdwib5cY8cE4Y7SCh+Zemfp+U/m6BDcgaPwd5Vmdv5LAASyV0wugn9sTb6NWX0sIQEdrGDl/RmD9EjcIke3A==",
"dependencies": {
"date-fns": "^3.6.0"
},
"engines": {
"node": ">=18.12.0"
},
"peerDependencies": {
"vue": ">=3.2.0"
}
},
"node_modules/@vueuse/core": { "node_modules/@vueuse/core": {
"version": "10.9.0", "version": "10.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz",
@@ -937,6 +952,15 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.12", "version": "1.11.12",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz",
+1
View File
@@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@vuepic/vue-datepicker": "^9.0.1",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"@vueuse/shared": "^10.9.0", "@vueuse/shared": "^10.9.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
+8 -6
View File
@@ -12,7 +12,7 @@ const store = DashboardConfigurationStore();
</nav> </nav>
<Suspense> <Suspense>
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition name="fade2" mode="out-in"> <Transition name="app" mode="out-in">
<Component :is="Component"></Component> <Component :is="Component"></Component>
</Transition> </Transition>
</RouterView> </RouterView>
@@ -22,14 +22,16 @@ const store = DashboardConfigurationStore();
<style scoped> <style scoped>
.app-enter-active, .app-enter-active,
.app-leave-active { .app-leave-active {
transition: all 0.3s ease-in-out; transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 0.9);
/*position: absolute;*/ }
/*padding-top: 50px*/
.app-enter-from{
transform: translateY(20px);
opacity: 0;
} }
.app-enter-from,
.app-leave-to { .app-leave-to {
transform: translateX(-30px); transform: translateY(-20px);
opacity: 0; opacity: 0;
} }
</style> </style>
@@ -14,7 +14,6 @@ export default {
data(){ data(){
return { return {
allowedIp: [], allowedIp: [],
availableIpSearchString: "", availableIpSearchString: "",
customAvailableIp: "", customAvailableIp: "",
allowedIpFormatError: false allowedIpFormatError: false
@@ -45,8 +44,16 @@ export default {
watch: { watch: {
customAvailableIp(){ customAvailableIp(){
this.allowedIpFormatError = false; this.allowedIpFormatError = false;
},
availableIp(){
if (this.availableIp !== undefined && this.availableIp.length > 0){
this.addAllowedIp(this.availableIp[0])
} }
} }
},
mounted() {
}
} }
</script> </script>
@@ -86,6 +86,7 @@ export default {
@setting="this.$emit('setting')" @setting="this.$emit('setting')"
@jobs="this.$emit('jobs')" @jobs="this.$emit('jobs')"
@refresh="this.$emit('refresh')" @refresh="this.$emit('refresh')"
@share="this.$emit('share')"
:Peer="Peer" :Peer="Peer"
v-if="this.subMenuOpened" v-if="this.subMenuOpened"
ref="target" ref="target"
@@ -40,6 +40,7 @@ import PeerJobs from "@/components/configurationComponents/peerJobs.vue";
import PeerJobsAllModal from "@/components/configurationComponents/peerJobsAllModal.vue"; import PeerJobsAllModal from "@/components/configurationComponents/peerJobsAllModal.vue";
import PeerJobsLogsModal from "@/components/configurationComponents/peerJobsLogsModal.vue"; import PeerJobsLogsModal from "@/components/configurationComponents/peerJobsLogsModal.vue";
import {ref} from "vue"; import {ref} from "vue";
import PeerShareLinkModal from "@/components/configurationComponents/peerShareLinkModal.vue";
Chart.register( Chart.register(
ArcElement, ArcElement,
@@ -70,6 +71,7 @@ Chart.register(
export default { export default {
name: "peerList", name: "peerList",
components: { components: {
PeerShareLinkModal,
PeerJobsLogsModal, PeerJobsLogsModal,
PeerJobsAllModal, PeerJobs, PeerCreate, PeerQRCode, PeerSettings, PeerSearch, Peer, Line, Bar}, PeerJobsAllModal, PeerJobs, PeerCreate, PeerQRCode, PeerSettings, PeerSearch, Peer, Line, Bar},
setup(){ setup(){
@@ -131,6 +133,10 @@ export default {
}, },
peerScheduleJobsLogs: { peerScheduleJobsLogs: {
modalOpen: false modalOpen: false
},
peerShare:{
modalOpen: false,
selectedPeer: undefined
} }
} }
}, },
@@ -141,26 +147,21 @@ export default {
'$route': { '$route': {
immediate: true, immediate: true,
handler(){ handler(){
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval); clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
this.loading = true; this.loading = true;
let id = this.$route.params.id; let id = this.$route.params.id;
this.configurationInfo = []; this.configurationInfo = [];
this.configurationPeers = []; this.configurationPeers = [];
if (id){ if (id){
this.getPeers(id) this.getPeers(id)
console.log("Changed..")
this.setPeerInterval(); this.setPeerInterval();
} }
} }
}, },
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){ 'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
console.log("Changed?")
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval); clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
this.setPeerInterval(); this.setPeerInterval();
}, }
}, },
beforeRouteLeave(){ beforeRouteLeave(){
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval); clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
@@ -211,7 +212,8 @@ export default {
{ {
label: 'Data Sent', label: 'Data Sent',
data: [...this.historySentData.datasets[0].data, 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, fill: false,
borderColor: '#198754', borderColor: '#198754',
tension: 0 tension: 0
@@ -231,7 +233,8 @@ export default {
{ {
label: 'Data Received', label: 'Data Received',
data: [...this.historyReceiveData.datasets[0].data, 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, fill: false,
borderColor: '#0d6efd', borderColor: '#0d6efd',
tension: 0 tension: 0
@@ -248,17 +251,24 @@ export default {
this.dashboardConfigurationStore.Peers.RefreshInterval = setInterval(() => { this.dashboardConfigurationStore.Peers.RefreshInterval = setInterval(() => {
this.getPeers() this.getPeers()
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval)) }, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
console.log(this.dashboardConfigurationStore.Peers.RefreshInterval)
}, },
}, },
computed: { computed: {
configurationSummary(){ configurationSummary(){
return { const k = {
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length, 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, totalUsage: this.configurationPeers.length > 0 ?
totalReceive: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b) : 0, this.configurationPeers.filter(x => !x.restricted)
totalSent: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b) : 0 .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(){ receiveData(){
return this.historyReceiveData return this.historyReceiveData
@@ -468,7 +478,7 @@ export default {
<div class="card-body d-flex"> <div class="card-body d-flex">
<div> <div>
<p class="mb-0 text-muted"><small>Total Usage</small></p> <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> </div>
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i> <i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
</div> </div>
@@ -479,7 +489,7 @@ export default {
<div class="card-body d-flex"> <div class="card-body d-flex">
<div> <div>
<p class="mb-0 text-muted"><small>Total Received</small></p> <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> </div>
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i> <i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
</div> </div>
@@ -490,7 +500,7 @@ export default {
<div class="card-body d-flex"> <div class="card-body d-flex">
<div> <div>
<p class="mb-0 text-muted"><small>Total Sent</small></p> <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> </div>
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i> <i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
</div> </div>
@@ -548,7 +558,7 @@ export default {
:key="peer.id" :key="peer.id"
v-for="peer in this.searchPeers"> v-for="peer in this.searchPeers">
<Peer :Peer="peer" <Peer :Peer="peer"
@share="this.peerShare.selectedPeer = peer.id; this.peerShare.modalOpen = true;"
@refresh="this.getPeers()" @refresh="this.getPeers()"
@jobs="peerScheduleJobs.modalOpen = true; peerScheduleJobs.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)" @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)" @setting="peerSetting.modalOpen = true; peerSetting.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"
@@ -595,6 +605,12 @@ export default {
> >
</PeerJobsLogsModal> </PeerJobsLogsModal>
</Transition> </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> </div>
</template> </template>
@@ -7,7 +7,6 @@ export default {
}, },
mounted() { mounted() {
QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfigData , (error) => { QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfigData , (error) => {
console.log(this.peerConfigData)
if (error) console.error(error) if (error) console.error(error)
}) })
} }
@@ -3,10 +3,12 @@ import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJ
import {ref} from "vue"; import {ref} from "vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchPost} from "@/utilities/fetch.js"; import {fetchPost} from "@/utilities/fetch.js";
import VueDatePicker from "@vuepic/vue-datepicker";
import dayjs from "dayjs";
export default { export default {
name: "schedulePeerJob", name: "schedulePeerJob",
components: {ScheduleDropdown}, components: {VueDatePicker, ScheduleDropdown},
props: { props: {
dropdowns: Array[Object], dropdowns: Array[Object],
pjob: Object, pjob: Object,
@@ -94,6 +96,11 @@ export default {
}) })
} }
this.$emit('delete') 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" :data="this.job.Operator"
@update="(value) => this.job.Operator = value" @update="(value) => this.job.Operator = value"
></ScheduleDropdown> ></ScheduleDropdown>
<input class="form-control form-control-sm form-control-dark rounded-3 flex-grow-1"
<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" :disabled="!edit"
type="datetime-local"
v-if="this.job.Field === 'date'" v-if="this.job.Field === 'date'"
v-model="this.job.Value" :dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
style="width: auto"> />
<!-- <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" <input class="form-control form-control-sm form-control-dark rounded-3 flex-grow-1"
:disabled="!edit" :disabled="!edit"
v-else v-else
@@ -188,4 +209,12 @@ input:disabled{
background-color: rgba(13, 110, 253, 0.09); background-color: rgba(13, 110, 253, 0.09);
color: #0d6efd; color: #0d6efd;
} }
.dp__main{
width: auto;
flex-grow: 1;
--dp-input-padding: 2.5px 30px 2.5px 12px;
--dp-border-radius: 0.5rem;
}
</style> </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"> 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 <i class="bi bi-plus-lg me-2"></i>Peer
</RouterLink> </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" <button class="btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle shadow-sm"
@click="this.downloadAllPeer()"> @click="this.downloadAllPeer()">
<i class="bi bi-download me-2"></i> Download All <i class="bi bi-download me-2"></i> Download All
</button> </button>
<div class="d-flex align-items-center ms-auto"> <div class="flex-grow-1">
<!-- <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 w-100"
<input class="form-control rounded-3 bg-secondary-subtle border-1 border-secondary-subtle shadow-sm"
placeholder="Search..." placeholder="Search..."
id="searchPeers" id="searchPeers"
@keyup="this.debounce()" @keyup="this.debounce()"
v-model="this.searchString"> v-model="this.searchString">
</div> </div>
<div class="dropdown"> <div class="dropdown dropup">
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm" <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"> type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-filter-circle me-2"></i> <i class="bi bi-filter-circle me-2"></i>
Sort Display
</button> </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"> <li v-for="(value, key) in this.sort">
<a class="dropdown-item d-flex" role="button" @click="this.updateSort(key)"> <a class="dropdown-item d-flex align-items-center" role="button" @click="this.updateSort(key)">
<span class="me-auto">{{value}}</span> <small class="me-auto">{{value}}</small>
<i class="bi bi-check text-primary" <i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_sort === key"></i> v-if="store.Configuration.Server.dashboard_sort === key"></i>
</a> </a>
</li> </li>
</ul> <li><hr class="dropdown-divider"></li>
</div> <li>
<div class="dropdown"> <small class="dropdown-header">Refresh Interval</small>
<button class="btn dropdown-toggle text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm" </li>
type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-arrow-repeat me-2"></i>Refresh Interval
</button>
<ul class="dropdown-menu shadow mt-2 rounded-3">
<li v-for="(value, key) in this.interval"> <li v-for="(value, key) in this.interval">
<a class="dropdown-item d-flex" role="button" @click="updateRefreshInterval(key)"> <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" <i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i> v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i>
</a></li> </a>
</li>
</ul> </ul>
</div> </div>
<div class="dropdown dropup">
<div class="dropdown"> <button class="btn text-secondary-emphasis bg-secondary-subtle rounded-3 border-1 border-secondary-subtle shadow-sm"
<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"> 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> </button>
<ul class="dropdown-menu shadow mt-2 rounded-3"> <ul class="dropdown-menu shadow mt-2 rounded-3 animate__animated animation__fadeInDropdown">
<li> <li>
<h6 class="dropdown-header">Peer Jobs</h6> <h6 class="dropdown-header">Peer Jobs</h6>
</li> </li>
<li> <li>
<a role="button" class="dropdown-item" @click="this.$emit('jobsAll')"> <a role="button" class="dropdown-item" @click="this.$emit('jobsAll')">
All Active Jobs Active Jobs
</a> </a>
</li> </li>
<li> <li>
@@ -165,4 +157,23 @@ export default {
<style scoped> <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> </style>
@@ -37,6 +37,21 @@ export default {
} }
this.$emit("refresh") 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() { beforeMount() {
@@ -57,15 +72,15 @@ export default {
<div class="container d-flex h-100 w-100"> <div class="container d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal"> <div class="m-auto modal-dialog-centered dashboardModal">
<div class="card rounded-3 shadow flex-grow-1"> <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> <h4 class="mb-0">Peer Settings</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button> <button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div> </div>
<div class="card-body px-4 pb-4" v-if="this.data"> <div class="card-body px-4 pb-4" v-if="this.data">
<div class="d-flex flex-column gap-2 mb-4"> <div class="d-flex flex-column gap-2 mb-4">
<div> <div class="d-flex align-items-center">
<small class="text-muted">Public Key</small><br> <small class="text-muted">Public Key</small>
<small><samp>{{this.data.id}}</samp></small> <small class="ms-auto"><samp>{{this.data.id}}</samp></small>
</div> </div>
<div> <div>
<label for="peer_name_textbox" class="form-label"> <label for="peer_name_textbox" class="form-label">
@@ -121,8 +136,7 @@ export default {
v-model="this.data.DNS" v-model="this.data.DNS"
id="peer_DNS_textbox"> id="peer_DNS_textbox">
</div> </div>
<hr> <div class="accordion mt-3" id="peerSettingsAccordion">
<div class="accordion mt-2" id="peerSettingsAccordion">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button rounded-3 collapsed" type="button" <button class="accordion-button rounded-3 collapsed" type="button"
@@ -162,12 +176,37 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<hr>
<div class="d-flex gap-2 align-items-center">
<strong>Reset Data Usage</strong>
<div class="d-flex gap-2 ms-auto">
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
@click="this.resetPeerData('total')"
>
<i class="bi bi-arrow-down-up me-2"></i>
Total
</button>
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
@click="this.resetPeerData('receive')"
>
<i class="bi bi-arrow-down me-2"></i>
Received
</button>
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
@click="this.resetPeerData('sent')"
>
<i class="bi bi-arrow-up me-2"></i>
Sent
</button>
</div>
</div>
</div> </div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<button class="btn btn-secondary rounded-3 shadow" <button class="btn btn-secondary rounded-3 shadow"
@click="this.reset()" @click="this.reset()"
:disabled="!this.dataChanged || this.saving"> :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>
<button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow" <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 set for this peer
</small> </small>
</li> </li>
<li><hr class="dropdown-divider"></li>
</template> </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> <li>
<a class="dropdown-item d-flex" role="button" <a class="dropdown-item d-flex" role="button"
@click="this.$emit('setting')" @click="this.$emit('setting')"
@@ -109,20 +124,7 @@ export default {
<i class="me-auto bi bi-app-indicator"></i> Schedule Jobs <i class="me-auto bi bi-app-indicator"></i> Schedule Jobs
</a> </a>
</li> </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><hr class="dropdown-divider"></li>
<li> <li>
@@ -0,0 +1,166 @@
<script>
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchPost} from "@/utilities/fetch.js";
import dayjs from "dayjs";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import VueDatePicker from '@vuepic/vue-datepicker';
export default {
name: "peerShareLinkModal",
props: {
peer: Object
},
components: {
VueDatePicker
},
data(){
return {
dataCopy: undefined,
loading: false
}
},
setup(){
const store = DashboardConfigurationStore();
return {store}
},
mounted() {
this.dataCopy = JSON.parse(JSON.stringify(this.peer.ShareLink)).at(0);
},
watch: {
'peer.ShareLink': {
deep: true,
handler(newVal, oldVal){
if (oldVal.length !== newVal.length){
this.dataCopy = JSON.parse(JSON.stringify(this.peer.ShareLink)).at(0);
}
}
}
},
methods: {
startSharing(){
this.loading = true;
fetchPost("/api/sharePeer/create", {
Configuration: this.peer.configuration.Name,
Peer: this.peer.id,
ExpireDate: dayjs().add(7, 'd').format("YYYY-MM-DD HH:mm:ss")
}, (res) => {
if (res.status){
this.peer.ShareLink = res.data;
this.dataCopy = res.data.at(0);
this.store.newMessage("Server", "Share link created successfully", "success")
}else{
this.store.newMessage("Server",
"Share link failed to create. Reason: " + res.message, "danger")
}
this.loading = false;
})
},
updateLinkExpireDate(){
fetchPost("/api/sharePeer/update", this.dataCopy, (res) => {
if (res.status){
this.dataCopy = res.data.at(0)
this.peer.ShareLink = res.data;
this.store.newMessage("Server", "Link expire date updated", "success")
}else{
this.store.newMessage("Server",
"Link expire date failed to update. Reason: " + res.message, "danger")
}
this.loading = false
});
},
stopSharing(){
this.loading = true;
this.dataCopy.ExpireDate = dayjs().format("YYYY-MM-DD HH:mm:ss")
this.updateLinkExpireDate()
},
parseTime(modelData){
if(modelData){
this.dataCopy.ExpireDate = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
}else{
this.dataCopy.ExpireDate = undefined
}
this.updateLinkExpireDate()
}
},
computed: {
getUrl(){
return window.location.origin
+ window.location.pathname
+ this.$router.resolve(
{path: "/share", query: {"ShareID": this.dataCopy.ShareID}}).href;
}
}
}
</script>
<template>
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
<div class="container d-flex h-100 w-100">
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 500px">
<div class="card rounded-3 shadow flex-grow-1">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
<h4 class="mb-0">Share Peer</h4>
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div>
<div class="card-body px-4 pb-4" v-if="this.peer.ShareLink">
<div v-if="!this.dataCopy">
<h6 class="mb-3 text-muted">
Currently the peer is not sharing
</h6>
<button
@click="this.startSharing()"
:disabled="this.loading"
class="w-100 btn bg-success-subtle text-success-emphasis border-1 border-success-subtle rounded-3 shadow-sm">
<span :class="{'animate__animated animate__flash animate__infinite animate__slower': this.loading}">
<i class="bi bi-send-fill me-2" ></i>
</span>
{{this.loading ? "Sharing...":"Start Sharing"}}
</button>
</div>
<div v-else>
<div class="d-flex gap-2 mb-4">
<i class="bi bi-link-45deg"></i>
<a :href="this.getUrl"
class="text-decoration-none" target="_blank">
{{ getUrl }}
</a>
</div>
<div class="d-flex flex-column gap-2 mb-3">
<small>
<i class="bi bi-calendar me-2"></i>
Expire Date
</small>
<VueDatePicker
:is24="true"
:min-date="new Date()"
:model-value="this.dataCopy.ExpireDate"
@update:model-value="this.parseTime" time-picker-inline
format="yyyy-MM-dd HH:mm:ss"
preview-format="yyyy-MM-dd HH:mm:ss"
:dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
/>
</div>
<button
@click="this.stopSharing()"
:disabled="this.loading"
class="w-100 btn bg-danger-subtle text-danger-emphasis border-1 border-danger-subtle rounded-3 shadow-sm">
<span :class="{'animate__animated animate__flash animate__infinite animate__slower': this.loading}">
<i class="bi bi-send-slash-fill me-2" ></i>
</span>
{{this.loading ? "Stop Sharing...":"Stop Sharing"}}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>
@@ -8,7 +8,6 @@ export default {
components: {ConfigurationCard}, components: {ConfigurationCard},
async setup(){ async setup(){
const wireguardConfigurationsStore = WireguardConfigurationsStore(); const wireguardConfigurationsStore = WireguardConfigurationsStore();
return {wireguardConfigurationsStore} return {wireguardConfigurationsStore}
}, },
data(){ data(){
@@ -19,6 +18,13 @@ export default {
async mounted() { async mounted() {
await this.wireguardConfigurationsStore.getConfigurations(); await this.wireguardConfigurationsStore.getConfigurations();
this.configurationLoaded = true; this.configurationLoaded = true;
this.wireguardConfigurationsStore.ConfigurationListInterval = setInterval(() => {
this.wireguardConfigurationsStore.getConfigurations()
}, 10000)
},
beforeUnmount() {
clearInterval(this.wireguardConfigurationsStore.ConfigurationListInterval)
} }
} }
</script> </script>
@@ -28,7 +34,9 @@ export default {
<div class="mt-5"> <div class="mt-5">
<div class="container"> <div class="container">
<div class="d-flex mb-4 "> <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"> <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> <i class="bi bi-plus-circle-fill me-2"></i>
Configuration Configuration
@@ -40,7 +48,7 @@ export default {
<p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0"> <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". 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> </p>
<div class="d-flex gap-3 flex-column" v-else> <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> <ConfigurationCard v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name" :c="c"></ConfigurationCard>
</div> </div>
</div> </div>
@@ -10,7 +10,6 @@ export default {
Status: Boolean, Status: Boolean,
PublicKey: String, PublicKey: String,
PrivateKey: String PrivateKey: String
} }
}, },
data(){ data(){
@@ -52,9 +51,24 @@ export default {
<i class="bi bi-chevron-right"></i> <i class="bi bi-chevron-right"></i>
</h6> </h6>
</RouterLink> </RouterLink>
<div class="card-footer d-flex align-items-center"> <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"> <small class="me-2 text-muted">
<strong>PUBLIC KEY</strong> <strong>Public Key</strong>
</small> </small>
<small class="mb-0 d-block d-lg-inline-block "> <small class="mb-0 d-block d-lg-inline-block ">
<samp style="line-break: anywhere">{{c.PublicKey}}</samp> <samp style="line-break: anywhere">{{c.PublicKey}}</samp>
@@ -76,6 +90,7 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
+30 -19
View File
@@ -17,41 +17,52 @@ export default {
<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" style="height: calc(-50px + 100vh);">
<nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow overflow-y-scroll" > <nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow overflow-y-scroll" >
<div class="sidebar-sticky pt-3"> <div class="sidebar-sticky pt-3">
<ul class="nav flex-column"> <ul class="nav flex-column px-2">
<li class="nav-item"> <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"> <li class="nav-item">
<RouterLink class="nav-link" to="/settings" <RouterLink class="nav-link rounded-3" to="/settings"
exact-active-class="active">Settings</RouterLink></li> exact-active-class="active">
<i class="bi bi-gear me-2"></i>
Settings</RouterLink></li>
</ul> </ul>
<hr> <hr class="text-body">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted text-center">
<span>Configurations</span> <i class="bi bi-body-text me-2"></i>
Configurations
</h6> </h6>
<ul class="nav flex-column"> <ul class="nav flex-column px-2">
<li class="nav-item"> <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" active-class="active"
v-for="c in this.wireguardConfigurationsStore.Configurations"> v-for="c in this.wireguardConfigurationsStore.Configurations">
<samp>{{c.Name}}</samp> {{c.Name}}
</RouterLink> </RouterLink>
</li> </li>
</ul> </ul>
<hr> <hr class="text-body">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted text-center">
<span>Tools</span> <i class="bi bi-tools me-2"></i>
Tools
</h6> </h6>
<ul class="nav flex-column"> <ul class="nav flex-column px-2">
<li class="nav-item"> <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"> <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> </li>
</ul> </ul>
<hr> <hr class="text-body">
<ul class="nav flex-column"> <ul class="nav flex-column px-2">
<li class="nav-item"><a class="nav-link text-danger" @click="this.dashboardConfigurationStore.signOut()" role="button" style="font-weight: bold">Sign Out</a></li> <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>
<ul class="nav flex-column"> <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> <li class="nav-item"><a href="https://github.com/donaldzou/WGDashboard/releases/tag/"><small class="nav-link text-muted"></small></a></li>
@@ -0,0 +1,63 @@
<script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {v4} from "uuid";
import {fetchPost} from "@/utilities/fetch.js";
export default {
name: "accountSettingsMFA",
setup(){
const store = DashboardConfigurationStore();
const uuid = `input_${v4()}`;
return {store, uuid};
},
data(){
return {
status: false
}
},
mounted() {
this.status = this.store.Configuration.Account["enable_totp"]
},
methods: {
async resetMFA(){
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Account",
key: "totp_verified",
value: "false"
}, async (res) => {
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Account",
key: "enable_totp",
value: "false"
}, (res) => {
if (res.status){
this.$router.push("/2FASetup")
}
})
})
}
}
}
</script>
<template>
<div>
<div class="d-flex align-items-center">
<strong>Multi-Factor Authentication</strong>
<div class="form-check form-switch ms-3">
<input class="form-check-input" type="checkbox"
v-model="this.status"
role="switch" id="allowAPIKeysSwitch">
</div>
<button class="btn bg-warning-subtle text-warning-emphasis border-1 border-warning-subtle ms-auto rounded-3 shadow-sm"
v-if="this.status" @click="this.resetMFA()">
<i class="bi bi-shield-lock-fill me-2"></i>
{{this.store.Configuration.Account["totp_verified"] ? "Reset" : "Setup" }} MFA
</button>
</div>
</div>
</template>
<style scoped>
</style>
@@ -2,13 +2,15 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import {fetchPost} from "@/utilities/fetch.js"; import {fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import VueDatePicker from "@vuepic/vue-datepicker";
export default { export default {
name: "newDashboardAPIKey", name: "newDashboardAPIKey",
components: {VueDatePicker},
data(){ data(){
return{ return{
newKeyData:{ 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 neverExpire: false
}, },
submitting: false submitting: false
@@ -39,6 +41,13 @@ export default {
fixDate(date){ fixDate(date){
console.log(dayjs(date).format("YYYY-MM-DDTHH:mm:ss")) console.log(dayjs(date).format("YYYY-MM-DDTHH:mm:ss"))
return 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)"> style="background-color: #00000060; backdrop-filter: blur(3px)">
<div class="card m-auto rounded-3 mt-5"> <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"> <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> <button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
</div> </div>
<div class="card-body d-flex gap-2 p-4 flex-column"> <div class="card-body d-flex gap-2 p-4 flex-column">
<small class="text-muted">When should this API Key expire?</small> <small class="text-muted">When should this API Key expire?</small>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<input class="form-control" type="datetime-local" <VueDatePicker
@change="this.newKeyData.ExpiredAt = this.fixDate(this.newKeyData.ExpiredAt)" :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" :disabled="this.newKeyData.neverExpire || this.submitting"
v-model="this.newKeyData.ExpiredAt"> :dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
/>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
@@ -1,15 +1,17 @@
<script> <script>
import {fetchGet, fetchPost} from "@/utilities/fetch.js"; import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import QRCode from "qrcode"; import QRCode from "qrcode";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default { export default {
name: "totp", name: "totp",
async setup(){ async setup(){
const store = DashboardConfigurationStore();
let l = "" let l = ""
await fetchGet("/api/Welcome_GetTotpLink", {}, (res => { await fetchGet("/api/Welcome_GetTotpLink", {}, (res => {
if (res.status) l = res.data; if (res.status) l = res.data;
})); }));
return {l} return {l, store}
}, },
mounted() { mounted() {
if (this.l) { if (this.l) {
@@ -58,7 +60,12 @@ export default {
</script> </script>
<template> <template>
<div class="mb-3"> <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> <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> <canvas id="qrcode" class="rounded-3 mb-2"></canvas>
<div class="p-3 bg-body-secondary rounded-3 border mb-3"> <div class="p-3 bg-body-secondary rounded-3 border mb-3">
@@ -66,7 +73,7 @@ export default {
</p><a :href="this.l"><code style="line-break: anywhere">{{this.l}}</code></a> </p><a :href="this.l"><code style="line-break: anywhere">{{this.l}}</code></a>
</div> </div>
<label for="totp" class="mb-2"><small class="text-muted">2. Enter the TOTP generated by your authenticator to verify</small></label> <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"> <div class="form-group mb-2">
<input class="form-control text-center totp" <input class="form-control text-center totp"
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code" id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
v-model="this.totp" v-model="this.totp"
@@ -79,7 +86,32 @@ export default {
TOTP verified! TOTP verified!
</div> </div>
</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>
</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> </template>
<style scoped> <style scoped>
+1
View File
@@ -3,6 +3,7 @@ import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.js' import 'bootstrap/dist/js/bootstrap.js'
import 'bootstrap-icons/font/bootstrap-icons.css' import 'bootstrap-icons/font/bootstrap-icons.css'
import 'animate.css/animate.compat.css' import 'animate.css/animate.compat.css'
import '@vuepic/vue-datepicker/dist/main.css'
import {createApp, markRaw} from 'vue' import {createApp, markRaw} from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
+19 -6
View File
@@ -4,20 +4,18 @@ import Index from "@/views/index.vue"
import Signin from "@/views/signin.vue"; import Signin from "@/views/signin.vue";
import ConfigurationList from "@/components/configurationList.vue"; import ConfigurationList from "@/components/configurationList.vue";
import {fetchGet} from "@/utilities/fetch.js"; import {fetchGet} from "@/utilities/fetch.js";
import {wgdashboardStore} from "@/stores/wgdashboardStore.js";
import Settings from "@/views/settings.vue"; import Settings from "@/views/settings.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"; import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Setup from "@/views/setup.vue"; import Setup from "@/views/setup.vue";
import NewConfiguration from "@/views/newConfiguration.vue"; import NewConfiguration from "@/views/newConfiguration.vue";
import Configuration from "@/views/configuration.vue"; import Configuration from "@/views/configuration.vue";
import PeerSettings from "@/components/configurationComponents/peerSettings.vue";
import PeerList from "@/components/configurationComponents/peerList.vue"; import PeerList from "@/components/configurationComponents/peerList.vue";
import PeerCreate from "@/components/configurationComponents/peerCreate.vue"; import PeerCreate from "@/components/configurationComponents/peerCreate.vue";
import RestrictedPeers from "@/components/configurationComponents/restrictedPeers.vue";
import Ping from "@/views/ping.vue"; import Ping from "@/views/ping.vue";
import Traceroute from "@/views/traceroute.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 () => { const checkAuth = async () => {
let result = false let result = false
@@ -30,6 +28,7 @@ const checkAuth = async () => {
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: [ routes: [
{ {
name: "Index", name: "Index",
path: '/', path: '/',
@@ -90,7 +89,6 @@ const router = createRouter({
path: 'create', path: 'create',
component: PeerCreate component: PeerCreate
}, },
] ]
}, },
@@ -105,8 +103,22 @@ const router = createRouter({
{ {
path: '/welcome', component: Setup, path: '/welcome', component: Setup,
meta: { 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"
}
} }
] ]
}); });
@@ -136,6 +148,7 @@ router.beforeEach(async (to, from, next) => {
}else{ }else{
dashboardConfigurationStore.Redirect = to; dashboardConfigurationStore.Redirect = to;
next("/signin") next("/signin")
dashboardConfigurationStore.newMessage("WGDashboard", "Session Ended", "warning")
} }
}else { }else {
next(); next();
@@ -6,6 +6,7 @@ export const WireguardConfigurationsStore = defineStore('WireguardConfigurations
state: () => ({ state: () => ({
Configurations: undefined, Configurations: undefined,
searchString: "", searchString: "",
ConfigurationListInterval: undefined,
PeerScheduleJobs: { PeerScheduleJobs: {
dropdowns: { dropdowns: {
Field: [ Field: [
+33 -13
View File
@@ -1,3 +1,5 @@
import router from "@/router/index.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export const fetchGet = async (url, params=undefined, callback=undefined) => { export const fetchGet = async (url, params=undefined, callback=undefined) => {
const urlSearchParams = new URLSearchParams(params); const urlSearchParams = new URLSearchParams(params);
await fetch(`${url}?${urlSearchParams.toString()}`, { await fetch(`${url}?${urlSearchParams.toString()}`, {
@@ -5,13 +7,21 @@ export const fetchGet = async (url, params=undefined, callback=undefined) => {
"content-type": "application/json" "content-type": "application/json"
} }
}) })
.then(x => x.json()) .then((x) => {
.then(x => callback ? callback(x) : undefined) const store = DashboardConfigurationStore();
.catch(x => { if (!x.ok){
// let router = useRouter() if (x.status !== 200){
// if (x.status === 401){ if (x.status === 401){
// router.push('/signin') router.push({path: '/signin'})
// } store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
}) })
} }
@@ -22,10 +32,20 @@ export const fetchPost = async (url, body, callback) => {
}, },
method: "POST", method: "POST",
body: JSON.stringify(body) body: JSON.stringify(body)
}) }).then((x) => {
.then(x => x.json()) const store = DashboardConfigurationStore();
.then(x => callback ? callback(x) : undefined) if (!x.ok){
// .catch(() => { if (x.status !== 200){
// alert("Error occurred! Check console") if (x.status === 401){
// }); router.push({path: '/signin'})
store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
})
} }
+7 -3
View File
@@ -11,11 +11,13 @@ import DashboardTheme from "@/components/settingsComponent/dashboardTheme.vue";
import DashboardSettingsInputIPAddressAndPort import DashboardSettingsInputIPAddressAndPort
from "@/components/settingsComponent/dashboardSettingsInputIPAddressAndPort.vue"; from "@/components/settingsComponent/dashboardSettingsInputIPAddressAndPort.vue";
import DashboardAPIKeys from "@/components/settingsComponent/dashboardAPIKeys.vue"; import DashboardAPIKeys from "@/components/settingsComponent/dashboardAPIKeys.vue";
import AccountSettingsMFA from "@/components/settingsComponent/accountSettingsMFA.vue";
export default { export default {
name: "settings", name: "settings",
methods: {ipV46RegexCheck}, methods: {ipV46RegexCheck},
components: { components: {
AccountSettingsMFA,
DashboardAPIKeys, DashboardAPIKeys,
DashboardSettingsInputIPAddressAndPort, DashboardSettingsInputIPAddressAndPort,
DashboardTheme, DashboardTheme,
@@ -49,7 +51,7 @@ export default {
<PeersDefaultSettingsInput targetData="peer_mtu" title="MTU (Max Transmission Unit)"></PeersDefaultSettingsInput> <PeersDefaultSettingsInput targetData="peer_mtu" title="MTU (Max Transmission Unit)"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput> <PeersDefaultSettingsInput targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput targetData="remote_endpoint" title="Peer Remote Endpoint" <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> ></PeersDefaultSettingsInput>
</div> </div>
</div> </div>
@@ -67,14 +69,16 @@ export default {
</div> </div>
<div class="card mb-4 shadow rounded-3"> <div class="card mb-4 shadow rounded-3">
<p class="card-header">Account Settings</p> <p class="card-header">Account Settings</p>
<div class="card-body"> <div class="card-body d-flex gap-4 flex-column">
<AccountSettingsInputUsername targetData="username" <AccountSettingsInputUsername targetData="username"
title="Username" title="Username"
></AccountSettingsInputUsername> ></AccountSettingsInputUsername>
<hr> <hr class="m-0">
<AccountSettingsInputPassword <AccountSettingsInputPassword
targetData="password"> targetData="password">
</AccountSettingsInputPassword> </AccountSettingsInputPassword>
<hr class="m-0">
<AccountSettingsMFA></AccountSettingsMFA>
</div> </div>
</div> </div>
<DashboardAPIKeys></DashboardAPIKeys> <DashboardAPIKeys></DashboardAPIKeys>
+16 -25
View File
@@ -1,11 +1,9 @@
<script> <script>
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import QRCode from 'qrcode'
import Totp from "@/components/setupComponent/totp.vue";
import {fetchPost} from "@/utilities/fetch.js"; import {fetchPost} from "@/utilities/fetch.js";
export default { export default {
name: "setup", name: "setup",
components: {Totp}, components: {},
setup(){ setup(){
const store = DashboardConfigurationStore(); const store = DashboardConfigurationStore();
return {store} return {store}
@@ -16,8 +14,7 @@ export default {
username: "", username: "",
newPassword: "", newPassword: "",
repeatNewPassword: "", repeatNewPassword: "",
enable_totp: false, enable_totp: true
verified_totp: false
}, },
loading: false, loading: false,
errorMessage: "", errorMessage: "",
@@ -30,7 +27,6 @@ export default {
&& this.setup.newPassword.length >= 8 && this.setup.newPassword.length >= 8
&& this.setup.repeatNewPassword.length >= 8 && this.setup.repeatNewPassword.length >= 8
&& this.setup.newPassword === this.setup.repeatNewPassword && this.setup.newPassword === this.setup.repeatNewPassword
&& ((this.setup.enable_totp && this.setup.verified_totp) || !this.setup.enable_totp)
} }
}, },
methods: { methods: {
@@ -39,9 +35,7 @@ export default {
fetchPost("/api/Welcome_Finish", this.setup, (res) => { fetchPost("/api/Welcome_Finish", this.setup, (res) => {
if (res.status){ if (res.status){
this.done = true; this.done = true;
setTimeout(() => { this.$router.push('/2FASetup')
this.$router.push('/')
}, 500)
}else{ }else{
document.querySelectorAll("#createAccount input").forEach(x => x.classList.add("is-invalid")) document.querySelectorAll("#createAccount input").forEach(x => x.classList.add("is-invalid"))
this.errorMessage = res.message; this.errorMessage = res.message;
@@ -62,7 +56,7 @@ export default {
<template> <template>
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll" <div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
:data-bs-theme="this.store.Configuration.Server.dashboard_theme"> :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> <span class="dashboardLogo display-4">Nice to meet you!</span>
<p class="mb-5">Please fill in the following fields to finish setup 😊</p> <p class="mb-5">Please fill in the following fields to finish setup 😊</p>
<div> <div>
@@ -94,26 +88,23 @@ export default {
class="form-control" id="confirmPassword" name="confirmPassword" placeholder="and you can remember it :)" required> class="form-control" id="confirmPassword" name="confirmPassword" placeholder="and you can remember it :)" required>
</div> </div>
</div> </div>
<hr> <!-- <div class="form-check form-switch">-->
<div class="form-check form-switch"> <!-- <input class="form-check-input" type="checkbox" role="switch" id="enable_totp" -->
<input class="form-check-input" type="checkbox" role="switch" id="enable_totp" <!-- v-model="this.setup.enable_totp">-->
v-model="this.setup.enable_totp"> <!-- <label class="form-check-label" -->
<label class="form-check-label" <!-- for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label>-->
for="enable_totp">Enable 2 Factor Authentication? <strong>Strongly recommended</strong></label> <!-- </div>-->
</div> <!-- <Suspense>-->
<Suspense> <!-- <Transition name="fade">-->
<Transition name="fade"> <!-- <Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp>-->
<Totp v-if="this.setup.enable_totp" @verified="this.setup.verified_totp = true"></Totp> <!-- </Transition>-->
</Transition> <!-- </Suspense>-->
</Suspense>
<button class="btn btn-dark btn-lg mb-5 d-flex btn-brand shadow align-items-center" <button class="btn btn-dark btn-lg mb-5 d-flex btn-brand shadow align-items-center"
ref="signInBtn" ref="signInBtn"
:disabled="!this.goodToSubmit || this.loading || this.done" @click="this.submit()"> :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"> <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> Next<i class="bi bi-chevron-right ms-auto"></i></span>
<span class="d-flex align-items-center w-100" v-else-if="this.done">
Welcome to WGDashboard!</span>
<span class="d-flex align-items-center w-100" v-else> <span class="d-flex align-items-center w-100" v-else>
Saving...<span class="spinner-border ms-auto spinner-border-sm" role="status"> Saving...<span class="spinner-border ms-auto spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
+109
View File
@@ -0,0 +1,109 @@
<script>
import {useRoute} from "vue-router";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchGet} from "@/utilities/fetch.js";
import {ref} from "vue";
import QRCode from "qrcode";
export default {
name: "share",
async setup(){
const route = useRoute();
const loaded = ref(false)
const store = DashboardConfigurationStore();
const theme = ref("");
const peerConfiguration = ref("");
const blob = ref(new Blob())
await fetchGet("/api/getDashboardTheme", {}, (res) => {
theme.value = res.data
});
const id = route.query.ShareID
if(id === undefined || id.length === 0){
peerConfiguration.value = undefined
loaded.value = true;
}else{
await fetchGet("/api/sharePeer/get", {
ShareID: id
}, (res) => {
if (res.status){
peerConfiguration.value = res.data;
blob.value = new Blob([peerConfiguration.value.file], { type: "text/plain" });
}else{
peerConfiguration.value = undefined
}
loaded.value = true;
})
}
return {store, theme, peerConfiguration, blob}
},
mounted() {
QRCode.toCanvas(document.querySelector("#qrcode"), this.peerConfiguration.file , (error) => {
if (error) console.error(error)
})
},
methods:{
download(){
const blob = new Blob([this.peerConfiguration.file], { type: "text/plain" });
const jsonObjectUrl = URL.createObjectURL(blob);
const filename = `${this.peerConfiguration.fileName}.conf`;
const anchorEl = document.createElement("a");
anchorEl.href = jsonObjectUrl;
anchorEl.download = filename;
anchorEl.click();
}
},
computed:{
getBlob(){
return URL.createObjectURL(this.blob)
}
}
}
</script>
<template>
<div class="container-fluid login-container-fluid d-flex main pt-5 overflow-scroll"
:data-bs-theme="this.theme">
<div class="m-auto text-body" style="width: 500px">
<div class="text-center position-relative" style=""
v-if="!this.peerConfiguration">
<div class="animate__animated animate__fadeInUp">
<h1 style="font-size: 20rem; filter: blur(1rem); animation-duration: 7s"
class="animate__animated animate__flash animate__infinite">
<i class="bi bi-file-binary"></i>
</h1>
</div>
<div class="position-absolute w-100 h-100 top-0 start-0 d-flex animate__animated animate__fadeInUp"
style="animation-delay: 0.1s;"
>
<h3 class="m-auto">Oh no... This link is either expired or invalid.</h3>
</div>
</div>
<div v-else class="d-flex align-items-center flex-column gap-3">
<div class="h1 dashboardLogo text-center animate__animated animate__fadeInUp">
<h6>WGDashboard</h6>
Scan QR Code from the WireGuard App
</div>
<canvas id="qrcode" class="rounded-3 shadow animate__animated animate__fadeInUp mb-3" ref="qrcode"></canvas>
<p class="text-muted animate__animated animate__fadeInUp mb-1"
style="animation-delay: 0.2s;"
>or click the button below to download the <samp>.conf</samp> file</p>
<a
:download="this.peerConfiguration.fileName + '.conf'"
:href="getBlob"
class="btn btn-lg bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle animate__animated animate__fadeInUp shadow-sm"
style="animation-delay: 0.25s;"
>
<i class="bi bi-download"></i>
</a>
</div>
</div>
</div>
</template>
<style scoped>
.animate__fadeInUp{
animation-timing-function: cubic-bezier(0.42, 0, 0.22, 1.0)
}
</style>
+21 -4
View File
@@ -1,9 +1,11 @@
<script> <script>
import {fetchGet, fetchPost} from "../utilities/fetch.js"; import {fetchGet, fetchPost} from "../utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Message from "@/components/messageCentreComponent/message.vue";
export default { export default {
name: "signin", name: "signin",
components: {Message},
async setup(){ async setup(){
const store = DashboardConfigurationStore() const store = DashboardConfigurationStore()
let theme = "" let theme = ""
@@ -26,6 +28,11 @@ export default {
loading: false loading: false
} }
}, },
computed: {
getMessages(){
return this.store.Messages.filter(x => x.show)
}
},
methods: { methods: {
async auth(){ async auth(){
if (this.username && this.password && ((this.totpEnabled && this.totp) || !this.totpEnabled)){ if (this.username && this.password && ((this.totpEnabled && this.totp) || !this.totpEnabled)){
@@ -76,10 +83,12 @@ export default {
<template> <template>
<div class="container-fluid login-container-fluid d-flex main flex-column" :data-bs-theme="this.theme"> <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;"> <div class="login-box m-auto" style="width: 600px;">
<h4 class="mb-0 text-body">Welcome to</h4>
<span class="dashboardLogo display-3">WGDashboard</span>
<div class="m-auto"> <div class="m-auto">
<div class="card px-4 py-5 rounded-4 shadow-lg">
<div class="card-body">
<h4 class="mb-0 text-body">Welcome to</h4>
<span class="dashboardLogo display-3"><strong>WGDashboard</strong></span>
<div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError"> <div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError">
{{this.loginErrorMessage}} {{this.loginErrorMessage}}
</div> </div>
@@ -107,7 +116,7 @@ export default {
v-model="this.totp" v-model="this.totp"
> >
</div> </div>
<button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand shadow signInBtn" ref="signInBtn"> <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"> <span v-if="!this.loading" class="d-flex w-100">
Sign In<i class="ms-auto bi bi-chevron-right"></i> Sign In<i class="ms-auto bi bi-chevron-right"></i>
</span> </span>
@@ -121,10 +130,18 @@ export default {
</form> </form>
</div> </div>
</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">
WGDashboard v4.0 | Developed with by WGDashboard v4.0 | Developed with by
<a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a> <a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
</small> </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> </div>
</template> </template>
+14 -3
View File
@@ -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 { ::-webkit-scrollbar {
display: none; display: none;
} }
@@ -138,8 +146,7 @@
} }
.nav-link:hover { .nav-link:hover {
padding-left: 30px; background-color: #e8e8e8;
background-color: #dfdfdf;
} }
.sidebar .nav-link .feather { .sidebar .nav-link .feather {
@@ -149,6 +156,7 @@
.sidebar .nav-link.active, .bottomNavContainer .nav-link.active { .sidebar .nav-link.active, .bottomNavContainer .nav-link.active {
color: #007bff; color: #007bff;
} }
.sidebar .nav-link:hover .feather, .sidebar .nav-link:hover .feather,
@@ -1120,6 +1128,7 @@ pre.index-alert {
background-color: #00000060; background-color: #00000060;
z-index: 9999; z-index: 9999;
backdrop-filter: blur(1px); backdrop-filter: blur(1px);
-webkit-backdrop-filter: blur(1px);
} }
.dashboardModal{ .dashboardModal{
@@ -1153,4 +1162,6 @@ pre.index-alert {
opacity: 0; opacity: 0;
} }
.messageCentre{
z-index: 9999;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+14 -5
View File
@@ -165,6 +165,7 @@ _checkWireguard(){
install_wgd(){ install_wgd(){
printf "[WGDashboard] Starting to install WGDashboard\n" printf "[WGDashboard] Starting to install WGDashboard\n"
_checkWireguard _checkWireguard
sudo chmod -R 755 /etc/wireguard/
if [ ! -d "log" ] if [ ! -d "log" ]
then then
@@ -183,7 +184,7 @@ install_wgd(){
_installPythonPip _installPythonPip
version_pass=$(python3 -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 7) else print("0");') 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" ] if [ $version_pass == "0" ]
then then
printf "[WGDashboard] WGDashboard required Python 3.7 or above\n" printf "[WGDashboard] WGDashboard required Python 3.7 or above\n"
@@ -236,10 +237,18 @@ gunicorn_start () {
export PATH=$PATH:/usr/local/bin:$HOME/.local/bin export PATH=$PATH:/usr/local/bin:$HOME/.local/bin
fi fi
_check_and_set_venv _check_and_set_venv
sudo "$venv_gunicorn" --access-logfile log/access_"$d".log \ sudo "$venv_gunicorn" --config ./gunicorn.conf.py
--log-level 'debug' --capture-output \ sleep 5
--error-logfile log/error_"$d".log 'dashboard:app' checkPIDExist=0
printf "[WGDashboard] Log files is under ./log\n" 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" printf "%s\n" "$dashes"
} }