Compare commits

..

19 Commits

Author SHA1 Message Date
Donald Zou
1faabd37af Merge pull request #936 from WGDashboard/fix-752
Internal merge for Fix 752
2025-10-02 07:18:09 +08:00
jinndi
c18ec5c83c Docker: Use relative paths instead of absolute ones to ensure proper namespace handling when working behind a reverse proxy. (#934)
* Docker: Use relative paths instead of absolute ones to ensure proper namespace handling when working behind a reverse proxy.

* Fix: Progress Swap Memory percent
2025-09-29 12:14:52 +02:00
Daan Selen
d8c89056cb refac: rework the variable times, mostly in ConnectionString 2025-09-23 09:08:21 +02:00
DaanSelen
8b541229d8 refac: make WGDashboard logging level configurable (#915)
* feat: init configurable logging level

* refac: correct some logging functions to debug from info. They were not that informational

* fix: logging somehow was broken due to disablement of external loggers

* refac: set default to info

---------

Co-authored-by: Daan Selen <dselen@systemec.nl>
2025-09-22 18:20:22 +02:00
Daan Selen
b7e65f7caf fix: initial run import massacre 2025-09-22 14:04:11 +02:00
Daan Selen
2eda18f18d refac: correct log output for the conditions checked 2025-09-22 13:35:53 +02:00
Daan Selen
d06709aeab Merge branch 'main' into v4.3.1-dev 2025-09-22 13:29:06 +02:00
Donald Zou
3de22ed2d6 Merge pull request #921 from WGDashboard/minor-patch-save-config 2025-09-22 17:54:40 +08:00
Donald Zou
10a8d22efd Merge pull request #922 from WGDashboard/docker-duplicate-hotfix
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docker Build and Push / docker_build (push) Has been cancelled
Docker Build and Push / docker_scan (push) Has been cancelled
2025-09-22 17:53:34 +08:00
Donald Zou
fc3ec61373 Merge pull request #923 from WGDashboard/remove-docker-funcs
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docker Build and Push / docker_build (push) Has been cancelled
Docker Build and Push / docker_scan (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2025-09-22 05:46:38 +08:00
Daan Selen
094d1c0718 refac: remove docker functions 2025-09-21 22:04:59 +02:00
Daan Selen
0d814ec03c refac: new logic to detecting a Wireguard interface 2025-09-21 21:57:28 +02:00
Daan Selen
a9036590a9 refac: save config before accessing it 2025-09-21 21:51:59 +02:00
dependabot[bot]
8f8843d449 Bump is-cidr from 5.1.1 to 6.0.0 in /src/static/app (#903)
Bumps [is-cidr](https://github.com/silverwind/is-cidr) from 5.1.1 to 6.0.0.
- [Release notes](https://github.com/silverwind/is-cidr/releases)
- [Commits](https://github.com/silverwind/is-cidr/compare/5.1.1...6.0.0)

---
updated-dependencies:
- dependency-name: is-cidr
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 19:52:39 +02:00
dependabot[bot]
6a21702768 Bump uuid from 11.1.0 to 13.0.0 in /src/static/app (#904)
Bumps [uuid](https://github.com/uuidjs/uuid) from 11.1.0 to 13.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v11.1.0...v13.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 19:52:10 +02:00
dependabot[bot]
5ccfe07e12 Bump npm from 10.9.3 to 11.6.0 in /src/static/app (#901)
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docker Build and Push / docker_build (push) Has been cancelled
Docker Build and Push / docker_scan (push) Has been cancelled
Bumps [npm](https://github.com/npm/cli) from 10.9.3 to 11.6.0.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v10.9.3...v11.6.0)

---
updated-dependencies:
- dependency-name: npm
  dependency-version: 11.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 17:30:31 +02:00
Donald Zou
56552d0a83 Merge pull request #917 from WGDashboard/db_prefix_add
feat: add configurable database prefix
2025-09-21 02:44:48 +08:00
Daan Selen
bd17b9080a fix: issue where I accidentally did the same name twice 2025-09-19 20:56:00 +02:00
Daan Selen
bf28983229 feat: make the prefix configurable 2025-09-19 20:55:47 +02:00
24 changed files with 732 additions and 897 deletions

2
.gitignore vendored
View File

@@ -6,6 +6,7 @@ __pycache__
src/test.py src/test.py
*.db *.db
src/wg-dashboard.ini src/wg-dashboard.ini
src/ssl-tls.ini
src/static/pic.xd src/static/pic.xd
*.conf *.conf
private_key.txt private_key.txt
@@ -19,7 +20,6 @@ node_modules/**
*/proxy.js */proxy.js
src/static/app/proxy.js src/static/app/proxy.js
.secrets .secrets
*.pid
# Logs # Logs
logs logs

View File

@@ -104,6 +104,7 @@ ARG wg_port="51820"
ENV TZ="Europe/Amsterdam" \ ENV TZ="Europe/Amsterdam" \
global_dns="9.9.9.9" \ global_dns="9.9.9.9" \
wgd_port="10086" \ wgd_port="10086" \
log_level="INFO" \
public_ip="" \ public_ip="" \
WGDASH=/opt/wgdashboard WGDASH=/opt/wgdashboard
@@ -129,6 +130,15 @@ SaveConfig = true\n\
DNS = ${global_dns}" > /configs/wg0.conf.template \ DNS = ${global_dns}" > /configs/wg0.conf.template \
&& chmod 600 /configs/wg0.conf.template && chmod 600 /configs/wg0.conf.template
# Replacement of paths with relative ones for namespace handling when using a reverse proxy
RUN set -ex && \
find ${WGDASH}/src/static/dist -type f \( -name "*.html" -o -name "*.js" \) \
-exec sed -i 's|/static/dist|./static/dist|g' {} + && \
find ${WGDASH}/src/static/dist -type f -name "*.css" \
-exec sed -i 's|/static/dist/WGDashboardClient/assets/|./|g' {} + && \
find ${WGDASH}/src/static/dist -type f -name "*.css" \
-exec sed -i 's|/static/dist/WGDashboardAdmin/assets/|./|g' {} +
# Set a healthcheck to determine the container its health # Set a healthcheck to determine the container its health
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD sh -c 'pgrep gunicorn > /dev/null && pgrep tail > /dev/null' || exit 1 CMD sh -c 'pgrep gunicorn > /dev/null && pgrep tail > /dev/null' || exit 1

View File

@@ -102,6 +102,7 @@ Updating the WGDashboard container should be through 'The Docker Way' - by pulli
| `global_dns` | IPv4 and IPv6 addresses | `9.9.9.9` | `8.8.8.8`, `1.1.1.1` | Default DNS for WireGuard clients. | | `global_dns` | IPv4 and IPv6 addresses | `9.9.9.9` | `8.8.8.8`, `1.1.1.1` | Default DNS for WireGuard clients. |
| `public_ip` | Public IP address | Retrieved automatically | `253.162.134.73` | Used to generate accurate client configs. Needed if container is NATd. | | `public_ip` | Public IP address | Retrieved automatically | `253.162.134.73` | Used to generate accurate client configs. Needed if container is NATd. |
| `wgd_port` | Any port that is allowed for the process | `10086` | `443` | This port is used to set the WGDashboard web port. | | `wgd_port` | Any port that is allowed for the process | `10086` | `443` | This port is used to set the WGDashboard web port. |
| `log_level` | `DEBUG`, `INFO`, `WARNING`, `ERROR` and `CRITICAL` | `INFO` | `WARNING` | Sets the severity of the logs being displayed. |
| `username` | Any nonempty string | `-` | `admin` | Username for the WGDashboard web interface account. | | `username` | Any nonempty string | `-` | `admin` | Username for the WGDashboard web interface account. |
| `password` | Any nonempty string | `-` | `s3cr3tP@ss` | Password for the WGDashboard web interface account (stored hashed). | | `password` | Any nonempty string | `-` | `s3cr3tP@ss` | Password for the WGDashboard web interface account (stored hashed). |
| `enable_totp` | `true`, `false` | `true` | `false` | Enable TOTPbased twofactor authentication for the account. | | `enable_totp` | `true`, `false` | `true` | `false` | Enable TOTPbased twofactor authentication for the account. |

View File

@@ -12,7 +12,8 @@ services:
# Environment variables can be used to configure certain values at startup. Without having to configure it from the dashboard. # Environment variables can be used to configure certain values at startup. Without having to configure it from the dashboard.
# By default its all disabled, but uncomment the following lines to apply these. (uncommenting is removing the # character) # By default its all disabled, but uncomment the following lines to apply these. (uncommenting is removing the # character)
# Refer to the documentation on https://wgdashboard.dev/ for more info on what everything means. # Refer to the documentation on https://wgdashboard.dev/ for more info on what everything means.
#environment: environment:
- log_level=WARNING
#- tz= # <--- Set container timezone, default: Europe/Amsterdam. #- tz= # <--- Set container timezone, default: Europe/Amsterdam.
#- public_ip= # <--- Set public IP to ensure the correct one is chosen, defaulting to the IP give by ifconfig.me. #- public_ip= # <--- Set public IP to ensure the correct one is chosen, defaulting to the IP give by ifconfig.me.
#- wgd_port= # <--- Set the port WGDashboard will use for its web-server. #- wgd_port= # <--- Set the port WGDashboard will use for its web-server.

View File

@@ -105,7 +105,7 @@ ensure_installation() {
echo "Looks like the installation succeeded. Moving on." echo "Looks like the installation succeeded. Moving on."
# Setup WireGuard if needed # Setup WireGuard if needed
if [ ! -f "/etc/wireguard/wg0.conf" ]; then if [ -z "$(ls -A /etc/wireguard)" ]; then
cp -a "/configs/wg0.conf.template" "/etc/wireguard/wg0.conf" cp -a "/configs/wg0.conf.template" "/etc/wireguard/wg0.conf"
echo "Setting a secure private key." echo "Setting a secure private key."
@@ -115,7 +115,7 @@ ensure_installation() {
echo "Done setting template." echo "Done setting template."
else else
echo "Existing wg0 configuration file found, using that." echo "Existing Wireguard configuration file found in /etc/wireguard."
fi fi
} }
@@ -137,6 +137,7 @@ set_envvars() {
set_ini Peers remote_endpoint "${public_ip}" set_ini Peers remote_endpoint "${public_ip}"
set_ini Server app_port "${wgd_port}" set_ini Server app_port "${wgd_port}"
set_ini Server log_level "${log_level}"
# Account settings - process all parameters # Account settings - process all parameters
[[ -n "$username" ]] && echo "Configuring user account:" [[ -n "$username" ]] && echo "Configuring user account:"

View File

@@ -1,45 +1,27 @@
# --- Standard library imports ---
import configparser
import hashlib
import ipaddress
import json
import logging import logging
import os import random, shutil, sqlite3, configparser, hashlib, ipaddress, json, os, secrets, subprocess
import random import time, re, uuid, bcrypt, psutil, pyotp, threading
import re
import secrets
import shutil
import sqlite3
import subprocess
import threading
import time
import traceback import traceback
import uuid
from datetime import datetime, timedelta
from itertools import islice
from uuid import uuid4 from uuid import uuid4
from zipfile import ZipFile from zipfile import ZipFile
from datetime import datetime, timedelta
# --- Third-party imports ---
import bcrypt
import psutil
import pyotp
import sqlalchemy import sqlalchemy
from flask import Flask, request, render_template, session, send_file, Response
from flask_cors import CORS
from flask.json.provider import DefaultJSONProvider
from icmplib import ping, traceroute
from jinja2 import Template from jinja2 import Template
from packaging import version from flask import Flask, request, render_template, session, send_file
from flask_cors import CORS
from icmplib import ping, traceroute
from flask.json.provider import DefaultJSONProvider
from itertools import islice
from sqlalchemy import RowMapping from sqlalchemy import RowMapping
# --- Local module imports ---
from client import createClientBlueprint
from modules.Utilities import ( from modules.Utilities import (
RegexMatch, StringToBoolean, RegexMatch, StringToBoolean,
ValidateIPAddressesWithRange, ValidateDNSAddress, ValidateIPAddressesWithRange, ValidateDNSAddress,
GenerateWireguardPublicKey, GenerateWireguardPrivateKey GenerateWireguardPublicKey, GenerateWireguardPrivateKey
) )
from packaging import version
from modules.Email import EmailSender from modules.Email import EmailSender
from modules.DashboardLogger import DashboardLogger from modules.DashboardLogger import DashboardLogger
from modules.PeerJob import PeerJob from modules.PeerJob import PeerJob
@@ -49,15 +31,16 @@ from modules.PeerJobs import PeerJobs
from modules.DashboardConfig import DashboardConfig from modules.DashboardConfig import DashboardConfig
from modules.WireguardConfiguration import WireguardConfiguration from modules.WireguardConfiguration import WireguardConfiguration
from modules.AmneziaWireguardConfiguration import AmneziaWireguardConfiguration from modules.AmneziaWireguardConfiguration import AmneziaWireguardConfiguration
from client import createClientBlueprint
from logging.config import dictConfig
from modules.DashboardClients import DashboardClients from modules.DashboardClients import DashboardClients
from modules.DashboardPlugins import DashboardPlugins from modules.DashboardPlugins import DashboardPlugins
from modules.DashboardWebHooks import DashboardWebHooks from modules.DashboardWebHooks import DashboardWebHooks
from modules.NewConfigurationTemplates import NewConfigurationTemplates from modules.NewConfigurationTemplates import NewConfigurationTemplates
# --- Logging configuration ---
from logging.config import dictConfig
class CustomJsonEncoder(DefaultJSONProvider): class CustomJsonEncoder(DefaultJSONProvider):
def __init__(self, app): def __init__(self, app):
super().__init__(app) super().__init__(app)
@@ -72,6 +55,7 @@ class CustomJsonEncoder(DefaultJSONProvider):
return super().default(self) return super().default(self)
''' '''
Response Object Response Object
''' '''
@@ -88,7 +72,6 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F
''' '''
Flask App Flask App
''' '''
app = Flask("WGDashboard", template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"))
def peerInformationBackgroundThread(): def peerInformationBackgroundThread():
global WireguardConfigurations global WireguardConfigurations
@@ -136,7 +119,8 @@ def peerJobScheduleBackgroundThread():
def gunicornConfig(): def gunicornConfig():
_, 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")
return app_ip, app_port
return app_ip, app_port, log_level
def ProtocolsEnabled() -> list[str]: def ProtocolsEnabled() -> list[str]:
from shutil import which from shutil import which
@@ -188,16 +172,7 @@ def startThreads():
scheduleJobThread = threading.Thread(target=peerJobScheduleBackgroundThread, daemon=True) scheduleJobThread = threading.Thread(target=peerJobScheduleBackgroundThread, daemon=True)
scheduleJobThread.start() scheduleJobThread.start()
dictConfig({ app = Flask("WGDashboard", template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"))
'version': 1,
'formatters': {'default': {
'format': '[%(asctime)s] [%(levelname)s] in [%(module)s] %(message)s',
}},
'root': {
'level': 'INFO'
}
})
WireguardConfigurations: dict[str, WireguardConfiguration] = {} WireguardConfigurations: dict[str, WireguardConfiguration] = {}
CONFIGURATION_PATH = os.getenv('CONFIGURATION_PATH', '.') CONFIGURATION_PATH = os.getenv('CONFIGURATION_PATH', '.')
@@ -205,127 +180,82 @@ CONFIGURATION_PATH = os.getenv('CONFIGURATION_PATH', '.')
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 5206928 app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 5206928
app.secret_key = secrets.token_urlsafe(32) app.secret_key = secrets.token_urlsafe(32)
app.json = CustomJsonEncoder(app) app.json = CustomJsonEncoder(app)
with app.app_context(): with app.app_context():
SystemStatus = SystemStatus() SystemStatus = SystemStatus()
DashboardConfig = DashboardConfig() DashboardConfig = DashboardConfig()
EmailSender = EmailSender(DashboardConfig) EmailSender = EmailSender(DashboardConfig)
AllPeerShareLinks: PeerShareLinks = PeerShareLinks(DashboardConfig, WireguardConfigurations) AllPeerShareLinks: PeerShareLinks = PeerShareLinks(DashboardConfig, WireguardConfigurations)
AllPeerJobs: PeerJobs = PeerJobs(DashboardConfig, WireguardConfigurations) AllPeerJobs: PeerJobs = PeerJobs(DashboardConfig, WireguardConfigurations)
DashboardLogger = DashboardLogger() DashboardLogger: DashboardLogger = DashboardLogger()
DashboardPlugins = DashboardPlugins(app, WireguardConfigurations) DashboardPlugins: DashboardPlugins = DashboardPlugins(app, WireguardConfigurations)
DashboardWebHooks = DashboardWebHooks(DashboardConfig) DashboardWebHooks: DashboardWebHooks = DashboardWebHooks(DashboardConfig)
NewConfigurationTemplates = NewConfigurationTemplates() NewConfigurationTemplates: NewConfigurationTemplates = NewConfigurationTemplates()
InitWireguardConfigurationsList(startup=True) InitWireguardConfigurationsList(startup=True)
DashboardClients = DashboardClients(WireguardConfigurations)
app.register_blueprint(createClientBlueprint(WireguardConfigurations, DashboardConfig, DashboardClients)) DashboardClients: DashboardClients = DashboardClients(WireguardConfigurations)
app.register_blueprint(
createClientBlueprint(WireguardConfigurations, DashboardConfig, DashboardClients)
)
_, APP_PREFIX = DashboardConfig.GetConfig("Server", "app_prefix") _, APP_PREFIX = DashboardConfig.GetConfig("Server", "app_prefix")
_, app_ip = DashboardConfig.GetConfig("Server", "app_ip")
_, app_port = DashboardConfig.GetConfig("Server", "app_port")
_, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
_, log_level = DashboardConfig.GetConfig("Server", "log_level")
cors = CORS(app, resources={rf"{APP_PREFIX}/api/*": { cors = CORS(app, resources={rf"{APP_PREFIX}/api/*": {
"origins": "*", "origins": "*",
"methods": "DELETE, POST, GET, OPTIONS", "methods": "DELETE, POST, GET, OPTIONS",
"allow_headers": ["Content-Type", "wg-dashboard-apikey"] "allow_headers": ["Content-Type", "wg-dashboard-apikey"]
}}) }})
_, app_ip = DashboardConfig.GetConfig("Server", "app_ip")
_, app_port = DashboardConfig.GetConfig("Server", "app_port")
_, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path")
app.logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
dictConfig({
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'default': {
'format': '[%(asctime)s] [%(levelname)s] in [%(module)s] %(message)s',
},
},
'handlers': {
'wsgi': {
'class': 'logging.StreamHandler',
'stream': 'ext://flask.logging.wsgi_errors_stream',
'formatter': 'default',
},
}
})
''' '''
API Routes API Routes
''' '''
def _enforce_session_auth():
"""Enforce session authentication for non-API key access."""
white_list = [
'/static/',
'validateAuthentication',
'authenticate',
'getDashboardConfiguration',
'getDashboardTheme',
'getDashboardVersion',
'sharePeer/get',
'isTotpEnabled',
'locale',
'/fileDownload',
'/client'
]
path_ok = (
("username" in session and session.get("role") == "admin")
or (f"{APP_PREFIX}/" == request.path or f"{APP_PREFIX}" == request.path)
or not all(sub not in request.path for sub in white_list)
)
if not path_ok:
response = Flask.make_response(app, {
"status": False,
"message": "Unauthorized access.",
"data": None
})
response.content_type = "application/json"
response.status_code = 401
return response
def _login_with_token(key):
auth_token = hashlib.sha256(f"{key}{datetime.now()}".encode()).hexdigest()
session.update({'role': 'admin', 'username': auth_token})
session.permanent = True
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
resp.set_cookie("authToken", auth_token)
return resp
def _login_with_credentials(data):
username = data.get('username')
password = data.get('password')
totp_code = data.get('totp')
valid_password = bcrypt.checkpw(password.encode("utf-8"),
DashboardConfig.GetConfig("Account", "password")[1].encode("utf-8"))
totp_enabled = DashboardConfig.GetConfig("Account", "enable_totp")[1]
totp_valid = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() == totp_code if totp_enabled else True
if username == DashboardConfig.GetConfig("Account", "username")[1] and valid_password and totp_valid:
auth_token = hashlib.sha256(f"{username}{datetime.now()}".encode()).hexdigest()
session.update({'role': 'admin', 'username': auth_token})
session.permanent = True
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login success: {username}")
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
resp.set_cookie("authToken", auth_token)
return resp
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login failed: {username}")
msg = "Sorry, your username, password or OTP is incorrect." if totp_enabled else "Sorry, your username or password is incorrect."
return ResponseObject(False, msg)
@app.before_request @app.before_request
def auth_req(): def auth_req():
# Skip preflight requests
if request.method.lower() == 'options': if request.method.lower() == 'options':
return ResponseObject(True) return ResponseObject(True)
DashboardConfig.APIAccessed = False DashboardConfig.APIAccessed = False
# Logging
if "api" in request.path: if "api" in request.path:
log_message = str(request.args) if request.method.upper() == "GET" else f"Request Args: {str(request.args)} Body:{str(request.get_json())}" if str(request.method) == "GET":
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=log_message) 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())}")
authentication_required = DashboardConfig.GetConfig("Server", "auth_req")[1]
headers = request.headers
if authentication_required: authenticationRequired = DashboardConfig.GetConfig("Server", "auth_req")[1]
api_key = headers.get('wg-dashboard-apikey') d = request.headers
api_key_enabled = DashboardConfig.GetConfig("Server", "dashboard_api_key")[1] if authenticationRequired:
apiKey = d.get('wg-dashboard-apikey')
# API key authentication apiKeyEnabled = DashboardConfig.GetConfig("Server", "dashboard_api_key")[1]
if api_key and api_key_enabled: if apiKey is not None and len(apiKey) > 0 and apiKeyEnabled:
api_key_exists = any(k.Key == api_key for k in DashboardConfig.DashboardAPIKeys) 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: {api_key_exists} - Key: {api_key}") 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 api_key_exists:
DashboardConfig.APIAccessed = False DashboardConfig.APIAccessed = False
response = Flask.make_response(app, { response = Flask.make_response(app, {
"status": False, "status": False,
@@ -335,49 +265,84 @@ def auth_req():
response.content_type = "application/json" response.content_type = "application/json"
response.status_code = 401 response.status_code = 401
return response return response
DashboardConfig.APIAccessed = True DashboardConfig.APIAccessed = True
else: else:
DashboardConfig.APIAccessed = False DashboardConfig.APIAccessed = False
_enforce_session_auth() whiteList = [
'/static/', 'validateAuthentication', 'authenticate', 'getDashboardConfiguration',
'getDashboardTheme', 'getDashboardVersion', 'sharePeer/get', 'isTotpEnabled', 'locale',
'/fileDownload',
'/client'
]
if (("username" not in session or session.get("role") != "admin")
and (f"{(APP_PREFIX if len(APP_PREFIX) > 0 else '')}/" != request.path
and f"{(APP_PREFIX if len(APP_PREFIX) > 0 else '')}" != request.path)
and len(list(filter(lambda x : x not in request.path, whiteList))) == len(whiteList)
):
response = Flask.make_response(app, {
"status": False,
"message": "Unauthorized access.",
"data": None
})
response.content_type = "application/json"
response.status_code = 401
return response
@app.route(f'{APP_PREFIX}/api/handshake', methods=["GET", "OPTIONS"]) @app.route(f'{APP_PREFIX}/api/handshake', methods=["GET", "OPTIONS"])
def API_Handshake(): def API_Handshake():
return ResponseObject(True) return ResponseObject(True)
@app.get(f'{APP_PREFIX}/api/validateAuthentication') @app.get(f'{APP_PREFIX}/api/validateAuthentication')
def API_ValidateAuthentication(): def API_ValidateAuthentication():
token = request.cookies.get("authToken") token = request.cookies.get("authToken")
auth_required = DashboardConfig.GetConfig("Server", "auth_req")[1] if DashboardConfig.GetConfig("Server", "auth_req")[1]:
if token is None or token == "" or "username" not in session or session["username"] != token:
if auth_required and (not token or "username" not in session or session["username"] != token): return ResponseObject(False, "Invalid authentication.")
return ResponseObject(False, "Invalid authentication.",
status_code=401)
return ResponseObject(True) return ResponseObject(True)
@app.get(f'{APP_PREFIX}/api/requireAuthentication') @app.get(f'{APP_PREFIX}/api/requireAuthentication')
def API_RequireAuthentication(): def API_RequireAuthentication():
return ResponseObject(data=DashboardConfig.GetConfig("Server", "auth_req")[1]) return ResponseObject(data=DashboardConfig.GetConfig("Server", "auth_req")[1])
@app.post(f'{APP_PREFIX}/api/authenticate') @app.post(f'{APP_PREFIX}/api/authenticate')
def API_AuthenticateLogin(): def API_AuthenticateLogin():
data = request.get_json() data = request.get_json()
auth_req = DashboardConfig.GetConfig("Server", "auth_req")[1] if not DashboardConfig.GetConfig("Server", "auth_req")[1]:
if not auth_req:
return ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1]) return ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
# API key login
if DashboardConfig.APIAccessed: if DashboardConfig.APIAccessed:
return _login_with_token(request.headers.get('wg-dashboard-apikey')) authToken = hashlib.sha256(f"{request.headers.get('wg-dashboard-apikey')}{datetime.now()}".encode()).hexdigest()
session['role'] = 'admin'
session['username'] = authToken
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
resp.set_cookie("authToken", authToken)
session.permanent = True
return resp
valid = bcrypt.checkpw(data['password'].encode("utf-8"),
DashboardConfig.GetConfig("Account", "password")[1].encode("utf-8"))
totpEnabled = DashboardConfig.GetConfig("Account", "enable_totp")[1]
totpValid = False
if totpEnabled:
totpValid = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() == data['totp']
# User login if (valid
return _login_with_credentials(data) and data['username'] == DashboardConfig.GetConfig("Account", "username")[1]
and ((totpEnabled and totpValid) or not totpEnabled)
):
authToken = hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest()
session['role'] = 'admin'
session['username'] = authToken
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
resp.set_cookie("authToken", authToken)
session.permanent = True
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login success: {data['username']}")
return resp
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Login failed: {data['username']}")
if totpEnabled:
return ResponseObject(False, "Sorry, your username, password or OTP is incorrect.")
else:
return ResponseObject(False, "Sorry, your username or password is incorrect.")
@app.get(f'{APP_PREFIX}/api/signout') @app.get(f'{APP_PREFIX}/api/signout')
def API_SignOut(): def API_SignOut():
@@ -389,7 +354,7 @@ def API_SignOut():
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurations') @app.get(f'{APP_PREFIX}/api/getWireguardConfigurations')
def API_getWireguardConfigurations(): def API_getWireguardConfigurations():
InitWireguardConfigurationsList() InitWireguardConfigurationsList()
return ResponseObject(data=list(WireguardConfigurations.values())) return ResponseObject(data=[wc for wc in WireguardConfigurations.values()])
@app.get(f'{APP_PREFIX}/api/newConfigurationTemplates') @app.get(f'{APP_PREFIX}/api/newConfigurationTemplates')
def API_NewConfigurationTemplates(): def API_NewConfigurationTemplates():
@@ -402,255 +367,193 @@ def API_NewConfigurationTemplates_CreateTemplate():
@app.post(f'{APP_PREFIX}/api/newConfigurationTemplates/updateTemplate') @app.post(f'{APP_PREFIX}/api/newConfigurationTemplates/updateTemplate')
def API_NewConfigurationTemplates_UpdateTemplate(): def API_NewConfigurationTemplates_UpdateTemplate():
data = request.get_json() data = request.get_json()
template = data.get('Template') template = data.get('Template', None)
if not template: if not template:
return ResponseObject(False, "Please provide template", return ResponseObject(False, "Please provide template")
status_code=400)
status, msg = NewConfigurationTemplates.UpdateTemplate(template) status, msg = NewConfigurationTemplates.UpdateTemplate(template)
return ResponseObject(status, msg) return ResponseObject(status, msg)
@app.post(f'{APP_PREFIX}/api/newConfigurationTemplates/deleteTemplate') @app.post(f'{APP_PREFIX}/api/newConfigurationTemplates/deleteTemplate')
def API_NewConfigurationTemplates_DeleteTemplate(): def API_NewConfigurationTemplates_DeleteTemplate():
data = request.get_json() data = request.get_json()
template = data.get('Template') template = data.get('Template', None)
if not template: if not template:
return ResponseObject(False, "Please provide template", return ResponseObject(False, "Please provide template")
status_code=400)
status, msg = NewConfigurationTemplates.DeleteTemplate(template) status, msg = NewConfigurationTemplates.DeleteTemplate(template)
return ResponseObject(status, msg) return ResponseObject(status, msg)
@app.post(f'{APP_PREFIX}/api/addWireguardConfiguration') @app.post(f'{APP_PREFIX}/api/addWireguardConfiguration')
def API_addWireguardConfiguration(): def API_addWireguardConfiguration():
data = request.get_json() data = request.get_json()
protocol = data.get("Protocol") requiredKeys = [
"ConfigurationName", "Address", "ListenPort", "PrivateKey", "Protocol"
]
for i in requiredKeys:
if i not in data.keys():
return ResponseObject(False, "Please provide all required parameters.")
required_keys = {"ConfigurationName", "Address", "ListenPort", "PrivateKey", "Protocol"} if data.get("Protocol") not in ProtocolsEnabled():
if not required_keys.issubset(data.keys()): return ResponseObject(False, "Please provide a valid protocol: wg / awg.")
return ResponseObject(False, "Please provide all required parameters.", status_code=400)
if protocol not in ProtocolsEnabled(): # Check duplicate names, ports, address
return ResponseObject(False, "Please provide a valid protocol: wg / awg.", status_code=400) for i in WireguardConfigurations.values():
if i.Name == data['ConfigurationName']:
return ResponseObject(False,
f"Already have a configuration with the name \"{data['ConfigurationName']}\"",
"ConfigurationName")
for cfg in WireguardConfigurations.values(): if str(i.ListenPort) == str(data["ListenPort"]):
duplicates = { return ResponseObject(False,
"ConfigurationName": cfg.Name == data['ConfigurationName'], f"Already have a configuration with the port \"{data['ListenPort']}\"",
"ListenPort": str(cfg.ListenPort) == str(data["ListenPort"]), "ListenPort")
"Address": cfg.Address == data["Address"]
if i.Address == data["Address"]:
return ResponseObject(False,
f"Already have a configuration with the address \"{data['Address']}\"",
"Address")
if "Backup" in data.keys():
path = {
"wg": DashboardConfig.GetConfig("Server", "wg_conf_path")[1],
"awg": DashboardConfig.GetConfig("Server", "awg_conf_path")[1]
} }
for key, is_duplicate in duplicates.items():
if is_duplicate:
return ResponseObject(
False,
f"Already have a configuration with the {key.lower()} \"{data[key]}\"",
key,
status_code=400
)
paths = { if (os.path.exists(os.path.join(path['wg'], 'WGDashboard_Backup', data["Backup"])) and
"wg": DashboardConfig.GetConfig("Server", "wg_conf_path")[1], os.path.exists(os.path.join(path['wg'], 'WGDashboard_Backup', data["Backup"].replace('.conf', '.sql')))):
"awg": DashboardConfig.GetConfig("Server", "awg_conf_path")[1] protocol = "wg"
} elif (os.path.exists(os.path.join(path['awg'], 'WGDashboard_Backup', data["Backup"])) and
os.path.exists(os.path.join(path['awg'], 'WGDashboard_Backup', data["Backup"].replace('.conf', '.sql')))):
if "Backup" in data: protocol = "awg"
backup_file = data["Backup"] else:
protocol_detected = None return ResponseObject(False, "Backup does not exist")
for proto, base_path in paths.items():
conf_path = os.path.join(base_path, 'WGDashboard_Backup', backup_file)
sql_path = os.path.join(base_path, 'WGDashboard_Backup', backup_file.replace('.conf', '.sql'))
if os.path.exists(conf_path) and os.path.exists(sql_path):
protocol_detected = proto
break
if not protocol_detected:
return ResponseObject(False, "Backup does not exist", status_code=400)
shutil.copy( shutil.copy(
os.path.join(paths[protocol_detected], 'WGDashboard_Backup', backup_file), os.path.join(path[protocol], 'WGDashboard_Backup', data["Backup"]),
os.path.join(paths[protocol_detected], f'{data["ConfigurationName"]}.conf') os.path.join(path[protocol], f'{data["ConfigurationName"]}.conf')
) )
protocol = protocol_detected # Use backup protocol WireguardConfigurations[data['ConfigurationName']] = (
WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) if protocol == 'wg' else (
AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName']))
else: else:
conf_path = os.path.join(paths[protocol], f'{data["ConfigurationName"]}.conf') WireguardConfigurations[data['ConfigurationName']] = (
if not os.path.exists(conf_path): WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) if data.get('Protocol') == 'wg' else (
with open(conf_path, 'w') as f: AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data))
f.write(
f"[Interface]\n"
f"Address = {data['Address']}\n"
f"ListenPort = {data['ListenPort']}\n"
f"PrivateKey = {data['PrivateKey']}\n"
)
os.chmod(conf_path, 0o600) # secure file permissions
ConfigClass = WireguardConfiguration if protocol == "wg" else AmneziaWireguardConfiguration
WireguardConfigurations[data['ConfigurationName']] = ConfigClass(
DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks,
data=data, name=data['ConfigurationName']
)
return ResponseObject() return ResponseObject()
@app.get(f'{APP_PREFIX}/api/toggleWireguardConfiguration') @app.get(f'{APP_PREFIX}/api/toggleWireguardConfiguration')
def API_toggleWireguardConfiguration(): def API_toggleWireguardConfiguration():
configuration_name = request.args.get('configurationName') configurationName = request.args.get('configurationName')
if configurationName is None or len(
if not configuration_name or configuration_name not in WireguardConfigurations: configurationName) == 0 or configurationName not in WireguardConfigurations.keys():
return ResponseObject(False, "Please provide a valid configuration name", return ResponseObject(False, "Please provide a valid configuration name", status_code=404)
status_code=404) toggleStatus, msg = WireguardConfigurations[configurationName].toggleConfiguration()
return ResponseObject(toggleStatus, msg, WireguardConfigurations[configurationName].Status)
target_configuration = WireguardConfigurations[configuration_name]
status, msg = target_configuration.toggleConfiguration()
configuration_status = target_configuration.Status
return ResponseObject(status, msg, configuration_status)
@app.post(f'{APP_PREFIX}/api/updateWireguardConfiguration') @app.post(f'{APP_PREFIX}/api/updateWireguardConfiguration')
def API_updateWireguardConfiguration(): def API_updateWireguardConfiguration():
data = request.get_json() or {} data = request.get_json()
requiredKeys = ["Name"]
for i in requiredKeys:
if i not in data.keys():
return ResponseObject(False, "Please provide these following field: " + ", ".join(requiredKeys))
name = data.get("Name") name = data.get("Name")
if name not in WireguardConfigurations.keys():
return ResponseObject(False, "Configuration does not exist", status_code=404)
if not name: status, msg = WireguardConfigurations[name].updateConfigurationSettings(data)
return ResponseObject(False, "Please provide the field: Name",
status_code=400)
if name not in WireguardConfigurations:
return ResponseObject(False, "Configuration does not exist",
status_code=404)
target_configuration = WireguardConfigurations[name]
status, msg = target_configuration.updateConfigurationSettings(data)
return ResponseObject(status, msg, target_configuration)
return ResponseObject(status, message=msg, data=WireguardConfigurations[name])
@app.post(f'{APP_PREFIX}/api/updateWireguardConfigurationInfo') @app.post(f'{APP_PREFIX}/api/updateWireguardConfigurationInfo')
def API_updateWireguardConfigurationInfo(): def API_updateWireguardConfigurationInfo():
data = request.get_json() or {} data = request.get_json()
name = data.get('Name') name = data.get('Name')
key = data.get('Key') key = data.get('Key')
value = data.get('Value') value = data.get('Value')
if not all([data, key, name]):
if not all([name, key, value]): # Required values return ResponseObject(status=False, message="Please provide configuration name, key and value")
return ResponseObject(False, "Please provide configuration name, key, and value") if name not in WireguardConfigurations.keys():
if name not in WireguardConfigurations:
return ResponseObject(False, "Configuration does not exist", status_code=404) return ResponseObject(False, "Configuration does not exist", status_code=404)
target_configuration = WireguardConfigurations[name] status, msg, key = WireguardConfigurations[name].updateConfigurationInfo(key, value)
status, msg, key = target_configuration.updateConfigurationInfo(key, value)
return ResponseObject(status, msg, key)
return ResponseObject(status=status, message=msg, data=key)
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurationRawFile') @app.get(f'{APP_PREFIX}/api/getWireguardConfigurationRawFile')
def API_getWireguardConfigurationRawFile(): def API_GetWireguardConfigurationRawFile():
configuration_name = request.args.get('configurationName') configurationName = request.args.get('configurationName')
if configurationName is None or len(
if not configuration_name or configuration_name not in WireguardConfigurations: configurationName) == 0 or configurationName not in WireguardConfigurations.keys():
return ResponseObject(False, "Please provide a valid configuration name", status_code=404) return ResponseObject(False, "Please provide a valid configuration name", status_code=404)
config = WireguardConfigurations[configuration_name]
return ResponseObject(data={ return ResponseObject(data={
"path": config.configPath, "path": WireguardConfigurations[configurationName].configPath,
"content": config.getRawConfigurationFile() "content": WireguardConfigurations[configurationName].getRawConfigurationFile()
}) })
@app.post(f'{APP_PREFIX}/api/updateWireguardConfigurationRawFile') @app.post(f'{APP_PREFIX}/api/updateWireguardConfigurationRawFile')
def API_UpdateWireguardConfigurationRawFile(): def API_UpdateWireguardConfigurationRawFile():
data = request.get_json() or {} data = request.get_json()
configuration_name = data.get('configurationName') configurationName = data.get('configurationName')
raw_configuration = data.get('rawConfiguration') rawConfiguration = data.get('rawConfiguration')
if configurationName is None or len(
if not configuration_name or configuration_name not in WireguardConfigurations: configurationName) == 0 or configurationName not in WireguardConfigurations.keys():
return ResponseObject(False, "Please provide a valid configuration name") return ResponseObject(False, "Please provide a valid configuration name")
if rawConfiguration is None or len(rawConfiguration) == 0:
if not raw_configuration:
return ResponseObject(False, "Please provide content") return ResponseObject(False, "Please provide content")
config = WireguardConfigurations[configuration_name] status, err = WireguardConfigurations[configurationName].updateRawConfigurationFile(rawConfiguration)
status, err = config.updateRawConfigurationFile(raw_configuration)
return ResponseObject(status, err) return ResponseObject(status=status, message=err)
@app.post(f'{APP_PREFIX}/api/deleteWireguardConfiguration') @app.post(f'{APP_PREFIX}/api/deleteWireguardConfiguration')
def API_deleteWireguardConfiguration(): def API_deleteWireguardConfiguration():
data = request.get_json() or {} data = request.get_json()
configuration_name = data.get("ConfigurationName") if "ConfigurationName" not in data.keys() or data.get("ConfigurationName") is None or data.get("ConfigurationName") not in WireguardConfigurations.keys():
if not configuration_name or configuration_name not in WireguardConfigurations:
return ResponseObject(False, "Please provide the configuration name you want to delete", status_code=404) return ResponseObject(False, "Please provide the configuration name you want to delete", status_code=404)
rp = WireguardConfigurations.pop(data.get("ConfigurationName"))
rp = WireguardConfigurations.pop(configuration_name)
status = rp.deleteConfiguration() status = rp.deleteConfiguration()
if not status: if not status:
WireguardConfigurations[configuration_name] = rp WireguardConfigurations[data.get("ConfigurationName")] = rp
return ResponseObject(status) return ResponseObject(status)
@app.post(f'{APP_PREFIX}/api/renameWireguardConfiguration') @app.post(f'{APP_PREFIX}/api/renameWireguardConfiguration')
def API_renameWireguardConfiguration(): def API_renameWireguardConfiguration():
data = request.get_json() or {} data = request.get_json()
keys = ["ConfigurationName", "NewConfigurationName"]
for k in keys:
if (k not in data.keys() or data.get(k) is None or len(data.get(k)) == 0 or
(k == "ConfigurationName" and data.get(k) not in WireguardConfigurations.keys())):
return ResponseObject(False, "Please provide the configuration name you want to rename", status_code=404)
old_name = data.get("ConfigurationName") if data.get("NewConfigurationName") in WireguardConfigurations.keys():
new_name = data.get("NewConfigurationName") return ResponseObject(False, "Configuration name already exist", status_code=400)
if not old_name or old_name not in WireguardConfigurations: rc = WireguardConfigurations.pop(data.get("ConfigurationName"))
return ResponseObject(False, "Please provide a valid configuration name to rename", status_code=404)
if not new_name:
return ResponseObject(False, "Please provide a new configuration name", status=400)
if new_name in WireguardConfigurations:
return ResponseObject(False, "The configuration name already exists", status_code=400)
rc = WireguardConfigurations.pop(old_name)
status, message = rc.renameConfiguration(new_name)
status, message = rc.renameConfiguration(data.get("NewConfigurationName"))
if status: if status:
if rc.Protocol == 'wg': WireguardConfigurations[data.get("NewConfigurationName")] = (WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")) if rc.Protocol == 'wg' else AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")))
ConfigClass = WireguardConfiguration
else:
ConfigClass = AmneziaWireguardConfiguration
WireguardConfigurations[new_name] = ConfigClass(
DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, new_name
)
else: else:
WireguardConfigurations[old_name] = rc WireguardConfigurations[data.get("ConfigurationName")] = rc
return ResponseObject(status, message) return ResponseObject(status, message)
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurationRealtimeTraffic') @app.get(f'{APP_PREFIX}/api/getWireguardConfigurationRealtimeTraffic')
def API_getWireguardConfigurationRealtimeTraffic(): def API_getWireguardConfigurationRealtimeTraffic():
configuration_name = requests.args.get('configurationName') configurationName = request.args.get('configurationName')
if configurationName is None or configurationName not in WireguardConfigurations.keys():
if not configuration_name or configuration_name not in WireguardConfigurations:
return ResponseObject(False, "Configuration does not exist", status_code=404) return ResponseObject(False, "Configuration does not exist", status_code=404)
return ResponseObject(data=WireguardConfigurations[configurationName].getRealtimeTrafficUsage())
rt_traffic_usage = WireguardConfigurations[configuration_name]
return ResponseObject(data=rt_traffic_usage)
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurationBackup') @app.get(f'{APP_PREFIX}/api/getWireguardConfigurationBackup')
def API_getWireguardConfigurationBackup(): def API_getWireguardConfigurationBackup():
configuration_name = request.args.get('configurationName') configurationName = request.args.get('configurationName')
if configurationName is None or configurationName not in WireguardConfigurations.keys():
if not configuration_name or configuration_name not in WireguardConfigurations:
return ResponseObject(False, "Configuration does not exist", status_code=404) return ResponseObject(False, "Configuration does not exist", status_code=404)
return ResponseObject(data=WireguardConfigurations[configurationName].getBackups())
target_configuration = WireguardConfigurations[configuration_name]
return ResponseObject(data=target_configuration.getBackups())
@app.get(f'{APP_PREFIX}/api/getAllWireguardConfigurationBackup') @app.get(f'{APP_PREFIX}/api/getAllWireguardConfigurationBackup')
def API_getAllWireguardConfigurationBackup(): def API_getAllWireguardConfigurationBackup():
@@ -658,86 +561,47 @@ def API_getAllWireguardConfigurationBackup():
"ExistingConfigurations": {}, "ExistingConfigurations": {},
"NonExistingConfigurations": {} "NonExistingConfigurations": {}
} }
existingConfiguration = WireguardConfigurations.keys()
existing_configurations = WireguardConfigurations.keys() for i in existingConfiguration:
b = WireguardConfigurations[i].getBackups(True)
for single_conf in existing_configurations: if len(b) > 0:
backups = WireguardConfigurations[single_conf].getBackups(True) data['ExistingConfigurations'][i] = WireguardConfigurations[i].getBackups(True)
if len(backups) > 0:
data['ExistingConfigurations'][single_conf] = WireguardConfigurations[single_conf].getBackups(True)
for protocol in ProtocolsEnabled(): for protocol in ProtocolsEnabled():
config_path_info = DashboardConfig.GetConfig("Server", f"{protocol}_conf_path") directory = os.path.join(DashboardConfig.GetConfig("Server", f"{protocol}_conf_path")[1], 'WGDashboard_Backup')
configuration_path = config_path_info[1] if os.path.exists(directory):
backup_directory = os.path.join(configuration_path, 'WGDashboard_Backup') files = [(file, os.path.getctime(os.path.join(directory, file)))
for file in os.listdir(directory) if os.path.isfile(os.path.join(directory, file))]
files.sort(key=lambda x: x[1], reverse=True)
if not os.path.exists(backup_directory): for f, ct in files:
continue if RegexMatch(r"^(.*)_(.*)\.(conf)$", f):
s = re.search(r"^(.*)_(.*)\.(conf)$", f)
backup_files = [] name = s.group(1)
for file_name in os.listdir(backup_directory): if name not in existingConfiguration:
full_file_path = os.path.join(backup_directory, file_name) if name not in data['NonExistingConfigurations'].keys():
if os.path.isfile(full_file_path): data['NonExistingConfigurations'][name] = []
creation_time = os.path.getctime(full_file_path)
backup_files.append((file_name, creation_time))
backup_files.sort(key=lambda file_info: file_info[1], reverse=True)
for file_name, creation_time in backup_files:
pattern = r"^(.*)_(.*)\.conf$"
match_result = re.match(pattern, file_name)
if not match_result:
continue
configuration_name = match_result.group(1)
backup_date = match_result.group(2)
if configuration_name in existing_configurations:
continue
if 'NonExistingConfigurations' not in data:
data['NonExistingConfigurations'] = {}
if configuration_name not in data['NonExistingConfigurations']:
data['NonExistingConfigurations'][configuration_name] = []
configuration_file_path = os.path.join(backup_directory, file_name)
with open(configuration_file_path, 'r') as configuration_file:
configuration_content = configuration_file.read()
backup_data = {
"protocol": protocol,
"filename": file_name,
"backupDate": backup_date,
"content": configuration_content
}
sql_file_name = file_name.replace(".conf", ".sql")
sql_file_path = os.path.join(backup_directory, sql_file_name)
if os.path.isfile(sql_file_path):
with open(sql_file_path, 'r') as sql_file:
sql_content = sql_file.read()
backup_data["database"] = True
backup_data["databaseContent"] = sql_content
data['NonExistingConfigurations'][configuration_name].append(backup_data)
date = s.group(2)
d = {
"protocol": protocol,
"filename": f,
"backupDate": date,
"content": open(os.path.join(DashboardConfig.GetConfig("Server", f"{protocol}_conf_path")[1], 'WGDashboard_Backup', f), 'r').read()
}
if f.replace(".conf", ".sql") in list(os.listdir(directory)):
d['database'] = True
d['databaseContent'] = open(os.path.join(DashboardConfig.GetConfig("Server", f"{protocol}_conf_path")[1], 'WGDashboard_Backup', f.replace(".conf", ".sql")), 'r').read()
data['NonExistingConfigurations'][name].append(d)
return ResponseObject(data=data) return ResponseObject(data=data)
@app.get(f'{APP_PREFIX}/api/createWireguardConfigurationBackup') @app.get(f'{APP_PREFIX}/api/createWireguardConfigurationBackup')
def API_createWireguardConfigurationBackup(): def API_createWireguardConfigurationBackup():
configuration_name = request.args.get('configurationName') configurationName = request.args.get('configurationName')
if configurationName is None or configurationName not in WireguardConfigurations.keys():
if not configuration_name or configuration_name not in WireguardConfigurations:
return ResponseObject(False, "Configuration does not exist", status_code=404) return ResponseObject(False, "Configuration does not exist", status_code=404)
return ResponseObject(status=WireguardConfigurations[configurationName].backupConfigurationFile()[0],
conf_backup_file = WireguardConfigurations[configuration_name].backupConfigurationFile()[0] data=WireguardConfigurations[configurationName].getBackups())
conf_backups = WireguardConfigurations[configuration_name].getBackups()
return ResponseObject(status=conf_backup_file,data=conf_backups)
@app.post(f'{APP_PREFIX}/api/deleteWireguardConfigurationBackup') @app.post(f'{APP_PREFIX}/api/deleteWireguardConfigurationBackup')
def API_deleteWireguardConfigurationBackup(): def API_deleteWireguardConfigurationBackup():
@@ -769,19 +633,18 @@ def API_downloadWireguardConfigurationBackup():
@app.post(f'{APP_PREFIX}/api/restoreWireguardConfigurationBackup') @app.post(f'{APP_PREFIX}/api/restoreWireguardConfigurationBackup')
def API_restoreWireguardConfigurationBackup(): def API_restoreWireguardConfigurationBackup():
data = request.get_json() data = request.get_json()
configuration_name = data['ConfigurationName']
backup_file_name = data['BackupFileName']
if ("ConfigurationName" not in data.keys() or if ("ConfigurationName" not in data.keys() or
"BackupFileName" not in data.keys() or "BackupFileName" not in data.keys() or
len(data['ConfigurationName']) == 0 or len(data['ConfigurationName']) == 0 or
len(data['BackupFileName']) == 0): len(data['BackupFileName']) == 0):
return ResponseObject(False,"Please provide ConfigurationName and BackupFileName in body", status_code=400) return ResponseObject(False,
"Please provide ConfigurationName and BackupFileName in body", status_code=400)
if configuration_name not in WireguardConfigurations.keys(): configurationName = data['ConfigurationName']
backupFileName = data['BackupFileName']
if configurationName not in WireguardConfigurations.keys():
return ResponseObject(False, "Configuration does not exist", status_code=404) return ResponseObject(False, "Configuration does not exist", status_code=404)
status = WireguardConfigurations[configuration_name].restoreBackup(backup_file_name) status = WireguardConfigurations[configurationName].restoreBackup(backupFileName)
return ResponseObject(status=status, message=(None if status else 'Restore backup failed')) return ResponseObject(status=status, message=(None if status else 'Restore backup failed'))
@app.get(f'{APP_PREFIX}/api/getDashboardConfiguration') @app.get(f'{APP_PREFIX}/api/getDashboardConfiguration')
@@ -1523,6 +1386,31 @@ def API_Welcome_Finish():
DashboardConfig.SetConfig("Other", "welcome_session", False) DashboardConfig.SetConfig("Other", "welcome_session", False)
return ResponseObject() return ResponseObject()
class Locale:
def __init__(self):
self.localePath = './static/locales/'
self.activeLanguages = {}
with open(os.path.join(f"{self.localePath}supported_locales.json"), "r") as f:
self.activeLanguages = sorted(json.loads(''.join(f.readlines())), key=lambda x : x['lang_name'])
def getLanguage(self) -> dict | None:
currentLanguage = DashboardConfig.GetConfig("Server", "dashboard_language")[1]
if currentLanguage == "en":
return None
if os.path.exists(os.path.join(f"{self.localePath}{currentLanguage}.json")):
with open(os.path.join(f"{self.localePath}{currentLanguage}.json"), "r") as f:
return dict(json.loads(''.join(f.readlines())))
else:
return None
def updateLanguage(self, lang_id):
if not os.path.exists(os.path.join(f"{self.localePath}{lang_id}.json")):
DashboardConfig.SetConfig("Server", "dashboard_language", "en-US")
else:
DashboardConfig.SetConfig("Server", "dashboard_language", lang_id)
Locale = Locale()
@app.get(f'{APP_PREFIX}/api/locale') @app.get(f'{APP_PREFIX}/api/locale')
def API_Locale_CurrentLang(): def API_Locale_CurrentLang():
return ResponseObject(data=Locale.getLanguage()) return ResponseObject(data=Locale.getLanguage())
@@ -1607,31 +1495,6 @@ def API_SystemStatus():
def API_ProtocolsEnabled(): def API_ProtocolsEnabled():
return ResponseObject(data=ProtocolsEnabled()) return ResponseObject(data=ProtocolsEnabled())
class Locale:
def __init__(self):
self.localePath = './static/locales/'
self.activeLanguages = {}
with open(os.path.join(f"{self.localePath}supported_locales.json"), "r") as f:
self.activeLanguages = sorted(json.loads(''.join(f.readlines())), key=lambda x : x['lang_name'])
def getLanguage(self) -> dict | None:
currentLanguage = DashboardConfig.GetConfig("Server", "dashboard_language")[1]
if currentLanguage == "en":
return None
if os.path.exists(os.path.join(f"{self.localePath}{currentLanguage}.json")):
with open(os.path.join(f"{self.localePath}{currentLanguage}.json"), "r") as f:
return dict(json.loads(''.join(f.readlines())))
else:
return None
def updateLanguage(self, lang_id):
if not os.path.exists(os.path.join(f"{self.localePath}{lang_id}.json")):
DashboardConfig.SetConfig("Server", "dashboard_language", "en-US")
else:
DashboardConfig.SetConfig("Server", "dashboard_language", lang_id)
Locale = Locale()
''' '''
OIDC Controller OIDC Controller
''' '''

View File

@@ -1,7 +1,7 @@
import dashboard import dashboard
from datetime import datetime from datetime import datetime
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger, Dash global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger, Dash
app_host, app_port = dashboard.gunicornConfig() app_host, app_port, log_level = dashboard.gunicornConfig()
date = datetime.today().strftime('%Y_%m_%d_%H_%M_%S') date = datetime.today().strftime('%Y_%m_%d_%H_%M_%S')
def post_worker_init(worker): def post_worker_init(worker):
@@ -16,7 +16,7 @@ daemon = True
pidfile = './gunicorn.pid' pidfile = './gunicorn.pid'
wsgi_app = "dashboard:app" wsgi_app = "dashboard:app"
accesslog = f"./log/access_{date}.log" accesslog = f"./log/access_{date}.log"
loglevel = "info" loglevel = f"{log_level}"
capture_output = True capture_output = True
errorlog = f"./log/error_{date}.log" errorlog = f"./log/error_{date}.log"
pythonpath = "., ./modules" pythonpath = "., ./modules"

View File

@@ -174,7 +174,7 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
def getPeers(self): def getPeers(self):
self.Peers.clear() self.Peers.clear()
current_app.logger.info(f"Refreshing {self.Name} peer list") current_app.logger.debug(f"Refreshing {self.Name} peer list")
if self.configurationFileChanged(): if self.configurationFileChanged():
with open(self.configPath, 'r') as configFile: with open(self.configPath, 'r') as configFile:
@@ -183,7 +183,7 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
content = configFile.read().split('\n') content = configFile.read().split('\n')
try: try:
if "[Peer]" not in content: if "[Peer]" not in content:
current_app.logger.info(f"{self.Name} config has no [Peer] section") current_app.logger.debug(f"{self.Name} config has no [Peer] section")
return return
peerStarts = content.index("[Peer]") peerStarts = content.index("[Peer]")

View File

@@ -1,25 +1,47 @@
# ConnectionString.py
import configparser import configparser
import os import os
from sqlalchemy_utils import database_exists, create_database from sqlalchemy_utils import database_exists, create_database
from flask import current_app
def ConnectionString(database) -> str: default_db = "wgdashboard"
default_log_db = "wgdashboard_log"
default_job_db = "wgdashboard_job"
sqlite_path = "db"
if os.path.exists(sqlite_path):
os.makedirs(sqlite_path, exist_ok=True)
def ConnectionString(database_name: str) -> str:
"""
Returns a SQLAlchemy-compatible connection string for the chosen database.
Creates the database if it doesn't exist.
"""
# Read and parse the INI file once at startup
parser = configparser.ConfigParser(strict=False) parser = configparser.ConfigParser(strict=False)
parser.read_file(open('wg-dashboard.ini', "r+")) parser.read("wg-dashboard.ini")
sqlitePath = os.path.join("db")
if not os.path.isdir(sqlitePath): db_type = parser.get("Database", "type")
os.mkdir(sqlitePath) db_prefix = parser.get("Database", "prefix")
if parser.get("Database", "type") == "postgresql": database_name = f"{db_prefix}{database_name}"
cn = f'postgresql+psycopg://{parser.get("Database", "username")}:{parser.get("Database", "password")}@{parser.get("Database", "host")}/{database}'
elif parser.get("Database", "type") == "mysql": if db_type == "postgresql":
cn = f'mysql+pymysql://{parser.get("Database", "username")}:{parser.get("Database", "password")}@{parser.get("Database", "host")}/{database}' username = parser.get("Database", "username")
password = parser.get("Database", "password")
host = parser.get("Database", "host")
cn = f"postgresql+psycopg://{username}:{password}@{host}/{database_name}"
elif db_type == "mysql":
username = parser.get("Database", "username")
password = parser.get("Database", "password")
host = parser.get("Database", "host")
cn = f"mysql+pymysql://{username}:{password}@{host}/{database_name}"
else: else:
cn = f'sqlite:///{os.path.join(sqlitePath, f"{database}.db")}' cn = f'sqlite:///{os.path.join(sqlite_path, f"{database_name}.db")}'
try: try:
if not database_exists(cn): if not database_exists(cn):
create_database(cn) create_database(cn)
except Exception as e: except Exception as e:
current_app.logger.error("Database error. Terminating...", e) current_app.logger.critical("Database error. Terminating...", e)
exit(1) exit(1)
return cn return cn

View File

@@ -8,7 +8,7 @@ import pyotp
import sqlalchemy as db import sqlalchemy as db
import requests import requests
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db
from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment
from .DashboardClientsTOTP import DashboardClientsTOTP from .DashboardClientsTOTP import DashboardClientsTOTP
from .DashboardOIDC import DashboardOIDC from .DashboardOIDC import DashboardOIDC
@@ -20,7 +20,7 @@ from flask import session
class DashboardClients: class DashboardClients:
def __init__(self, wireguardConfigurations): def __init__(self, wireguardConfigurations):
self.logger = DashboardLogger() self.logger = DashboardLogger()
self.engine = db.create_engine(ConnectionString("wgdashboard")) self.engine = db.create_engine(ConnectionString(default_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.OIDC = DashboardOIDC("Client") self.OIDC = DashboardOIDC("Client")
@@ -32,10 +32,10 @@ class DashboardClients:
db.Column('TotpKey', db.String(500)), db.Column('TotpKey', db.String(500)),
db.Column('TotpKeyVerified', db.Integer), db.Column('TotpKeyVerified', db.Integer),
db.Column('CreatedDate', db.Column('CreatedDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP),
server_default=db.func.now()), server_default=db.func.now()),
db.Column('DeletedDate', db.Column('DeletedDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP)), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP)),
extend_existing=True, extend_existing=True,
) )
@@ -46,10 +46,10 @@ class DashboardClients:
db.Column('ProviderIssuer', db.String(500), nullable=False, index=True), db.Column('ProviderIssuer', db.String(500), nullable=False, index=True),
db.Column('ProviderSubject', db.String(500), nullable=False, index=True), db.Column('ProviderSubject', db.String(500), nullable=False, index=True),
db.Column('CreatedDate', db.Column('CreatedDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP),
server_default=db.func.now()), server_default=db.func.now()),
db.Column('DeletedDate', db.Column('DeletedDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP)), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP)),
extend_existing=True, extend_existing=True,
) )
@@ -65,10 +65,10 @@ class DashboardClients:
db.Column('ResetToken', db.String(255), nullable=False, primary_key=True), db.Column('ResetToken', db.String(255), nullable=False, primary_key=True),
db.Column('ClientID', db.String(255), nullable=False), db.Column('ClientID', db.String(255), nullable=False),
db.Column('CreatedDate', db.Column('CreatedDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP),
server_default=db.func.now()), server_default=db.func.now()),
db.Column('ExpiryDate', db.Column('ExpiryDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP)), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP)),
extend_existing=True extend_existing=True
) )

View File

@@ -1,7 +1,7 @@
import datetime import datetime
import uuid import uuid
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db
from .DashboardLogger import DashboardLogger from .DashboardLogger import DashboardLogger
import sqlalchemy as db import sqlalchemy as db
from .WireguardConfiguration import WireguardConfiguration from .WireguardConfiguration import WireguardConfiguration
@@ -31,7 +31,7 @@ class Assignment:
class DashboardClientsPeerAssignment: class DashboardClientsPeerAssignment:
def __init__(self, wireguardConfigurations: dict[str, WireguardConfiguration]): def __init__(self, wireguardConfigurations: dict[str, WireguardConfiguration]):
self.logger = DashboardLogger() self.logger = DashboardLogger()
self.engine = db.create_engine(ConnectionString("wgdashboard")) self.engine = db.create_engine(ConnectionString(default_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.wireguardConfigurations = wireguardConfigurations self.wireguardConfigurations = wireguardConfigurations
self.dashboardClientsPeerAssignmentTable = db.Table( self.dashboardClientsPeerAssignmentTable = db.Table(
@@ -41,10 +41,10 @@ class DashboardClientsPeerAssignment:
db.Column('ConfigurationName', db.String(255)), db.Column('ConfigurationName', db.String(255)),
db.Column('PeerID', db.String(500)), db.Column('PeerID', db.String(500)),
db.Column('AssignedDate', db.Column('AssignedDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP),
server_default=db.func.now()), server_default=db.func.now()),
db.Column('UnassignedDate', db.Column('UnassignedDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP)), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP)),
extend_existing=True extend_existing=True
) )
self.metadata.create_all(self.engine) self.metadata.create_all(self.engine)

View File

@@ -3,19 +3,19 @@ import hashlib
import uuid import uuid
import sqlalchemy as db import sqlalchemy as db
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db
class DashboardClientsTOTP: class DashboardClientsTOTP:
def __init__(self): def __init__(self):
self.engine = db.create_engine(ConnectionString("wgdashboard")) self.engine = db.create_engine(ConnectionString(default_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.dashboardClientsTOTPTable = db.Table( self.dashboardClientsTOTPTable = db.Table(
'DashboardClientsTOTPTokens', self.metadata, 'DashboardClientsTOTPTokens', self.metadata,
db.Column("Token", db.String(500), primary_key=True, index=True), db.Column("Token", db.String(500), primary_key=True, index=True),
db.Column("ClientID", db.String(500), index=True), db.Column("ClientID", db.String(500), index=True),
db.Column( db.Column(
"ExpireTime", (db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP) "ExpireTime", (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP)
) )
) )
self.metadata.create_all(self.engine) self.metadata.create_all(self.engine)

View File

@@ -7,7 +7,7 @@ import sqlalchemy as db
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from flask import current_app from flask import current_app
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db
from .Utilities import ( from .Utilities import (
GetRemoteEndpoint, ValidateDNSAddress GetRemoteEndpoint, ValidateDNSAddress
) )
@@ -47,7 +47,8 @@ class DashboardConfig:
"dashboard_sort": "status", "dashboard_sort": "status",
"dashboard_theme": "dark", "dashboard_theme": "dark",
"dashboard_api_key": "false", "dashboard_api_key": "false",
"dashboard_language": "en-US" "dashboard_language": "en-US",
"log_level": "INFO"
}, },
"Peers": { "Peers": {
"peer_global_DNS": "1.1.1.1", "peer_global_DNS": "1.1.1.1",
@@ -65,7 +66,8 @@ class DashboardConfig:
"host": "", "host": "",
"port": "", "port": "",
"username": "", "username": "",
"password": "" "password": "",
"prefix": ""
}, },
"Email":{ "Email":{
"server": "", "server": "",
@@ -95,28 +97,14 @@ class DashboardConfig:
if not exist: if not exist:
self.SetConfig(section, key, value, True) self.SetConfig(section, key, value, True)
self.engine = db.create_engine(ConnectionString('wgdashboard')) self.SetConfig("Server", "version", DashboardConfig.DashboardVersion)
self.SaveConfig()
self.engine = db.create_engine(ConnectionString(default_db))
self.dbMetadata = db.MetaData() self.dbMetadata = db.MetaData()
self.__createAPIKeyTable() self.__createAPIKeyTable()
self.DashboardAPIKeys = self.__getAPIKeys() self.DashboardAPIKeys = self.__getAPIKeys()
self.APIAccessed = False self.APIAccessed = False
self.SetConfig("Server", "version", DashboardConfig.DashboardVersion)
def getConnectionString(self, database) -> str or None:
sqlitePath = os.path.join(DashboardConfig.ConfigurationPath, "db")
if not os.path.isdir(sqlitePath):
os.mkdir(sqlitePath)
if self.GetConfig("Database", "type")[1] == "postgresql":
cn = f'postgresql+psycopg2://{self.GetConfig("Database", "username")[1]}:{self.GetConfig("Database", "password")[1]}@{self.GetConfig("Database", "host")[1]}/{database}'
elif self.GetConfig("Database", "type")[1] == "mysql":
cn = f'mysql+mysqldb://{self.GetConfig("Database", "username")[1]}:{self.GetConfig("Database", "password")[1]}@{self.GetConfig("Database", "host")[1]}/{database}'
else:
cn = f'sqlite:///{os.path.join(sqlitePath, f"{database}.db")}'
if not database_exists(cn):
create_database(cn)
return cn
def __createAPIKeyTable(self): def __createAPIKeyTable(self):
self.apiKeyTable = db.Table('DashboardAPIKeys', self.dbMetadata, self.apiKeyTable = db.Table('DashboardAPIKeys', self.dbMetadata,

View File

@@ -4,18 +4,18 @@ Dashboard Logger Class
import uuid import uuid
import sqlalchemy as db import sqlalchemy as db
from flask import current_app from flask import current_app
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db, default_log_db
class DashboardLogger: class DashboardLogger:
def __init__(self): def __init__(self):
self.engine = db.create_engine(ConnectionString("wgdashboard_log")) self.engine = db.create_engine(ConnectionString(default_log_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.dashboardLoggerTable = db.Table('DashboardLog', self.metadata, self.dashboardLoggerTable = db.Table('DashboardLog', self.metadata,
db.Column('LogID', db.String(255), nullable=False, primary_key=True), db.Column('LogID', db.String(255), nullable=False, primary_key=True),
db.Column('LogDate', db.Column('LogDate',
(db.DATETIME if 'sqlite:///' in ConnectionString("wgdashboard") else db.TIMESTAMP), (db.DATETIME if 'sqlite:///' in ConnectionString(default_db) else db.TIMESTAMP),
server_default=db.func.now()), server_default=db.func.now()),
db.Column('URL', db.String(255)), db.Column('URL', db.String(255)),
db.Column('IP', db.String(255)), db.Column('IP', db.String(255)),

View File

@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
import requests import requests
from pydantic import BaseModel, field_serializer from pydantic import BaseModel, field_serializer
import sqlalchemy as db import sqlalchemy as db
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db
from flask import current_app from flask import current_app
WebHookActions = ['peer_created', 'peer_deleted', 'peer_updated'] WebHookActions = ['peer_created', 'peer_deleted', 'peer_updated']
@@ -40,7 +40,7 @@ class WebHookSessionLogs(BaseModel):
class DashboardWebHooks: class DashboardWebHooks:
def __init__(self, DashboardConfig): def __init__(self, DashboardConfig):
self.engine = db.create_engine(ConnectionString("wgdashboard")) self.engine = db.create_engine(ConnectionString(default_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.webHooksTable = db.Table( self.webHooksTable = db.Table(
'DashboardWebHooks', self.metadata, 'DashboardWebHooks', self.metadata,
@@ -201,7 +201,7 @@ class DashboardWebHooks:
class WebHookSession: class WebHookSession:
def __init__(self, webHook: WebHook, data: dict[str, str]): def __init__(self, webHook: WebHook, data: dict[str, str]):
self.engine = db.create_engine(ConnectionString("wgdashboard")) self.engine = db.create_engine(ConnectionString(default_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.webHookSessionsTable = db.Table('DashboardWebHookSessions', self.metadata, autoload_with=self.engine) self.webHookSessionsTable = db.Table('DashboardWebHookSessions', self.metadata, autoload_with=self.engine)
self.webHook = webHook self.webHook = webHook

View File

@@ -2,7 +2,7 @@ import uuid
from pydantic import BaseModel, field_serializer from pydantic import BaseModel, field_serializer
import sqlalchemy as db import sqlalchemy as db
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db
class NewConfigurationTemplate(BaseModel): class NewConfigurationTemplate(BaseModel):
@@ -14,7 +14,7 @@ class NewConfigurationTemplate(BaseModel):
class NewConfigurationTemplates: class NewConfigurationTemplates:
def __init__(self): def __init__(self):
self.engine = db.create_engine(ConnectionString("wgdashboard")) self.engine = db.create_engine(ConnectionString(default_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.templatesTable = db.Table( self.templatesTable = db.Table(
'NewConfigurationTemplates', self.metadata, 'NewConfigurationTemplates', self.metadata,

View File

@@ -4,12 +4,12 @@ Peer Job Logger
import uuid import uuid
import sqlalchemy as db import sqlalchemy as db
from flask import current_app from flask import current_app
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_log_db
from .Log import Log from .Log import Log
class PeerJobLogger: class PeerJobLogger:
def __init__(self, AllPeerJobs, DashboardConfig): def __init__(self, AllPeerJobs, DashboardConfig):
self.engine = db.create_engine(ConnectionString("wgdashboard_log")) self.engine = db.create_engine(ConnectionString(default_log_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.jobLogTable = db.Table('JobLog', self.metadata, self.jobLogTable = db.Table('JobLog', self.metadata,
db.Column('LogID', db.String(255), nullable=False, primary_key=True), db.Column('LogID', db.String(255), nullable=False, primary_key=True),

View File

@@ -1,7 +1,7 @@
""" """
Peer Jobs Peer Jobs
""" """
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_job_db
from .PeerJob import PeerJob from .PeerJob import PeerJob
from .PeerJobLogger import PeerJobLogger from .PeerJobLogger import PeerJobLogger
import sqlalchemy as db import sqlalchemy as db
@@ -11,7 +11,7 @@ from flask import current_app
class PeerJobs: class PeerJobs:
def __init__(self, DashboardConfig, WireguardConfigurations): def __init__(self, DashboardConfig, WireguardConfigurations):
self.Jobs: list[PeerJob] = [] self.Jobs: list[PeerJob] = []
self.engine = db.create_engine(ConnectionString('wgdashboard_job')) self.engine = db.create_engine(ConnectionString(default_job_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.peerJobTable = db.Table('PeerJobs', self.metadata, self.peerJobTable = db.Table('PeerJobs', self.metadata,
db.Column('JobID', db.String(255), nullable=False, primary_key=True), db.Column('JobID', db.String(255), nullable=False, primary_key=True),
@@ -141,7 +141,7 @@ class PeerJobs:
def runJob(self): def runJob(self):
current_app.logger.info("Running scheduled jobs") current_app.logger.debug("Running scheduled jobs")
needToDelete = [] needToDelete = []
self.__getJobs() self.__getJobs()
for job in self.Jobs: for job in self.Jobs:

View File

@@ -1,4 +1,4 @@
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db
from .PeerShareLink import PeerShareLink from .PeerShareLink import PeerShareLink
import sqlalchemy as db import sqlalchemy as db
from datetime import datetime from datetime import datetime
@@ -10,7 +10,7 @@ Peer Share Links
class PeerShareLinks: class PeerShareLinks:
def __init__(self, DashboardConfig, WireguardConfigurations): def __init__(self, DashboardConfig, WireguardConfigurations):
self.Links: list[PeerShareLink] = [] self.Links: list[PeerShareLink] = []
self.engine = db.create_engine(ConnectionString("wgdashboard")) self.engine = db.create_engine(ConnectionString(default_db))
self.metadata = db.MetaData() self.metadata = db.MetaData()
self.peerShareLinksTable = db.Table( self.peerShareLinksTable = db.Table(
'PeerShareLinks', self.metadata, 'PeerShareLinks', self.metadata,

View File

@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
from itertools import islice from itertools import islice
from flask import current_app from flask import current_app
from .ConnectionString import ConnectionString from .ConnectionString import ConnectionString, default_db
from .DashboardConfig import DashboardConfig from .DashboardConfig import DashboardConfig
from .Peer import Peer from .Peer import Peer
from .PeerJobs import PeerJobs from .PeerJobs import PeerJobs
@@ -64,7 +64,7 @@ class WireguardConfiguration:
self.AllPeerShareLinks = AllPeerShareLinks self.AllPeerShareLinks = AllPeerShareLinks
self.DashboardWebHooks = DashboardWebHooks self.DashboardWebHooks = DashboardWebHooks
self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf') self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf')
self.engine: sqlalchemy.Engine = sqlalchemy.create_engine(ConnectionString("wgdashboard")) self.engine: sqlalchemy.Engine = sqlalchemy.create_engine(ConnectionString(default_db))
self.metadata: sqlalchemy.MetaData = sqlalchemy.MetaData() self.metadata: sqlalchemy.MetaData = sqlalchemy.MetaData()
self.dbType = self.DashboardConfig.GetConfig("Database", "type")[1] self.dbType = self.DashboardConfig.GetConfig("Database", "type")[1]
@@ -396,7 +396,7 @@ class WireguardConfiguration:
def getPeers(self): def getPeers(self):
tmpList = [] tmpList = []
current_app.logger.info(f"Refreshing {self.Name} peer list") current_app.logger.debug(f"Refreshing {self.Name} peer list")
if self.configurationFileChanged(): if self.configurationFileChanged():
with open(self.configPath, 'r') as configFile: with open(self.configPath, 'r') as configFile:
@@ -405,7 +405,7 @@ class WireguardConfiguration:
content = configFile.read().split('\n') content = configFile.read().split('\n')
try: try:
if "[Peer]" not in content: if "[Peer]" not in content:
current_app.logger.info(f"{self.Name} config has no [Peer] section") current_app.logger.debug(f"{self.Name} config has no [Peer] section")
return return
peerStarts = content.index("[Peer]") peerStarts = content.index("[Peer]")

File diff suppressed because it is too large Load Diff

View File

@@ -26,15 +26,15 @@
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"i": "^0.3.7", "i": "^0.3.7",
"is-cidr": "^5.0.3", "is-cidr": "^6.0.0",
"npm": "^10.5.0", "npm": "^11.6.0",
"ol": "^10.2.1", "ol": "^10.2.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0", "pinia-plugin-persistedstate": "^4.5.0",
"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": "^11.1.0", "uuid": "^13.0.0",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-chartjs": "^5.3.0", "vue-chartjs": "^5.3.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"

View File

@@ -114,7 +114,7 @@ const data = computed(() => {
</h6> </h6>
</div> </div>
<div class="progress" role="progressbar" style="height: 6px"> <div class="progress" role="progressbar" style="height: 6px">
<div class="progress-bar bg-warning" :style="{width: `$ data?.Memory.SwapMemory.percent}%` }"></div> <div class="progress-bar bg-warning" :style="{width: `${data?.Memory.SwapMemory.percent}%` }"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -464,47 +464,6 @@ stop_wgd() {
fi fi
} }
# ============= Docker Functions =============
startwgd_docker() {
_checkWireguard
printf "[WGDashboard][Docker] WireGuard configuration started\n"
{ date; start_core ; printf "\n\n"; } >> ./log/install.txt
gunicorn_start
}
start_core() {
# Re-assign config_files to ensure it includes any newly created configurations
local config_files=$(find /etc/wireguard -type f -name "*.conf")
# Set file permissions
find /etc/wireguard -type f -name "*.conf" -exec chmod 600 {} \;
find "$iptable_dir" -type f -name "*.sh" -exec chmod +x {} \;
# Start WireGuard for each config file
for file in $config_files; do
config_name=$(basename "$file" ".conf")
wg-quick up "$config_name"
done
}
newconf_wgd() {
local wg_port_listen=$wg_port
local wg_addr_range=$wg_net
private_key=$(wg genkey)
public_key=$(echo "$private_key" | wg pubkey)
cat <<EOF >"/etc/wireguard/wg0.conf"
[Interface]
PrivateKey = $private_key
Address = $wg_addr_range
ListenPort = $wg_port_listen
SaveConfig = true
PostUp = /opt/wireguarddashboard/src/iptable-rules/postup.sh
PreDown = /opt/wireguarddashboard/src/iptable-rules/postdown.sh
EOF
}
# ============= Docker Functions =============
start_wgd_debug() { start_wgd_debug() {
_checkWireguard _checkWireguard
printf "[WGDashboard] Starting WGDashboard in the foreground.\n" printf "[WGDashboard] Starting WGDashboard in the foreground.\n"