mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2026-04-22 20:56:18 +00:00
Compare commits
1 Commits
developmen
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e59ad0ebc |
@@ -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.
|
||||||
|
|||||||
110
src/dashboard.py
110
src/dashboard.py
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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'})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
12
src/wgd.sh
12
src/wgd.sh
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user