Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
9e59ad0ebc build(deps): bump pydantic from 2.13.0 to 2.13.3 in /src
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.13.0 to 2.13.3.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.13.0...v2.13.3)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:26:14 +00:00
9 changed files with 58 additions and 202 deletions

View File

@@ -213,75 +213,6 @@ 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.

View File

@@ -2,8 +2,6 @@ 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
@@ -70,21 +68,6 @@ 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
''' '''
@@ -1221,75 +1204,52 @@ 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/PeerScheduleJob') @app.post(f'{APP_PREFIX}/api/savePeerScheduleJob')
@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():
configuration = WireguardConfigurations.get(data['Configuration']) return ResponseObject(False, "Please specify job")
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", status_code=404) return ResponseObject(False, "Configuration does not exist")
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)
jobID = data.get('JobID', str(uuid4())) s, p = AllPeerJobs.saveJob(PeerJob(
if len(AllPeerJobs.searchJobById(jobID)) > 0: job['JobID'], job['Configuration'], job['Peer'], job['Field'], job['Operator'], job['Value'],
return ResponseObject(False, "Job already exists", status_code=409) job['CreationDate'], job['ExpireDate'], job['Action']))
if s:
return ResponseObject(s, data=p)
return ResponseObject(s, message=p)
success, result = AllPeerJobs.saveJob(PeerJob( @app.post(f'{APP_PREFIX}/api/deletePeerScheduleJob')
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():
configuration = WireguardConfigurations.get(data['Configuration']) return ResponseObject(False, "Please specify job")
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", status_code=404) return ResponseObject(False, "Configuration does not exist")
# f, fp = configuration.searchPeer(job['Peer'])
# if not f:
# return ResponseObject(False, "Peer does not exist")
peerKey = unquote(data['Peer']) s, p = AllPeerJobs.deleteJob(PeerJob(
success, result = AllPeerJobs.deleteJob(PeerJob( job['JobID'], job['Configuration'], job['Peer'], job['Field'], job['Operator'], job['Value'],
data['JobID'], data['Configuration'], peerKey, data.get('Field', ''), job['CreationDate'], job['ExpireDate'], job['Action']))
data.get('Operator', ''), data.get('Value', ''), if s:
datetime.now(), data.get('ExpireDate'), data.get('Action', ''))) return ResponseObject(s)
if success: return ResponseObject(s, message=p)
return ResponseObject(success, message="Job deleted successfully")
return ResponseObject(success, message=result)
@app.get(f'{APP_PREFIX}/api/PeerScheduleJobLogs/<configName>') @app.get(f'{APP_PREFIX}/api/getPeerScheduleJobLogs/<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")

View File

@@ -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"

View File

@@ -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]:

View File

@@ -14,4 +14,4 @@ psycopg[binary]==3.3.3
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.13.0 pydantic==2.13.3

View File

@@ -26,7 +26,7 @@ export default {
methods: { methods: {
async fetchLog(){ async fetchLog(){
this.dataLoading = true; this.dataLoading = true;
await fetchGet(`/api/PeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => { await fetchGet(`/api/getPeerScheduleJobLogs/${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;

View File

@@ -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, fetchPut, fetchDelete} from "@/utilities/fetch.js"; import {fetchPost} 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,8 +46,9 @@ 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){
const fn = this.newJob ? fetchPost : fetchPut; fetchPost(`/api/savePeerScheduleJob`, {
fn(`/api/PeerScheduleJob`, this.job, (res) => { Job: this.job
}, (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")
@@ -83,7 +84,9 @@ export default {
}, },
delete(){ delete(){
if(this.job.CreationDate){ if(this.job.CreationDate){
fetchDelete(`/api/PeerScheduleJob`, this.job, (res) => { fetchPost(`/api/deletePeerScheduleJob`, {
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')

View File

@@ -80,49 +80,3 @@ 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'})
})
}

View File

@@ -247,19 +247,27 @@ _checkWireguard(){
_checkPythonVersion(){ _checkPythonVersion(){
version_pass=$($pythonExecutable -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 12) else print("0");') version_pass=$($pythonExecutable -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 10) 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.12 or above. Halting install now.\n" printf "[WGDashboard] WGDashboard required Python 3.10, 3.11 or 3.12. Halting install now.\n"
kill $TOP_PID kill $TOP_PID
fi fi
} }