mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2026-05-06 03:16:19 +00:00
Compare commits
55 Commits
v4.3.3-qui
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40c0a91afa | ||
|
|
9d5478e3e3 | ||
|
|
b07be518fb | ||
|
|
35be7ca3db | ||
|
|
36521a7d70 | ||
|
|
3520087dce | ||
|
|
d354e8ad38 | ||
|
|
4be7cd4073 | ||
|
|
b04be970e6 | ||
|
|
de81384f88 | ||
|
|
071a4765b9 | ||
|
|
fbaa15cb8d | ||
|
|
c9abfa5f61 | ||
|
|
d3d7c7e678 | ||
|
|
930c4169c5 | ||
|
|
90614a6360 | ||
|
|
ef8459415b | ||
|
|
365f07da0b | ||
|
|
8d00cca321 | ||
|
|
1fffa33dd5 | ||
|
|
ff34e4f8c8 | ||
|
|
af385a4537 | ||
|
|
e2ff34ce1d | ||
|
|
2ac9fea881 | ||
|
|
9e59ad0ebc | ||
|
|
18c2568c22 | ||
|
|
d58e082336 | ||
|
|
523fee7b42 | ||
|
|
624206d5a7 | ||
|
|
fa9d9f24b5 | ||
|
|
c42bc47761 | ||
|
|
24e0f514d7 | ||
|
|
1744570086 | ||
|
|
9669782b0c | ||
|
|
03eddd2846 | ||
|
|
26b82745fc | ||
|
|
abc82369d9 | ||
|
|
f1764d3822 | ||
|
|
8fd15b9ca6 | ||
|
|
5f62e9ac69 | ||
|
|
8eb7e97d6e | ||
|
|
3b03099700 | ||
|
|
8d6eb4bb41 | ||
|
|
387adfee8c | ||
|
|
a33d6bfc9a | ||
|
|
5726627058 | ||
|
|
14b5885814 | ||
|
|
3d8cdcaf84 | ||
|
|
f1448b0e52 | ||
|
|
6e92b2b6a2 | ||
|
|
18ee8822d9 | ||
|
|
326628d258 | ||
|
|
566dda93f3 | ||
|
|
5c28e639ae | ||
|
|
3dfe740780 |
@@ -213,6 +213,75 @@ ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
|
|||||||
- Access the web interface via `http://your-ip:10086` (or whichever port you specified in the compose).
|
- Access the web interface via `http://your-ip:10086` (or whichever port you specified in the compose).
|
||||||
- The first time run will auto-generate WireGuard keys and configs (configs are generated from the template).
|
- The first time run will auto-generate WireGuard keys and configs (configs are generated from the template).
|
||||||
|
|
||||||
|
## 🧑💻 Local Development with Docker
|
||||||
|
|
||||||
|
You can develop against WGDashboard locally by mounting the `src/` directory into the container. This lets you edit Python and frontend code on your host and see changes reflected immediately (with a service restart for Python).
|
||||||
|
|
||||||
|
Create a `docker/compose-local.yaml` alongside the existing `compose.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
wgdashboard:
|
||||||
|
image: ghcr.io/wgdashboard/wgdashboard:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: wgdashboard
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- 10086:10086/tcp
|
||||||
|
- 51820:51820/udp
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- aconf:/etc/amnezia/amneziawg
|
||||||
|
- conf:/etc/wireguard
|
||||||
|
- data:/data
|
||||||
|
# Mount local src for live editing
|
||||||
|
- ../src:/opt/wgdashboard/src
|
||||||
|
# Keep venv in a named volume so it isn't overwritten by the mount
|
||||||
|
- venv:/opt/wgdashboard/src/venv
|
||||||
|
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
aconf:
|
||||||
|
conf:
|
||||||
|
data:
|
||||||
|
venv:
|
||||||
|
```
|
||||||
|
|
||||||
|
The key additions compared to the production compose file:
|
||||||
|
- `../src:/opt/wgdashboard/src` — mounts your local `src/` directory into the container so code changes are reflected without rebuilding the image.
|
||||||
|
- `venv:/opt/wgdashboard/src/venv` — keeps the Python virtual environment in a named Docker volume. Without this, the host mount would overwrite the venv created during image build.
|
||||||
|
|
||||||
|
To start the development container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f compose-local.yaml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
After editing Python files (e.g. `src/dashboard.py`), restart the container to pick up changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker restart wgdashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
For frontend changes, install dependencies and rebuild the Vue app on your host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src/static/app
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart the container so it serves the updated dist files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker restart wgdashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Closing remarks:
|
## Closing remarks:
|
||||||
|
|
||||||
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.
|
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
date = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
|
date = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
|
||||||
emailSender = EmailSender(dashboardConfig)
|
emailSender = EmailSender(dashboardConfig)
|
||||||
if not emailSender.ready():
|
if not emailSender.is_ready():
|
||||||
return ResponseObject(False, "We can't send you an email due to your Administrator has not setup email service. Please contact your administrator.")
|
return ResponseObject(False, "We can't send you an email due to your Administrator has not setup email service. Please contact your administrator.")
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|||||||
110
src/dashboard.py
110
src/dashboard.py
@@ -2,6 +2,8 @@ import logging
|
|||||||
import random, shutil, sqlite3, configparser, hashlib, ipaddress, json, os, secrets, subprocess
|
import random, shutil, sqlite3, configparser, hashlib, ipaddress, json, os, secrets, subprocess
|
||||||
import time, re, uuid, bcrypt, psutil, pyotp, threading
|
import time, re, uuid, bcrypt, psutil, pyotp, threading
|
||||||
import traceback
|
import traceback
|
||||||
|
from functools import wraps
|
||||||
|
from urllib.parse import unquote
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -68,6 +70,21 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F
|
|||||||
response.content_type = "application/json"
|
response.content_type = "application/json"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def require_fields(*fields):
|
||||||
|
"""Decorator that validates required fields in request.json."""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
data = request.json
|
||||||
|
if data is None:
|
||||||
|
return ResponseObject(False, "Request body must be JSON", status_code=400)
|
||||||
|
missing = [field for field in fields if field not in data]
|
||||||
|
if missing:
|
||||||
|
return ResponseObject(False, f"Missing required fields: {', '.join(missing)}", status_code=400)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
return decorator
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Flask App
|
Flask App
|
||||||
'''
|
'''
|
||||||
@@ -1204,52 +1221,75 @@ def API_getDashboardTheme():
|
|||||||
def API_getDashboardVersion():
|
def API_getDashboardVersion():
|
||||||
return ResponseObject(data=DashboardConfig.GetConfig("Server", "version")[1])
|
return ResponseObject(data=DashboardConfig.GetConfig("Server", "version")[1])
|
||||||
|
|
||||||
@app.post(f'{APP_PREFIX}/api/savePeerScheduleJob')
|
@app.post(f'{APP_PREFIX}/api/PeerScheduleJob')
|
||||||
|
@require_fields('Configuration', 'Peer', 'Field', 'Operator', 'Value', 'Action')
|
||||||
def API_savePeerScheduleJob():
|
def API_savePeerScheduleJob():
|
||||||
data = request.json
|
data = request.json
|
||||||
if "Job" not in data.keys():
|
|
||||||
return ResponseObject(False, "Please specify job")
|
configuration = WireguardConfigurations.get(data['Configuration'])
|
||||||
job: dict = data['Job']
|
|
||||||
if "Peer" not in job.keys() or "Configuration" not in job.keys():
|
|
||||||
return ResponseObject(False, "Please specify peer and configuration")
|
|
||||||
configuration = WireguardConfigurations.get(job['Configuration'])
|
|
||||||
if configuration is None:
|
if configuration is None:
|
||||||
return ResponseObject(False, "Configuration does not exist")
|
return ResponseObject(False, "Configuration does not exist", status_code=404)
|
||||||
f, fp = configuration.searchPeer(job['Peer'])
|
|
||||||
if not f:
|
|
||||||
return ResponseObject(False, "Peer does not exist")
|
|
||||||
|
|
||||||
|
peerKey = unquote(data['Peer'])
|
||||||
|
found, _ = configuration.searchPeer(peerKey)
|
||||||
|
if not found:
|
||||||
|
return ResponseObject(False, "Peer does not exist", status_code=404)
|
||||||
|
|
||||||
s, p = AllPeerJobs.saveJob(PeerJob(
|
jobID = data.get('JobID', str(uuid4()))
|
||||||
job['JobID'], job['Configuration'], job['Peer'], job['Field'], job['Operator'], job['Value'],
|
if len(AllPeerJobs.searchJobById(jobID)) > 0:
|
||||||
job['CreationDate'], job['ExpireDate'], job['Action']))
|
return ResponseObject(False, "Job already exists", status_code=409)
|
||||||
if s:
|
|
||||||
return ResponseObject(s, data=p)
|
|
||||||
return ResponseObject(s, message=p)
|
|
||||||
|
|
||||||
@app.post(f'{APP_PREFIX}/api/deletePeerScheduleJob')
|
success, result = AllPeerJobs.saveJob(PeerJob(
|
||||||
|
jobID, data['Configuration'], peerKey, data['Field'], data['Operator'], data['Value'],
|
||||||
|
datetime.now(), data.get('ExpireDate'), data['Action']))
|
||||||
|
if success:
|
||||||
|
return ResponseObject(success, data=result)
|
||||||
|
return ResponseObject(success, message=result)
|
||||||
|
|
||||||
|
@app.put(f'{APP_PREFIX}/api/PeerScheduleJob')
|
||||||
|
@require_fields('JobID', 'Configuration', 'Peer', 'Field', 'Operator', 'Value', 'Action')
|
||||||
|
def API_updatePeerScheduleJob():
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
configuration = WireguardConfigurations.get(data['Configuration'])
|
||||||
|
if configuration is None:
|
||||||
|
return ResponseObject(False, "Configuration does not exist", status_code=404)
|
||||||
|
|
||||||
|
peerKey = unquote(data['Peer'])
|
||||||
|
found, _ = configuration.searchPeer(peerKey)
|
||||||
|
if not found:
|
||||||
|
return ResponseObject(False, "Peer does not exist", status_code=404)
|
||||||
|
|
||||||
|
existing = AllPeerJobs.searchJobById(data['JobID'])
|
||||||
|
if len(existing) == 0:
|
||||||
|
return ResponseObject(False, "Job does not exist", status_code=404)
|
||||||
|
|
||||||
|
success, result = AllPeerJobs.saveJob(PeerJob(
|
||||||
|
data['JobID'], data['Configuration'], peerKey, data['Field'], data['Operator'], data['Value'],
|
||||||
|
datetime.now(), data.get('ExpireDate'), data['Action']))
|
||||||
|
if success:
|
||||||
|
return ResponseObject(success, data=result)
|
||||||
|
return ResponseObject(success, message=result)
|
||||||
|
|
||||||
|
@app.delete(f'{APP_PREFIX}/api/PeerScheduleJob')
|
||||||
|
@require_fields('JobID', 'Configuration', 'Peer')
|
||||||
def API_deletePeerScheduleJob():
|
def API_deletePeerScheduleJob():
|
||||||
data = request.json
|
data = request.json
|
||||||
if "Job" not in data.keys():
|
|
||||||
return ResponseObject(False, "Please specify job")
|
configuration = WireguardConfigurations.get(data['Configuration'])
|
||||||
job: dict = data['Job']
|
|
||||||
if "Peer" not in job.keys() or "Configuration" not in job.keys():
|
|
||||||
return ResponseObject(False, "Please specify peer and configuration")
|
|
||||||
configuration = WireguardConfigurations.get(job['Configuration'])
|
|
||||||
if configuration is None:
|
if configuration is None:
|
||||||
return ResponseObject(False, "Configuration does not exist")
|
return ResponseObject(False, "Configuration does not exist", status_code=404)
|
||||||
# f, fp = configuration.searchPeer(job['Peer'])
|
|
||||||
# if not f:
|
|
||||||
# return ResponseObject(False, "Peer does not exist")
|
|
||||||
|
|
||||||
s, p = AllPeerJobs.deleteJob(PeerJob(
|
peerKey = unquote(data['Peer'])
|
||||||
job['JobID'], job['Configuration'], job['Peer'], job['Field'], job['Operator'], job['Value'],
|
success, result = AllPeerJobs.deleteJob(PeerJob(
|
||||||
job['CreationDate'], job['ExpireDate'], job['Action']))
|
data['JobID'], data['Configuration'], peerKey, data.get('Field', ''),
|
||||||
if s:
|
data.get('Operator', ''), data.get('Value', ''),
|
||||||
return ResponseObject(s)
|
datetime.now(), data.get('ExpireDate'), data.get('Action', '')))
|
||||||
return ResponseObject(s, message=p)
|
if success:
|
||||||
|
return ResponseObject(success, message="Job deleted successfully")
|
||||||
|
return ResponseObject(success, message=result)
|
||||||
|
|
||||||
@app.get(f'{APP_PREFIX}/api/getPeerScheduleJobLogs/<configName>')
|
@app.get(f'{APP_PREFIX}/api/PeerScheduleJobLogs/<configName>')
|
||||||
def API_getPeerScheduleJobLogs(configName):
|
def API_getPeerScheduleJobLogs(configName):
|
||||||
if configName not in WireguardConfigurations.keys():
|
if configName not in WireguardConfigurations.keys():
|
||||||
return ResponseObject(False, "Configuration does not exist")
|
return ResponseObject(False, "Configuration does not exist")
|
||||||
|
|||||||
@@ -32,11 +32,15 @@ class AmneziaConfiguration(WireguardConfiguration):
|
|||||||
self.H2 = 2
|
self.H2 = 2
|
||||||
self.H3 = 3
|
self.H3 = 3
|
||||||
self.H4 = 4
|
self.H4 = 4
|
||||||
self.I1 = "0"
|
self.I1 = ""
|
||||||
self.I2 = "0"
|
self.I2 = ""
|
||||||
self.I3 = "0"
|
self.I3 = ""
|
||||||
self.I4 = "0"
|
self.I4 = ""
|
||||||
self.I5 = "0"
|
self.I5 = ""
|
||||||
|
self.J1 = ""
|
||||||
|
self.J2 = ""
|
||||||
|
self.J3 = ""
|
||||||
|
self.Itime = ""
|
||||||
|
|
||||||
super().__init__(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, name, data, backup, startup, wg=False)
|
super().__init__(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, name, data, backup, startup, wg=False)
|
||||||
|
|
||||||
@@ -79,7 +83,11 @@ class AmneziaConfiguration(WireguardConfiguration):
|
|||||||
"I2": self.I2,
|
"I2": self.I2,
|
||||||
"I3": self.I3,
|
"I3": self.I3,
|
||||||
"I4": self.I4,
|
"I4": self.I4,
|
||||||
"I5": self.I5
|
"I5": self.I5,
|
||||||
|
"J1": self.J1,
|
||||||
|
"J2": self.J2,
|
||||||
|
"J3": self.J3,
|
||||||
|
"Itime": self.Itime
|
||||||
}
|
}
|
||||||
|
|
||||||
def createDatabase(self, dbName = None):
|
def createDatabase(self, dbName = None):
|
||||||
|
|||||||
@@ -116,5 +116,5 @@ class AmneziaPeer(Peer):
|
|||||||
self.configuration.getPeers()
|
self.configuration.getPeers()
|
||||||
return True, None
|
return True, None
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode("UTF-8")}")
|
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode('UTF-8')}")
|
||||||
return False, "Internal server error"
|
return False, "Internal server error"
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ class DashboardConfig:
|
|||||||
"peer_display_mode": "grid",
|
"peer_display_mode": "grid",
|
||||||
"remote_endpoint": GetRemoteEndpoint(),
|
"remote_endpoint": GetRemoteEndpoint(),
|
||||||
"peer_MTU": "1420",
|
"peer_MTU": "1420",
|
||||||
"peer_keep_alive": "21"
|
"peer_keep_alive": "21",
|
||||||
|
"peer_preshared_key_default": "false"
|
||||||
},
|
},
|
||||||
"Other": {
|
"Other": {
|
||||||
"welcome_session": "true"
|
"welcome_session": "true"
|
||||||
@@ -146,7 +147,10 @@ class DashboardConfig:
|
|||||||
if col_name not in existing_columns:
|
if col_name not in existing_columns:
|
||||||
type_str = col_type().compile(dialect=self.engine.dialect)
|
type_str = col_type().compile(dialect=self.engine.dialect)
|
||||||
current_app.logger.info(f"Adding missing column '{col_name}' to table '{table_name}'")
|
current_app.logger.info(f"Adding missing column '{col_name}' to table '{table_name}'")
|
||||||
conn.execute(db.text(f'ALTER TABLE "{table_name}" ADD COLUMN "{col_name}" {type_str}'))
|
preparer = self.engine.dialect.identifier_preparer
|
||||||
|
quoted_table = preparer.quote_identifier(table_name)
|
||||||
|
quoted_column = preparer.quote_identifier(col_name)
|
||||||
|
conn.execute(db.text(f"ALTER TABLE {quoted_table} ADD COLUMN {quoted_column} {type_str}"))
|
||||||
|
|
||||||
def getConnectionString(self, database) -> str or None:
|
def getConnectionString(self, database) -> str or None:
|
||||||
sqlitePath = os.path.join(DashboardConfig.ConfigurationPath, "db")
|
sqlitePath = os.path.join(DashboardConfig.ConfigurationPath, "db")
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class Peer:
|
|||||||
)
|
)
|
||||||
return True, None
|
return True, None
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode("UTF-8")}")
|
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode('UTF-8')}")
|
||||||
return False, "Internal server error"
|
return False, "Internal server error"
|
||||||
|
|
||||||
def downloadPeer(self) -> dict[str, str]:
|
def downloadPeer(self) -> dict[str, str]:
|
||||||
@@ -206,7 +206,11 @@ class Peer:
|
|||||||
"I2": self.configuration.I2,
|
"I2": self.configuration.I2,
|
||||||
"I3": self.configuration.I3,
|
"I3": self.configuration.I3,
|
||||||
"I4": self.configuration.I4,
|
"I4": self.configuration.I4,
|
||||||
"I5": self.configuration.I5
|
"I5": self.configuration.I5,
|
||||||
|
"J1": self.configuration.J1,
|
||||||
|
"J2": self.configuration.J2,
|
||||||
|
"J3": self.configuration.J3,
|
||||||
|
"Itime": self.configuration.Itime
|
||||||
})
|
})
|
||||||
|
|
||||||
peerSection = {
|
peerSection = {
|
||||||
|
|||||||
@@ -109,23 +109,31 @@ class WireguardConfiguration:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.Protocol == 'awg':
|
if self.Protocol == 'awg':
|
||||||
self.__parser["Interface"]["Jc"] = self.Jc
|
values = {
|
||||||
self.__parser["Interface"]["Jc"] = self.Jc
|
"Jc": self.Jc,
|
||||||
self.__parser["Interface"]["Jmin"] = self.Jmin
|
"Jmin": self.Jmin,
|
||||||
self.__parser["Interface"]["Jmax"] = self.Jmax
|
"Jmax": self.Jmax,
|
||||||
self.__parser["Interface"]["S1"] = self.S1
|
"S1": self.S1,
|
||||||
self.__parser["Interface"]["S2"] = self.S2
|
"S2": self.S2,
|
||||||
self.__parser["Interface"]["S3"] = self.S3
|
"S3": self.S3,
|
||||||
self.__parser["Interface"]["S4"] = self.S4
|
"S4": self.S4,
|
||||||
self.__parser["Interface"]["H1"] = self.H1
|
"H1": self.H1,
|
||||||
self.__parser["Interface"]["H2"] = self.H2
|
"H2": self.H2,
|
||||||
self.__parser["Interface"]["H3"] = self.H3
|
"H3": self.H3,
|
||||||
self.__parser["Interface"]["H4"] = self.H4
|
"H4": self.H4,
|
||||||
self.__parser["Interface"]["I1"] = self.I1
|
"I1": self.I1,
|
||||||
self.__parser["Interface"]["I2"] = self.I2
|
"I2": self.I2,
|
||||||
self.__parser["Interface"]["I3"] = self.I3
|
"I3": self.I3,
|
||||||
self.__parser["Interface"]["I4"] = self.I4
|
"I4": self.I4,
|
||||||
self.__parser["Interface"]["I5"] = self.I5
|
"I5": self.I5,
|
||||||
|
"J1": self.J1,
|
||||||
|
"J2": self.J2,
|
||||||
|
"J3": self.J3,
|
||||||
|
"Itime": self.Itime
|
||||||
|
}
|
||||||
|
for key, value in values.items():
|
||||||
|
if value != None and str(value).strip():
|
||||||
|
self.__parser["Interface"][key] = str(value)
|
||||||
|
|
||||||
if "Backup" not in data.keys():
|
if "Backup" not in data.keys():
|
||||||
self.createDatabase()
|
self.createDatabase()
|
||||||
@@ -1007,8 +1015,10 @@ class WireguardConfiguration:
|
|||||||
with open(self.configPath, 'r') as f:
|
with open(self.configPath, 'r') as f:
|
||||||
original = [l.rstrip("\n") for l in f.readlines()]
|
original = [l.rstrip("\n") for l in f.readlines()]
|
||||||
allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"]
|
allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"]
|
||||||
|
awgKeys = []
|
||||||
if self.Protocol == 'awg':
|
if self.Protocol == 'awg':
|
||||||
allowEdit += ["Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5"]
|
awgKeys = ["Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5", "J1", "J2", "J3", "Itime"]
|
||||||
|
allowEdit += awgKeys
|
||||||
start = original.index("[Interface]")
|
start = original.index("[Interface]")
|
||||||
try:
|
try:
|
||||||
end = original.index("[Peer]")
|
end = original.index("[Peer]")
|
||||||
@@ -1022,7 +1032,10 @@ class WireguardConfiguration:
|
|||||||
if split[0] not in allowEdit:
|
if split[0] not in allowEdit:
|
||||||
new.append(original[line])
|
new.append(original[line])
|
||||||
for key in allowEdit:
|
for key in allowEdit:
|
||||||
new.insert(1, f"{key} = {str(newData[key]).strip()}")
|
val = str(newData.get(key, "")).strip()
|
||||||
|
if key in awgKeys and val == "":
|
||||||
|
continue
|
||||||
|
new.insert(1, f"{key} = {val}")
|
||||||
new.append("")
|
new.append("")
|
||||||
for line in range(end, len(original)):
|
for line in range(end, len(original)):
|
||||||
new.append(original[line])
|
new.append(original[line])
|
||||||
@@ -1072,6 +1085,11 @@ class WireguardConfiguration:
|
|||||||
doRenameStatement("_restrict_access")
|
doRenameStatement("_restrict_access")
|
||||||
doRenameStatement("_deleted")
|
doRenameStatement("_deleted")
|
||||||
doRenameStatement("_transfer")
|
doRenameStatement("_transfer")
|
||||||
|
conn.execute(
|
||||||
|
self.infoTable.update()
|
||||||
|
.where(self.infoTable.c.ID == self.Name)
|
||||||
|
.values(ID=newConfigurationName)
|
||||||
|
)
|
||||||
|
|
||||||
self.AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName)
|
self.AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ icmplib==3.0.4
|
|||||||
gunicorn==25.0.3
|
gunicorn==25.0.3
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
tcconfig==0.30.1
|
tcconfig==0.30.1
|
||||||
sqlalchemy==2.0.46
|
sqlalchemy==2.0.49
|
||||||
sqlalchemy_utils==0.42.1
|
sqlalchemy_utils==0.42.1
|
||||||
psycopg[binary]==3.3.3
|
psycopg[binary]==3.3.4
|
||||||
PyMySQL==1.1.2
|
PyMySQL==1.1.2
|
||||||
tzlocal==5.3.1
|
tzlocal==5.3.1
|
||||||
python-jose==3.5.0
|
python-jose==3.5.0
|
||||||
pydantic==2.12.5
|
pydantic==2.13.3
|
||||||
|
|||||||
1753
src/static/app/package-lock.json
generated
1753
src/static/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,35 +13,35 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/language-server": "2.4.28",
|
"@volar/language-server": "2.4.28",
|
||||||
"@vue/language-server": "3.2.4",
|
"@vue/language-server": "3.2.7",
|
||||||
"@vuepic/vue-datepicker": "^12.1.0",
|
"@vuepic/vue-datepicker": "^12.1.0",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
"@vueuse/shared": "^14.1.0",
|
"@vueuse/shared": "^14.1.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
"cidr-tools": "^11.3.2",
|
"cidr-tools": "^11.3.3",
|
||||||
"css-color-converter": "^2.0.0",
|
"css-color-converter": "^2.0.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"electron-builder": "^26.7.0",
|
"electron-builder": "^26.7.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.3.0",
|
||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
"is-cidr": "^6.0.3",
|
"is-cidr": "^6.0.3",
|
||||||
"npm": "^11.8.0",
|
"npm": "^11.8.0",
|
||||||
"ol": "^10.7.0",
|
"ol": "^10.9.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"qrcodejs": "^1.0.0",
|
"qrcodejs": "^1.0.0",
|
||||||
"simple-code-editor": "^2.0.9",
|
"simple-code-editor": "^2.0.9",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^14.0.0",
|
||||||
"vue": "^3.5.31",
|
"vue": "^3.5.32",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.4",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"vite": "^7.3.1"
|
"vite": "^8.0.9"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"tar": "^7.5.6"
|
"tar": "^7.5.6"
|
||||||
|
|||||||
@@ -6,18 +6,27 @@ export default {
|
|||||||
components: {LocaleText},
|
components: {LocaleText},
|
||||||
props: {
|
props: {
|
||||||
data: Object,
|
data: Object,
|
||||||
saving: Boolean
|
saving: Boolean,
|
||||||
|
defaultEnabled: Boolean
|
||||||
},
|
},
|
||||||
data(){
|
data(){
|
||||||
return{
|
return{
|
||||||
enable: false
|
enable: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
const hasKey = !!(this.data && this.data.preshared_key && this.data.preshared_key.length > 0)
|
||||||
|
if (hasKey || this.defaultEnabled){
|
||||||
|
this.enable = true
|
||||||
|
}
|
||||||
|
},
|
||||||
watch:{
|
watch:{
|
||||||
enable(){
|
enable(){
|
||||||
if (this.enable){
|
if (this.enable) {
|
||||||
this.data.preshared_key = window.wireguard.generateKeypair().presharedKey
|
if (!this.data.preshared_key){
|
||||||
}else {
|
this.data.preshared_key = window.wireguard.generateKeypair().presharedKey
|
||||||
|
}
|
||||||
|
} else {
|
||||||
this.data.preshared_key = ""
|
this.data.preshared_key = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const peerData = ref({
|
|||||||
keepalive: parseInt(dashboardStore.Configuration.Peers.peer_keep_alive),
|
keepalive: parseInt(dashboardStore.Configuration.Peers.peer_keep_alive),
|
||||||
mtu: parseInt(dashboardStore.Configuration.Peers.peer_mtu),
|
mtu: parseInt(dashboardStore.Configuration.Peers.peer_mtu),
|
||||||
preshared_key: "",
|
preshared_key: "",
|
||||||
preshared_key_bulkAdd: false,
|
preshared_key_bulkAdd: Boolean(dashboardStore.Configuration.Peers.peer_preshared_key_default),
|
||||||
allowed_ips_validation: true,
|
allowed_ips_validation: true,
|
||||||
})
|
})
|
||||||
const availableIp = ref([])
|
const availableIp = ref([])
|
||||||
@@ -128,7 +128,7 @@ watch(() => {
|
|||||||
<EndpointAllowedIps :saving="saving" :data="peerData"></EndpointAllowedIps>
|
<EndpointAllowedIps :saving="saving" :data="peerData"></EndpointAllowedIps>
|
||||||
<div class="row gy-3">
|
<div class="row gy-3">
|
||||||
<div class="col-sm" v-if="!peerData.bulkAdd">
|
<div class="col-sm" v-if="!peerData.bulkAdd">
|
||||||
<PresharedKeyInput :saving="saving" :data="peerData" :bulk="peerData.bulkAdd"></PresharedKeyInput>
|
<PresharedKeyInput :saving="saving" :data="peerData" :bulk="peerData.bulkAdd" :defaultEnabled="Boolean(dashboardStore.Configuration.Peers.peer_preshared_key_default)"></PresharedKeyInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default {
|
|||||||
keepalive: parseInt(this.dashboardStore.Configuration.Peers.peer_keep_alive),
|
keepalive: parseInt(this.dashboardStore.Configuration.Peers.peer_keep_alive),
|
||||||
mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu),
|
mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu),
|
||||||
preshared_key: "",
|
preshared_key: "",
|
||||||
preshared_key_bulkAdd: false,
|
preshared_key_bulkAdd: Boolean(this.dashboardStore.Configuration.Peers.peer_preshared_key_default),
|
||||||
},
|
},
|
||||||
availableIp: undefined,
|
availableIp: undefined,
|
||||||
availableIpSearchString: "",
|
availableIpSearchString: "",
|
||||||
@@ -133,7 +133,7 @@ export default {
|
|||||||
<hr class="mb-0 mt-2">
|
<hr class="mb-0 mt-2">
|
||||||
<div class="row gy-3">
|
<div class="row gy-3">
|
||||||
<div class="col-sm" v-if="!this.data.bulkAdd">
|
<div class="col-sm" v-if="!this.data.bulkAdd">
|
||||||
<PresharedKeyInput :saving="saving" :data="data" :bulk="this.data.bulkAdd"></PresharedKeyInput>
|
<PresharedKeyInput :saving="saving" :data="data" :bulk="this.data.bulkAdd" :defaultEnabled="Boolean(this.dashboardStore.Configuration.Peers.peer_preshared_key_default)"></PresharedKeyInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async fetchLog(){
|
async fetchLog(){
|
||||||
this.dataLoading = true;
|
this.dataLoading = true;
|
||||||
await fetchGet(`/api/getPeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => {
|
await fetchGet(`/api/PeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => {
|
||||||
this.data = res.data;
|
this.data = res.data;
|
||||||
this.logFetchTime = dayjs().format("YYYY-MM-DD HH:mm:ss")
|
this.logFetchTime = dayjs().format("YYYY-MM-DD HH:mm:ss")
|
||||||
this.dataLoading = false;
|
this.dataLoading = false;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue";
|
import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue";
|
||||||
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, fetchPut, fetchDelete} from "@/utilities/fetch.js";
|
||||||
import { VueDatePicker } from "@vuepic/vue-datepicker";
|
import { VueDatePicker } from "@vuepic/vue-datepicker";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import LocaleText from "@/components/text/localeText.vue";
|
import LocaleText from "@/components/text/localeText.vue";
|
||||||
@@ -46,9 +46,8 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
save(){
|
save(){
|
||||||
if (this.job.Field && this.job.Operator && this.job.Action && this.job.Value){
|
if (this.job.Field && this.job.Operator && this.job.Action && this.job.Value){
|
||||||
fetchPost(`/api/savePeerScheduleJob`, {
|
const fn = this.newJob ? fetchPost : fetchPut;
|
||||||
Job: this.job
|
fn(`/api/PeerScheduleJob`, this.job, (res) => {
|
||||||
}, (res) => {
|
|
||||||
if (res.status){
|
if (res.status){
|
||||||
this.edit = false;
|
this.edit = false;
|
||||||
this.store.newMessage("Server", "Peer job saved", "success")
|
this.store.newMessage("Server", "Peer job saved", "success")
|
||||||
@@ -84,9 +83,7 @@ export default {
|
|||||||
},
|
},
|
||||||
delete(){
|
delete(){
|
||||||
if(this.job.CreationDate){
|
if(this.job.CreationDate){
|
||||||
fetchPost(`/api/deletePeerScheduleJob`, {
|
fetchDelete(`/api/PeerScheduleJob`, this.job, (res) => {
|
||||||
Job: this.job
|
|
||||||
}, (res) => {
|
|
||||||
if (!res.status){
|
if (!res.status){
|
||||||
this.store.newMessage("Server", res.message, "danger")
|
this.store.newMessage("Server", res.message, "danger")
|
||||||
this.$emit('delete')
|
this.$emit('delete')
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import PeersDefaultSettingsInput from "@/components/settingsComponent/peersDefau
|
|||||||
targetData="peer_mtu" title="MTU"></PeersDefaultSettingsInput>
|
targetData="peer_mtu" title="MTU"></PeersDefaultSettingsInput>
|
||||||
<PeersDefaultSettingsInput
|
<PeersDefaultSettingsInput
|
||||||
targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
|
targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
|
||||||
|
<PeersDefaultSettingsInput
|
||||||
|
targetData="peer_preshared_key_default" title="Pre-Shared Key Default"></PeersDefaultSettingsInput>
|
||||||
<PeersDefaultSettingsInput
|
<PeersDefaultSettingsInput
|
||||||
targetData="remote_endpoint" title="Peer Remote Endpoint"
|
targetData="remote_endpoint" title="Peer Remote Endpoint"
|
||||||
:warning="true" warningText="This will be changed globally, and will be apply to all peer's QR code and configuration file."
|
:warning="true" warningText="This will be changed globally, and will be apply to all peer's QR code and configuration file."
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.value = this.store.Configuration.Peers[this.targetData];
|
this.value = this.store.Configuration.Peers[this.targetData];
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isBoolean(){
|
||||||
|
return typeof this.value === "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
methods:{
|
methods:{
|
||||||
async useValidation(){
|
async useValidation(){
|
||||||
if(this.changed){
|
if(this.changed){
|
||||||
@@ -67,7 +72,14 @@ export default {
|
|||||||
<LocaleText :t="this.title"></LocaleText>
|
<LocaleText :t="this.title"></LocaleText>
|
||||||
</small></strong>
|
</small></strong>
|
||||||
</label>
|
</label>
|
||||||
<input type="text" class="form-control"
|
<div v-if="isBoolean" class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
|
v-model="this.value"
|
||||||
|
:id="this.uuid"
|
||||||
|
@change="this.changed = true; useValidation()"
|
||||||
|
:disabled="this.updating">
|
||||||
|
</div>
|
||||||
|
<input v-else type="text" class="form-control"
|
||||||
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
|
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
|
||||||
:id="this.uuid"
|
:id="this.uuid"
|
||||||
v-model="this.value"
|
v-model="this.value"
|
||||||
|
|||||||
@@ -80,3 +80,49 @@ export const fetchPost = async (url, body, callback) => {
|
|||||||
router.push({path: '/signin'})
|
router.push({path: '/signin'})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fetchPut = async (url, body, callback) => {
|
||||||
|
await fetch(`${getUrl(url)}`, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}).then((x) => {
|
||||||
|
const store = DashboardConfigurationStore();
|
||||||
|
if (!x.ok){
|
||||||
|
if (x.status !== 200){
|
||||||
|
if (x.status === 401){
|
||||||
|
store.newMessage("WGDashboard", "Sign in session ended, please sign in again", "warning")
|
||||||
|
}
|
||||||
|
throw new Error(x.statusText)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return x.json()
|
||||||
|
}
|
||||||
|
}).then(x => callback ? callback(x) : undefined).catch(x => {
|
||||||
|
console.log("Error:", x)
|
||||||
|
router.push({path: '/signin'})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchDelete = async (url, body, callback) => {
|
||||||
|
await fetch(`${getUrl(url)}`, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}).then((x) => {
|
||||||
|
const store = DashboardConfigurationStore();
|
||||||
|
if (!x.ok){
|
||||||
|
if (x.status !== 200){
|
||||||
|
if (x.status === 401){
|
||||||
|
store.newMessage("WGDashboard", "Sign in session ended, please sign in again", "warning")
|
||||||
|
}
|
||||||
|
throw new Error(x.statusText)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return x.json()
|
||||||
|
}
|
||||||
|
}).then(x => callback ? callback(x) : undefined).catch(x => {
|
||||||
|
console.log("Error:", x)
|
||||||
|
router.push({path: '/signin'})
|
||||||
|
})
|
||||||
|
}
|
||||||
2761
src/static/client/package-lock.json
generated
2761
src/static/client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,20 +9,20 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.15.1",
|
||||||
"bootstrap": "^5.3.6",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"oidc-client-ts": "^3.2.1",
|
"oidc-client-ts": "^3.5.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.4",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^14.0.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"vite": "^6.2.4",
|
"vite": "^8.0.8",
|
||||||
"vite-plugin-vue-devtools": "^7.7.2"
|
"vite-plugin-vue-devtools": "^8.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/wgd.sh
12
src/wgd.sh
@@ -247,27 +247,19 @@ _checkWireguard(){
|
|||||||
|
|
||||||
|
|
||||||
_checkPythonVersion(){
|
_checkPythonVersion(){
|
||||||
version_pass=$($pythonExecutable -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 10) else print("0");')
|
version_pass=$($pythonExecutable -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 12) else print("0");')
|
||||||
version=$($pythonExecutable --version)
|
version=$($pythonExecutable --version)
|
||||||
if [ $version_pass == "1" ]
|
if [ $version_pass == "1" ]
|
||||||
then
|
then
|
||||||
printf "[WGDashboard] %s Found compatible version of Python. Will be using %s to install WGDashboard.\n" "$heavy_checkmark" "$($pythonExecutable --version)"
|
printf "[WGDashboard] %s Found compatible version of Python. Will be using %s to install WGDashboard.\n" "$heavy_checkmark" "$($pythonExecutable --version)"
|
||||||
return;
|
return;
|
||||||
elif python3.10 --version > /dev/null 2>&1
|
|
||||||
then
|
|
||||||
printf "[WGDashboard] %s Found Python 3.10. Will be using [python3.10] to install WGDashboard.\n" "$heavy_checkmark"
|
|
||||||
pythonExecutable="python3.10"
|
|
||||||
elif python3.11 --version > /dev/null 2>&1
|
|
||||||
then
|
|
||||||
printf "[WGDashboard] %s Found Python 3.11. Will be using [python3.11] to install WGDashboard.\n" "$heavy_checkmark"
|
|
||||||
pythonExecutable="python3.11"
|
|
||||||
elif python3.12 --version > /dev/null 2>&1
|
elif python3.12 --version > /dev/null 2>&1
|
||||||
then
|
then
|
||||||
printf "[WGDashboard] %s Found Python 3.12. Will be using [python3.12] to install WGDashboard.\n" "$heavy_checkmark"
|
printf "[WGDashboard] %s Found Python 3.12. Will be using [python3.12] to install WGDashboard.\n" "$heavy_checkmark"
|
||||||
pythonExecutable="python3.12"
|
pythonExecutable="python3.12"
|
||||||
else
|
else
|
||||||
printf "[WGDashboard] %s Could not find a compatible version of Python. Current Python is %s.\n" "$heavy_crossmark" "$version"
|
printf "[WGDashboard] %s Could not find a compatible version of Python. Current Python is %s.\n" "$heavy_crossmark" "$version"
|
||||||
printf "[WGDashboard] WGDashboard required Python 3.10, 3.11 or 3.12. Halting install now.\n"
|
printf "[WGDashboard] WGDashboard required Python 3.12 or above. Halting install now.\n"
|
||||||
kill $TOP_PID
|
kill $TOP_PID
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user