1
0
mirror of https://github.com/donaldzou/WGDashboard.git synced 2024-11-22 15:20:09 +01:00

Finished revamping peer edit

This commit is contained in:
Donald Zou 2024-03-24 18:24:01 -04:00
parent f1e71ecb78
commit bcd845fd59
8 changed files with 382 additions and 36 deletions

View File

@ -1,4 +1,5 @@
import itertools import itertools
import random
import sqlite3 import sqlite3
import configparser import configparser
import hashlib import hashlib
@ -13,6 +14,7 @@ import re
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import urllib.error import urllib.error
import uuid
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from json import JSONEncoder from json import JSONEncoder
@ -262,7 +264,7 @@ class WireguardConfiguration:
sqldb.commit() sqldb.commit()
def __getPublicKey(self) -> str: def __getPublicKey(self) -> str:
return subprocess.check_output(['wg', 'pubkey'], input=self.PrivateKey.encode()).decode().strip('\n') return _generatePublicKey(self.PrivateKey)[1]
def getStatus(self) -> bool: def getStatus(self) -> bool:
self.Status = self.Name in psutil.net_if_addrs().keys() self.Status = self.Name in psutil.net_if_addrs().keys()
@ -333,6 +335,12 @@ class WireguardConfiguration:
except ValueError: except ValueError:
pass pass
def searchPeer(self, publicKey):
for i in self.Peers:
if i.id == publicKey:
return True, i
return False, None
def __savePeers(self): def __savePeers(self):
for i in self.Peers: for i in self.Peers:
d = i.toJson() d = i.toJson()
@ -671,6 +679,56 @@ def _getConfigurationList() -> [WireguardConfiguration]:
return configurations return configurations
def _checkIPWithRange(ip):
ip_patterns = (
r"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|\/)){4}([0-9]{1,2})(,|$)",
r"[0-9a-fA-F]{0,4}(:([0-9a-fA-F]{0,4})){1,7}\/([0-9]{1,3})(,|$)"
)
for match_pattern in ip_patterns:
match_result = regex_match(match_pattern, ip)
if match_result:
result = match_result
break
else:
result = None
return result
def _checkIP(ip):
ip_patterns = (
r"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}",
r"[0-9a-fA-F]{0,4}(:([0-9a-fA-F]{0,4})){1,7}$"
)
for match_pattern in ip_patterns:
match_result = regex_match(match_pattern, ip)
if match_result:
result = match_result
break
else:
result = None
return result
def _checkDNS(dns):
dns = dns.replace(' ', '').split(',')
for i in dns:
if not (_checkIP(i) or regex_match(r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z]{0,61}[a-z]", i)):
return False
return True
def _generatePublicKey(privateKey) -> [bool, str]:
try:
publicKey = subprocess.check_output(f"wg pubkey", input=privateKey.encode(), shell=True,
stderr=subprocess.STDOUT)
return True, publicKey.decode().strip('\n')
except subprocess.CalledProcessError:
return False, None
''' '''
API Routes API Routes
''' '''
@ -827,6 +885,88 @@ def API_updateDashboardConfigurationItem():
return ResponseObject() return ResponseObject()
@app.route('/api/updatePeerSettings/<configName>', methods=['POST'])
def API_updatePeerSettings(configName):
data = request.get_json()
id = data['id']
if len(id) > 0 and configName in WireguardConfigurations.keys():
name = data['name']
private_key = data['private_key']
dns_addresses = data['DNS']
allowed_ip = data['allowed_ip']
endpoint_allowed_ip = data['endpoint_allowed_ip']
preshared_key = data['preshared_key']
wireguardConfig = WireguardConfigurations[configName]
foundPeer, peer = wireguardConfig.searchPeer(id)
if foundPeer:
for p in wireguardConfig.Peers:
if allowed_ip in p.allowed_ip and p.id != peer.id:
return ResponseObject(False, f"Allowed IP already taken by another peer.")
if not _checkIPWithRange(endpoint_allowed_ip):
return ResponseObject(False, f"Endpoint Allowed IPs format is incorrect.")
if not _checkDNS(dns_addresses):
return ResponseObject(False, f"DNS format is incorrect.")
if data['mtu'] < 0 or data['mtu'] > 1460:
return ResponseObject(False, "MTU format is not correct.")
if data['keepalive'] < 0:
return ResponseObject(False, "Persistent Keepalive format is not correct.")
if len(private_key) > 0:
pubKey = _generatePublicKey(private_key)
if not pubKey[0] or pubKey[1] != peer.id:
return ResponseObject(False, "Private key does not match with the public key.")
try:
rd = random.Random()
uid = uuid.UUID(int=rd.getrandbits(128), version=4)
with open(f"{uid}", "w+") as f:
f.write(preshared_key)
updatePsk = subprocess.check_output(
f"wg set {configName} peer {peer.id} preshared-key {uid}",
shell=True, stderr=subprocess.STDOUT)
os.remove(str(uid))
if len(updatePsk.decode().strip("\n")) != 0:
return ResponseObject(False,
"Update peer failed when updating preshared key: " + updatePsk.decode().strip(
"\n"))
allowed_ip = allowed_ip.replace(" ", "")
updateAllowedIp = subprocess.check_output(
f'wg set {configName} peer {peer.id} allowed-ips "{allowed_ip}"',
shell=True, stderr=subprocess.STDOUT)
if len(updateAllowedIp.decode().strip("\n")) != 0:
return ResponseObject(False,
"Update peer failed when updating allowed IPs: " + updateAllowedIp.decode().strip(
"\n"))
saveConfig = subprocess.check_output(f"wg-quick save {configName}",
shell=True, stderr=subprocess.STDOUT)
if f"wg showconf {configName}" not in saveConfig.decode().strip('\n'):
return ResponseObject(False,
"Update peer failed when saving the configuration." + saveConfig.decode().strip(
'\n'))
cursor.execute(
'''UPDATE %s SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?,
keepalive = ?, preshared_key = ? WHERE id = ?''' % configName,
(name, private_key, dns_addresses, endpoint_allowed_ip, data["mtu"],
data["keepalive"], preshared_key, id,)
)
return ResponseObject()
except subprocess.CalledProcessError as exc:
return ResponseObject(False, exc.output.decode("UTF-8").strip())
return ResponseObject(False, "Peer does not exist")
@app.route("/api/downloadPeer/<configName>")
def API_downloadPeer(configName):
if configName in WireguardConfigurations.keys():
pass
return ResponseObject(False)
@app.route('/api/getWireguardConfigurationInfo', methods=["GET"]) @app.route('/api/getWireguardConfigurationInfo', methods=["GET"])
def API_getConfigurationInfo(): def API_getConfigurationInfo():
configurationName = request.args.get("configurationName") configurationName = request.args.get("configurationName")
@ -843,6 +983,11 @@ def API_getDashboardTheme():
return ResponseObject(data=DashboardConfig.GetConfig("Server", "dashboard_theme")[1]) return ResponseObject(data=DashboardConfig.GetConfig("Server", "dashboard_theme")[1])
'''
Sign Up
'''
@app.route('/api/isTotpEnabled') @app.route('/api/isTotpEnabled')
def API_isTotpEnabled(): def API_isTotpEnabled():
return ResponseObject(data=DashboardConfig.GetConfig("Account", "enable_totp")[1]) return ResponseObject(data=DashboardConfig.GetConfig("Account", "enable_totp")[1])

View File

@ -74,6 +74,7 @@ export default {
</a> </a>
<Transition name="slide-fade"> <Transition name="slide-fade">
<PeerSettingsDropdown <PeerSettingsDropdown
@setting="this.$emit('setting')"
:Peer="Peer" :Peer="Peer"
v-if="this.subMenuOpened" v-if="this.subMenuOpened"
ref="target" ref="target"

View File

@ -33,6 +33,7 @@ import {
Tooltip Tooltip
} from 'chart.js'; } from 'chart.js';
import dayjs from "dayjs"; import dayjs from "dayjs";
import PeerSettings from "@/components/configurationComponents/peerSettings.vue";
Chart.register( Chart.register(
ArcElement, ArcElement,
@ -62,7 +63,7 @@ Chart.register(
export default { export default {
name: "peerList", name: "peerList",
components: {PeerSearch, Peer, Line, Bar}, components: {PeerSettings, PeerSearch, Peer, Line, Bar},
setup(){ setup(){
const dashboardConfigurationStore = DashboardConfigurationStore(); const dashboardConfigurationStore = DashboardConfigurationStore();
const wireguardConfigurationStore = WireguardConfigurationsStore(); const wireguardConfigurationStore = WireguardConfigurationsStore();
@ -100,10 +101,14 @@ export default {
}, },
], ],
}, },
peerSetting: {
modalOpen: false,
selectedPeer: undefined
}
} }
}, },
watch: { watch: {
'$route.params.id': { '$route.params': {
immediate: true, immediate: true,
handler(){ handler(){
clearInterval(this.interval) clearInterval(this.interval)
@ -430,17 +435,9 @@ export default {
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<RouterView v-slot="{Component}">
<Transition name="fade3" mode="out-in">
<Component :is="Component"></Component>
</Transition>
</RouterView>
<div class="d-flex align-items-center gap-3 mb-2 "> <div class="d-flex align-items-center gap-3 mb-2 ">
<h3>Peers</h3> <h3>Peers</h3>
<RouterLink <a href="#" class="text-decoration-none ms-auto"><i class="bi bi-plus-circle-fill me-2"></i>Add Peer</a>
to="./peer_settings"
class="ms-auto text-secondary text-decoration-none"><i class="bi bi-sliders2 me-2"></i>Peer Settings</RouterLink>
<a href="#" class="text-decoration-none"><i class="bi bi-plus-circle-fill me-2"></i>Add Peer</a>
</div> </div>
<PeerSearch></PeerSearch> <PeerSearch></PeerSearch>
@ -448,11 +445,17 @@ export default {
<div class="col-12 col-lg-6 col-xl-4" <div class="col-12 col-lg-6 col-xl-4"
:key="peer.id" :key="peer.id"
v-for="peer in this.searchPeers"> v-for="peer in this.searchPeers">
<Peer :Peer="peer"></Peer> <Peer :Peer="peer" @setting="peerSetting.modalOpen = true; peerSetting.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"></Peer>
</div> </div>
</TransitionGroup> </TransitionGroup>
</div> </div>
<Transition name="fade">
<PeerSettings v-if="this.peerSetting.modalOpen"
:selectedPeer="this.peerSetting.selectedPeer"
@refresh="this.getPeers(this.$route.params.id)"
@close="this.peerSetting.modalOpen = false">
</PeerSettings>
</Transition>
</div> </div>
</template> </template>

View File

@ -1,32 +1,198 @@
<script> <script>
import {fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default { export default {
name: "peerSettings" name: "peerSettings",
props: {
selectedPeer: Object
},
data(){
return {
data: undefined,
dataChanged: false,
showKey: false,
saving: false
}
},
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
return {dashboardConfigurationStore}
},
methods: {
reset(){
if (this.selectedPeer){
this.data = JSON.parse(JSON.stringify(this.selectedPeer))
this.dataChanged = false;
}
},
savePeer(){
this.saving = true;
fetchPost(`/api/updatePeerSettings/${this.$route.params.id}`, this.data, (res) => {
this.saving = false;
if (res.status){
this.dashboardConfigurationStore.newMessage("Server", "Peer Updated!", "success")
}else{
this.dashboardConfigurationStore.newMessage("Server", res.message, "danger")
}
this.$emit("refresh")
})
}
},
beforeMount() {
this.reset();
},
mounted() {
this.$el.querySelectorAll("input").forEach(x => {
x.addEventListener("keyup", () => {
this.dataChanged = true;
});
})
}
} }
</script> </script>
<template> <template>
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0"> <div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0">
<div class="container d-flex h-100 w-100"> <div class="container d-flex h-100 w-100">
<div class="card m-auto rounded-3 w-100"> <div class="card m-auto rounded-3 shadow" style="width: 700px">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4"> <div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
<h4 class="mb-0">Peer Settings</h4> <h4 class="mb-0">Peer Settings</h4>
<router-link to="./" class="ms-auto btn"> <button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
<i class="bi bi-x-lg"></i>
</router-link>
</div> </div>
<div class="card-body px-4 pb-4" v-if="this.data">
<div class="card-body p-4"> <div class="d-flex flex-column gap-2 mb-4">
<div>
<small class="text-muted">Public Key</small><br>
<small><samp>{{this.data.id}}</samp></small>
</div>
<div>
<label for="peer_name_textbox" class="form-label">
<small class="text-muted">Name</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.name"
id="peer_name_textbox" placeholder="">
</div>
<div>
<div class="d-flex position-relative">
<label for="peer_private_key_textbox" class="form-label">
<small class="text-muted">Private Key <code>(Required for QR Code and Download)</code></small>
</label>
<a role="button" class="ms-auto text-decoration-none toggleShowKey"
@click="this.showKey = !this.showKey"
>
<i class="bi" :class="[this.showKey ? 'bi-eye-slash-fill':'bi-eye-fill']"></i>
</a>
</div>
<input :type="[this.showKey ? 'text':'password']" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.private_key"
id="peer_private_key_textbox"
style="padding-right: 40px">
</div>
<div>
<label for="peer_allowed_ip_textbox" class="form-label">
<small class="text-muted">Allowed IPs <code>(Required)</code></small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.allowed_ip"
id="peer_allowed_ip_textbox">
</div>
<div>
<label for="peer_DNS_textbox" class="form-label">
<small class="text-muted">DNS <code>(Required)</code></small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.DNS"
id="peer_DNS_textbox">
</div>
<div>
<label for="peer_endpoint_allowed_ips" class="form-label">
<small class="text-muted">Endpoint Allowed IPs <code>(Required)</code></small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.endpoint_allowed_ip"
id="peer_endpoint_allowed_ips">
</div>
<hr>
<div class="accordion mt-2" id="peerSettingsAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button rounded-3 collapsed" type="button"
data-bs-toggle="collapse" data-bs-target="#peerSettingsAccordionOptional">
Optional Settings
</button>
</h2>
<div id="peerSettingsAccordionOptional" class="accordion-collapse collapse"
data-bs-parent="#peerSettingsAccordion">
<div class="accordion-body d-flex flex-column gap-2 mb-2">
<div>
<label for="peer_preshared_key_textbox" class="form-label">
<small class="text-muted">Pre-Shared Key</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.preshared_key"
id="peer_preshared_key_textbox">
</div>
<div>
<label for="peer_mtu" class="form-label"><small class="text-muted">MTU</small></label>
<input type="number" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.mtu"
id="peer_mtu">
</div>
<div>
<label for="peer_keep_alive" class="form-label">
<small class="text-muted">Persistent Keepalive</small>
</label>
<input type="number" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.keepalive"
id="peer_keep_alive">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-secondary rounded-3 shadow"
@click="this.reset()"
:disabled="!this.dataChanged || this.saving">
Reset <i class="bi bi-arrow-clockwise ms-2"></i>
</button>
<button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow"
:disabled="!this.dataChanged || this.saving"
@click="this.savePeer()"
>
Save Peer<i class="bi bi-save-fill ms-2"></i></button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.peerSettingContainer{ .peerSettingContainer {
background-color: #00000060; background-color: #00000060;
z-index: 1000; z-index: 1000;
} }
.toggleShowKey{
position: absolute;
top: 35px;
right: 12px;
}
</style> </style>

View File

@ -21,7 +21,9 @@ export default {
</template> </template>
<li> <li>
<a class="dropdown-item d-flex" role="button"> <a class="dropdown-item d-flex" role="button"
@click="this.$emit('setting')"
>
<i class="me-auto bi bi-pen"></i> Edit <i class="me-auto bi bi-pen"></i> Edit
</a> </a>
</li> </li>

View File

@ -18,7 +18,7 @@ export default {
<div class="mt-4"> <div class="mt-4">
<div class="container"> <div class="container">
<div class="d-flex mb-4 "> <div class="d-flex mb-4 ">
<h3 class="text-body">Wireguard Configurations</h3> <h3 class="text-body">WireGuard Configurations</h3>
<RouterLink to="/new_configuration" class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto rounded-3"> <RouterLink to="/new_configuration" class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto rounded-3">
Configuration Configuration
<i class="bi bi-plus-circle-fill ms-2"></i> <i class="bi bi-plus-circle-fill ms-2"></i>

View File

@ -30,34 +30,46 @@ const router = createRouter({
path: '/', path: '/',
component: Index, component: Index,
meta: { meta: {
requiresAuth: true requiresAuth: true,
}, },
children: [ children: [
{ {
name: "Configuration List", name: "Configuration List",
path: '', path: '',
component: ConfigurationList component: ConfigurationList,
meta: {
title: "WireGuard Configurations"
}
}, },
{ {
name: "Settings", name: "Settings",
path: '/settings', path: '/settings',
component: Settings component: Settings,
meta: {
title: "Settings"
}
}, },
{ {
name: "New Configuration", name: "New Configuration",
path: '/new_configuration', path: '/new_configuration',
component: NewConfiguration component: NewConfiguration,
meta: {
title: "New Configuration"
}
}, },
{ {
name: "Configuration", name: "Configuration",
path: '/configuration/:id/', path: '/configuration/:id',
component: Configuration, component: Configuration,
meta: {
title: "Configuration"
},
children: [ children: [
{ {
name: "Peer Settings", name: "Peers List",
path: 'peer_settings', path: '',
component: PeerSettings component: PeerList
} }
] ]
}, },
@ -65,7 +77,10 @@ const router = createRouter({
] ]
}, },
{ {
path: '/signin', component: Signin path: '/signin', component: Signin,
meta: {
title: "Sign In"
}
}, },
{ {
path: '/welcome', component: Setup, path: '/welcome', component: Setup,
@ -80,6 +95,15 @@ router.beforeEach(async (to, from, next) => {
const wireguardConfigurationsStore = WireguardConfigurationsStore(); const wireguardConfigurationsStore = WireguardConfigurationsStore();
const dashboardConfigurationStore = DashboardConfigurationStore(); const dashboardConfigurationStore = DashboardConfigurationStore();
if (to.meta.title){
if (to.params.id){
document.title = to.params.id + " | WGDashboard";
}else{
document.title = to.meta.title + " | WGDashboard";
}
}else{
document.title = "WGDashboard"
}
if (to.meta.requiresAuth){ if (to.meta.requiresAuth){
if (cookie.getCookie("authToken") && await checkAuth()){ if (cookie.getCookie("authToken") && await checkAuth()){

View File

@ -71,7 +71,12 @@ export default {
<template> <template>
<div class="mt-5 text-body"> <div class="mt-5 text-body">
<PeerList></PeerList> <!-- <PeerList></PeerList>-->
<RouterView v-slot="{ Component }">
<Transition name="fade2" mode="out-in">
<Component :is="Component"></Component>
</Transition>
</RouterView>
</div> </div>
</template> </template>