Updated how available IP is generated

This commit is contained in:
Donald Zou 2025-02-16 17:42:32 +08:00
parent f055241802
commit 6f15389411
11 changed files with 1382 additions and 1287 deletions

View File

@ -10,6 +10,7 @@ from json import JSONEncoder
from flask_cors import CORS from flask_cors import CORS
from icmplib import ping, traceroute from icmplib import ping, traceroute
from flask.json.provider import DefaultJSONProvider from flask.json.provider import DefaultJSONProvider
from itertools import islice
from Utilities import ( from Utilities import (
RegexMatch, GetRemoteEndpoint, StringToBoolean, RegexMatch, GetRemoteEndpoint, StringToBoolean,
ValidateIPAddressesWithRange, ValidateDNSAddress, ValidateIPAddressesWithRange, ValidateDNSAddress,
@ -1107,41 +1108,69 @@ class WireguardConfiguration:
return False, str(e) return False, str(e)
return True, None return True, None
def getAvailableIP(self, all: bool = False) -> tuple[bool, list[str]] | tuple[bool, None]: def getNumberOfAvailableIP(self):
if len(self.Address) < 0: if len(self.Address) < 0:
return False, None return False, None
address = self.Address.split(',') existedAddress = set()
existedAddress = [] availableAddress = {}
availableAddress = [] for p in self.Peers + self.getRestrictedPeersList():
for p in self.Peers: peerAllowedIP = p.allowed_ip.split(',')
if len(p.allowed_ip) > 0: for pip in peerAllowedIP:
add = p.allowed_ip.split(',') ppip = pip.strip().split('/')
for i in add: if len(ppip) == 2:
a, c = i.split('/')
try: try:
existedAddress.append(ipaddress.ip_address(a.replace(" ", ""))) check = ipaddress.ip_network(ppip[0])
except ValueError as error: existedAddress.add(check)
except Exception as e:
print(f"[WGDashboard] Error: {self.Name} peer {p.id} have invalid ip") print(f"[WGDashboard] Error: {self.Name} peer {p.id} have invalid ip")
for p in self.getRestrictedPeersList(): configurationAddresses = self.Address.split(',')
if len(p.allowed_ip) > 0: for ca in configurationAddresses:
add = p.allowed_ip.split(',') ca = ca.strip()
for i in add: caSplit = ca.split('/')
a, c = i.split('/') try:
existedAddress.append(ipaddress.ip_address(a.replace(" ", ""))) if len(caSplit) == 2:
for i in address: network = ipaddress.ip_network(ca, False)
addressSplit, cidr = i.split('/') existedAddress.add(ipaddress.ip_network(caSplit[0]))
existedAddress.append(ipaddress.ip_address(addressSplit.replace(" ", ""))) availableAddress[ca] = network.num_addresses
for i in address: for p in existedAddress:
network = ipaddress.ip_network(i.replace(" ", ""), False) if p.subnet_of(network):
count = 0 availableAddress[ca] -= 1
for h in network.hosts():
if h not in existedAddress: # map(lambda iph : ipaddress.ip_network(iph).compressed, network.hosts())
availableAddress.append(ipaddress.ip_network(h).compressed)
count += 1 except Exception as e:
if not all: print(e)
if network.version == 6 and count > 255: print(f"[WGDashboard] Error: Failed to parse IP address {ca} from {self.Name}")
break return True, availableAddress
def getAvailableIP(self, threshold = 255) -> tuple[bool, list[str]] | tuple[bool, None]:
if len(self.Address) < 0:
return False, None
existedAddress = set()
availableAddress = {}
for p in self.Peers + self.getRestrictedPeersList():
peerAllowedIP = p.allowed_ip.split(',')
for pip in peerAllowedIP:
ppip = pip.strip().split('/')
if len(ppip) == 2:
try:
check = ipaddress.ip_network(ppip[0])
existedAddress.add(pip)
except Exception as e:
print(f"[WGDashboard] Error: {self.Name} peer {p.id} have invalid ip")
configurationAddresses = self.Address.split(',')
for ca in configurationAddresses:
ca = ca.strip()
caSplit = ca.split('/')
try:
if len(caSplit) == 2:
network = ipaddress.ip_network(ca, False)
existedAddress.add(ipaddress.ip_network(caSplit[0]).compressed)
availableAddress[ca] = list(islice(filter(lambda ip : ip not in existedAddress,
map(lambda iph : ipaddress.ip_network(iph).compressed, network.hosts())), threshold))
except Exception as e:
print(e)
print(f"[WGDashboard] Error: Failed to parse IP address {ca} from {self.Name}")
return True, availableAddress return True, availableAddress
def getRealtimeTrafficUsage(self): def getRealtimeTrafficUsage(self):
@ -2491,7 +2520,7 @@ def API_addPeers(configName):
public_key: str = data.get('public_key', "") public_key: str = data.get('public_key', "")
allowed_ips: list[str] = data.get('allowed_ips', []) allowed_ips: list[str] = data.get('allowed_ips', [])
override_allowed_ips: bool = data.get('override_allowed_ips', False) allowed_ips_validation: bool = data.get('allowed_ips_validation', True)
endpoint_allowed_ip: str = data.get('endpoint_allowed_ip', DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1]) endpoint_allowed_ip: str = data.get('endpoint_allowed_ip', DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1])
dns_addresses: str = data.get('DNS', DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1]) dns_addresses: str = data.get('DNS', DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1])
@ -2508,21 +2537,24 @@ def API_addPeers(configName):
if len(endpoint_allowed_ip) == 0: if len(endpoint_allowed_ip) == 0:
endpoint_allowed_ip = DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1] endpoint_allowed_ip = DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1]
config = WireguardConfigurations.get(configName) config = WireguardConfigurations.get(configName)
# if not bulkAdd and (len(public_key) == 0 or len(allowed_ips) == 0):
# return ResponseObject(False, "Please provide at least public_key and allowed_ips")
if not config.getStatus(): if not config.getStatus():
config.toggleConfiguration() config.toggleConfiguration()
availableIps = config.getAvailableIP() ipStatus, availableIps = config.getAvailableIP()
defaultIPSubnet = list(availableIps.keys())[0]
if bulkAdd: if bulkAdd:
if type(preshared_key_bulkAdd) is not bool: if type(preshared_key_bulkAdd) is not bool:
preshared_key_bulkAdd = False preshared_key_bulkAdd = False
if type(bulkAddAmount) is not int or bulkAddAmount < 1: if type(bulkAddAmount) is not int or bulkAddAmount < 1:
return ResponseObject(False, "Please specify amount of peers you want to add") return ResponseObject(False, "Please specify amount of peers you want to add")
if not availableIps[0]: if not ipStatus:
return ResponseObject(False, "No more available IP can assign") return ResponseObject(False, "No more available IP can assign")
if bulkAddAmount > len(availableIps[1]): if len(availableIps.keys()) == 0:
return ResponseObject(False, "This configuration does not have any IP address available")
if bulkAddAmount > len(availableIps[defaultIPSubnet]):
return ResponseObject(False, return ResponseObject(False,
f"The maximum number of peers can add is {len(availableIps[1])}") f"The maximum number of peers can add is {len(availableIps[defaultIPSubnet])}")
keyPairs = [] keyPairs = []
for i in range(bulkAddAmount): for i in range(bulkAddAmount):
newPrivateKey = GenerateWireguardPrivateKey()[1] newPrivateKey = GenerateWireguardPrivateKey()[1]
@ -2530,7 +2562,7 @@ def API_addPeers(configName):
"private_key": newPrivateKey, "private_key": newPrivateKey,
"id": GenerateWireguardPublicKey(newPrivateKey)[1], "id": GenerateWireguardPublicKey(newPrivateKey)[1],
"preshared_key": (GenerateWireguardPrivateKey()[1] if preshared_key_bulkAdd else ""), "preshared_key": (GenerateWireguardPrivateKey()[1] if preshared_key_bulkAdd else ""),
"allowed_ip": availableIps[1][i], "allowed_ip": availableIps[defaultIPSubnet][i],
"name": f"BulkPeer #{(i + 1)}_{datetime.now().strftime('%Y%m%d_%H%M%S')}", "name": f"BulkPeer #{(i + 1)}_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
"DNS": dns_addresses, "DNS": dns_addresses,
"endpoint_allowed_ip": endpoint_allowed_ip, "endpoint_allowed_ip": endpoint_allowed_ip,
@ -2554,14 +2586,19 @@ def API_addPeers(configName):
public_key = GenerateWireguardPublicKey(private_key)[1] public_key = GenerateWireguardPublicKey(private_key)[1]
if len(allowed_ips) == 0: if len(allowed_ips) == 0:
if availableIps[0]: if ipStatus:
allowed_ips = [availableIps[1][0]] allowed_ips = [availableIps[defaultIPSubnet][0]]
else: else:
return ResponseObject(False, "No more available IP can assign") return ResponseObject(False, "No more available IP can assign")
if not override_allowed_ips: if allowed_ips_validation:
for i in allowed_ips: for i in allowed_ips:
if i not in availableIps[1]: found = False
for key in availableIps.keys():
if i in availableIps[key]:
found = True
break
if not found:
return ResponseObject(False, f"This IP is not available: {i}") return ResponseObject(False, f"This IP is not available: {i}")
status, result = config.addPeers([ status, result = config.addPeers([
@ -2580,7 +2617,7 @@ def API_addPeers(configName):
) )
return ResponseObject(status=status, message=result['message'], data=result['peers']) return ResponseObject(status=status, message=result['message'], data=result['peers'])
except Exception as e: except Exception as e:
print(e) print(e, str(e.__traceback__))
return ResponseObject(False, "Add peers failed. Please see data for specific issue") return ResponseObject(False, "Add peers failed. Please see data for specific issue")
return ResponseObject(False, "Configuration does not exist") return ResponseObject(False, "Configuration does not exist")
@ -2618,6 +2655,13 @@ def API_getAvailableIPs(configName):
status, ips = WireguardConfigurations.get(configName).getAvailableIP() status, ips = WireguardConfigurations.get(configName).getAvailableIP()
return ResponseObject(status=status, data=ips) return ResponseObject(status=status, data=ips)
@app.get(f"{APP_PREFIX}/api/getNumberOfAvailableIPs/<configName>")
def API_getNumberOfAvailableIPs(configName):
if configName not in WireguardConfigurations.keys():
return ResponseObject(False, "Configuration does not exist")
status, ips = WireguardConfigurations.get(configName).getNumberOfAvailableIP()
return ResponseObject(status=status, data=ips)
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurationInfo') @app.get(f'{APP_PREFIX}/api/getWireguardConfigurationInfo')
def API_getConfigurationInfo(): def API_getConfigurationInfo():
configurationName = request.args.get("configurationName") configurationName = request.args.get("configurationName")

View File

@ -4,6 +4,7 @@ import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStor
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
import {GetLocale} from "@/utilities/locale.js"; import {GetLocale} from "@/utilities/locale.js";
import {ref} from "vue";
export default { export default {
name: "allowedIPsInput", name: "allowedIPsInput",
@ -22,17 +23,23 @@ export default {
allowedIpFormatError: false allowedIpFormatError: false
} }
}, },
setup(){ setup(props){
const store = WireguardConfigurationsStore(); const store = WireguardConfigurationsStore();
const dashboardStore = DashboardConfigurationStore(); const dashboardStore = DashboardConfigurationStore();
return {store, dashboardStore} const selectedSubnet = ref("")
if(Object.keys(props.availableIp).length > 0){
selectedSubnet.value = Object.keys(props.availableIp)[0]
}
return {store, dashboardStore, selectedSubnet}
}, },
computed: { computed: {
searchAvailableIps(){ searchAvailableIps(){
return this.availableIpSearchString ? return (this.availableIpSearchString ?
this.availableIp.filter(x => this.availableIp[this.selectedSubnet].filter(x =>
x.includes(this.availableIpSearchString) && !this.data.allowed_ips.includes(x)) : x.includes(this.availableIpSearchString) && !this.data.allowed_ips.includes(x)) :
this.availableIp.filter(x => !this.data.allowed_ips.includes(x)) this.availableIp[this.selectedSubnet].filter(x => !this.data.allowed_ips.includes(x)))
}, },
inputGetLocale(){ inputGetLocale(){
return GetLocale("Enter IP Address/CIDR") return GetLocale("Enter IP Address/CIDR")
@ -59,14 +66,16 @@ export default {
watch: { watch: {
customAvailableIp(){ customAvailableIp(){
this.allowedIpFormatError = false; this.allowedIpFormatError = false;
},
availableIp(){
} }
}, },
mounted() { mounted() {
if (this.availableIp !== undefined && this.availableIp.length > 0 && this.data.allowed_ips.length === 0){ if (this.availableIp !== undefined &&
this.addAllowedIp(this.availableIp[0]) Object.keys(this.availableIp).length > 0 && this.data.allowed_ips.length === 0){
for (let subnet in this.availableIp){
if (this.availableIp[subnet].length > 0){
this.addAllowedIp(this.availableIp[subnet][0])
}
}
} }
} }
} }
@ -74,19 +83,19 @@ export default {
<template> <template>
<div :class="{inactiveField: this.bulk}"> <div :class="{inactiveField: this.bulk}">
<div class="d-flex"> <div class="d-flex flex-column flex-md-row mb-2">
<label for="peer_allowed_ip_textbox" class="form-label"> <label for="peer_allowed_ip_textbox" class="form-label mb-0">
<small class="text-muted"> <small class="text-muted">
<LocaleText t="Allowed IPs"></LocaleText> <code><LocaleText t="(Required)"></LocaleText></code> <LocaleText t="Allowed IPs"></LocaleText> <code><LocaleText t="(Required)"></LocaleText></code>
</small> </small>
</label> </label>
<div class="form-check form-switch ms-auto"> <div class="form-check form-switch ms-md-auto">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
v-model="this.data.override_allowed_ips" v-model="this.data.allowed_ips_validation"
role="switch" id="disableIPValidation"> role="switch" id="disableIPValidation">
<label class="form-check-label" for="disableIPValidation"> <label class="form-check-label" for="disableIPValidation">
<small> <small>
<LocaleText t="Disable Allowed IPs Validation"></LocaleText> <LocaleText t="Allowed IPs Validation"></LocaleText>
</small> </small>
</label> </label>
</div> </div>
@ -107,6 +116,7 @@ export default {
<input type="text" class="form-control form-control-sm rounded-start-3" <input type="text" class="form-control form-control-sm rounded-start-3"
:placeholder="this.inputGetLocale" :placeholder="this.inputGetLocale"
:class="{'is-invalid': this.allowedIpFormatError}" :class="{'is-invalid': this.allowedIpFormatError}"
@keyup.enter="this.customAvailableIp ? this.addAllowedIp(this.customAvailableIp) : undefined"
v-model="customAvailableIp" v-model="customAvailableIp"
id="peer_allowed_ip_textbox" id="peer_allowed_ip_textbox"
:disabled="bulk"> :disabled="bulk">
@ -129,11 +139,11 @@ export default {
<i class="bi bi-filter-circle me-2"></i> <i class="bi bi-filter-circle me-2"></i>
<LocaleText t="Pick Available IP"></LocaleText> <LocaleText t="Pick Available IP"></LocaleText>
</button> </button>
<ul class="dropdown-menu mt-2 shadow w-100 dropdown-menu-end rounded-3" <ul class="dropdown-menu mt-2 shadow w-100 dropdown-menu-end rounded-3 pb-0"
v-if="this.availableIp" v-if="this.availableIp"
style="overflow-y: scroll; max-height: 270px; width: 300px !important;"> style="width: 300px !important;">
<li> <li>
<div class="px-3 pb-2 pt-1 d-flex gap-3 align-items-center"> <div class="px-3 d-flex gap-3 align-items-center">
<label for="availableIpSearchString" class="text-muted"> <label for="availableIpSearchString" class="text-muted">
<i class="bi bi-search"></i> <i class="bi bi-search"></i>
</label> </label>
@ -142,16 +152,32 @@ export default {
class="form-control form-control-sm rounded-3" class="form-control form-control-sm rounded-3"
v-model="this.availableIpSearchString"> v-model="this.availableIpSearchString">
</div> </div>
<hr class="my-2">
<div class="px-3 overflow-x-scroll d-flex overflow-x-scroll overflow-y-hidden align-items-center gap-2">
<small class="text-muted">Subnet</small>
<button
v-for="key in Object.keys(this.availableIp)"
@click="this.selectedSubnet = key"
:class="{'bg-primary-subtle': this.selectedSubnet === key}"
class="btn btn-sm text-primary-emphasis rounded-3">
{{key}}
</button>
</div>
<hr class="mt-2 mb-0">
</li> </li>
<li v-for="ip in this.searchAvailableIps" > <li>
<a class="dropdown-item d-flex" role="button" @click="this.addAllowedIp(ip)"> <div class="overflow-y-scroll" style="height: 270px;">
<span class="me-auto"><small>{{ip}}</small></span> <div v-for="ip in this.searchAvailableIps" style="">
</a> <a class="dropdown-item d-flex" role="button" @click="this.addAllowedIp(ip)">
</li> <span class="me-auto"><small>{{ip}}</small></span>
<li v-if="this.searchAvailableIps.length === 0"> </a>
<small class="px-3 text-muted"> </div>
<LocaleText t="No available IP containing"></LocaleText> <div v-if="this.searchAvailableIps.length === 0">
"{{this.availableIpSearchString}}"</small> <small class="px-3 text-muted">
<LocaleText t="No available IP containing"></LocaleText>
"{{this.availableIpSearchString}}"</small>
</div>
</div>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,6 +1,8 @@
<script> <script>
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
import {GetLocale} from "@/utilities/locale.js"; import {GetLocale} from "@/utilities/locale.js";
import {useRoute} from "vue-router";
import {fetchGet} from "@/utilities/fetch.js";
export default { export default {
name: "bulkAdd", name: "bulkAdd",
@ -10,9 +12,37 @@ export default {
data: Object, data: Object,
availableIp: undefined availableIp: undefined
}, },
data(){
return {
numberOfAvailableIPs: null
}
},
computed:{ computed:{
bulkAddGetLocale(){ bulkAddGetLocale(){
return GetLocale("How many peers you want to add?") return GetLocale("How many peers you want to add?")
},
getNumberOfAvailableIPs(){
if (!this.numberOfAvailableIPs){
return '...'
}else{
return Object.values(this.numberOfAvailableIPs).reduce((x, y) => {
return x + y
})
}
}
},
watch: {
'data.bulkAdd': {
immediate: true,
handler(newVal){
if (newVal){
fetchGet("/api/getNumberOfAvailableIPs/" + this.$route.params.id, {}, (res) => {
if (res.status){
this.numberOfAvailableIPs = res.data
}
})
}
}
} }
} }
} }
@ -42,7 +72,9 @@ export default {
v-model="this.data.bulkAddAmount" v-model="this.data.bulkAddAmount"
:placeholder="this.bulkAddGetLocale"> :placeholder="this.bulkAddGetLocale">
<small class="text-muted"> <small class="text-muted">
<LocaleText :t="`You can add up to ` + this.availableIp.length + ' peers'"></LocaleText> <LocaleText :t="`You can add up to ` +
getNumberOfAvailableIPs
+ ' peers'"></LocaleText>
</small> </small>
</div> </div>
</div> </div>

View File

@ -80,10 +80,7 @@ export default {
<div> <div>
<label for="peer_private_key_textbox" class="form-label"> <label for="peer_private_key_textbox" class="form-label">
<small class="text-muted"> <small class="text-muted">
<LocaleText t="Private Key"></LocaleText> <LocaleText t="Private Key"></LocaleText> <code><LocaleText t="(Required for QR Code and Download)"></LocaleText></code></small>
<code>
<LocaleText t="(Required for QR Code and Download)"></LocaleText>
</code></small>
</label> </label>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control form-control-sm rounded-start-3" <input type="text" class="form-control form-control-sm rounded-start-3"
@ -100,15 +97,14 @@ export default {
</div> </div>
</div> </div>
<div> <div>
<div class="d-flex"> <div class="d-flex flex-column flex-md-row mb-2">
<label for="public_key" class="form-label"> <label for="public_key" class="form-label mb-0">
<small class="text-muted"> <small class="text-muted">
<LocaleText t="Public Key"></LocaleText> <LocaleText t="Public Key"></LocaleText> <code>
<code>
<LocaleText t="(Required)"></LocaleText> <LocaleText t="(Required)"></LocaleText>
</code></small> </code></small>
</label> </label>
<div class="form-check form-switch ms-auto"> <div class="form-check form-switch ms-md-auto">
<input class="form-check-input" type="checkbox" role="switch" <input class="form-check-input" type="checkbox" role="switch"
:disabled="this.bulk" :disabled="this.bulk"
id="enablePublicKeyEdit" v-model="this.editKey"> id="enablePublicKeyEdit" v-model="this.editKey">

View File

@ -32,7 +32,7 @@ const peerData = ref({
preshared_key: "", preshared_key: "",
preshared_key_bulkAdd: false, preshared_key_bulkAdd: false,
advanced_security: "off", advanced_security: "off",
override_allowed_ips: false, allowed_ips_validation: true,
}) })
const availableIp = ref([]) const availableIp = ref([])
const saving = ref(false) const saving = ref(false)

View File

@ -0,0 +1,776 @@
<script>
import PeerSearch from "@/components/configurationComponents/peerSearch.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchGet} from "@/utilities/fetch.js";
import Peer from "@/components/configurationComponents/peer.vue";
import { Line, Bar } from 'vue-chartjs'
import Fuse from "fuse.js";
import {
Chart,
LineElement,
BarElement,
BarController,
LineController,
LinearScale,
Legend,
Title,
Tooltip,
CategoryScale,
PointElement
} from 'chart.js';
Chart.register(
LineElement,
BarElement,
BarController,
LineController,
LinearScale,
Legend,
Title,
Tooltip,
CategoryScale,
PointElement
);
import dayjs from "dayjs";
import {defineAsyncComponent, ref} from "vue";
import LocaleText from "@/components/text/localeText.vue";
import PeerRow from "@/components/configurationComponents/peerRow.vue";
import {GetLocale} from "@/utilities/locale.js";
import PeerSearchBar from "@/components/configurationComponents/peerSearchBar.vue";
import ProtocolBadge from "@/components/protocolBadge.vue";
import EditRawConfigurationFile
from "@/components/configurationComponents/editConfigurationComponents/editRawConfigurationFile.vue";
import PeerIntersectionObserver from "@/components/configurationComponents/peerIntersectionObserver.vue";
export default {
name: "peerList",
components: {
PeerIntersectionObserver,
EditRawConfigurationFile,
ProtocolBadge,
PeerSearchBar,
PeerRow,
DeleteConfiguration:
defineAsyncComponent(() => import("@/components/configurationComponents/deleteConfiguration.vue")),
ConfigurationBackupRestore:
defineAsyncComponent(() => import("@/components/configurationComponents/configurationBackupRestore.vue")),
SelectPeers:
defineAsyncComponent(() => import("@/components/configurationComponents/selectPeers.vue")),
EditConfiguration:
defineAsyncComponent(() => import("@/components/configurationComponents/editConfiguration.vue")),
LocaleText,
PeerShareLinkModal:
defineAsyncComponent(() => import("@/components/configurationComponents/peerShareLinkModal.vue")),
PeerJobsLogsModal:
defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsLogsModal.vue")),
PeerJobsAllModal:
defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsAllModal.vue")),
PeerJobs:
defineAsyncComponent(() => import("@/components/configurationComponents/peerJobs.vue")),
PeerCreate:
defineAsyncComponent(() => import("@/components/configurationComponents/peerCreate.vue")),
PeerQRCode:
defineAsyncComponent(() => import("@/components/configurationComponents/peerQRCode.vue")),
PeerConfigurationFile:
defineAsyncComponent(() => import("@/components/configurationComponents/peerConfigurationFile.vue")),
PeerSettings:
defineAsyncComponent(() => import("@/components/configurationComponents/peerSettings.vue")),
PeerSearch,
Peer,
Line,
Bar
},
async setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
const wireguardConfigurationStore = WireguardConfigurationsStore();
const interval = ref(undefined)
return {dashboardConfigurationStore, wireguardConfigurationStore, interval}
},
data(){
return {
configurationToggling: false,
loading: false,
error: null,
configurationInfo: [],
configurationPeers: [],
historyDataSentDifference: [],
historyDataReceivedDifference: [],
historySentData: {
labels: [],
datasets: [
{
label: 'Data Sent',
data: [],
fill: false,
borderColor: '#198754',
tension: 0
},
],
},
historyReceiveData: {
labels: [],
datasets: [
{
label: 'Data Received',
data: [],
fill: false,
borderColor: '#0d6efd',
tension: 0
},
],
},
peerSetting: {
modalOpen: false,
selectedPeer: undefined
},
peerScheduleJobs:{
modalOpen: false,
selectedPeer: undefined
},
peerQRCode: {
modalOpen: false,
peerConfigData: undefined
},
peerConfigurationFile: {
modalOpen: false,
peerConfigData: undefined
},
peerCreate: {
modalOpen: false
},
peerScheduleJobsAll: {
modalOpen: false
},
peerScheduleJobsLogs: {
modalOpen: false
},
peerShare:{
modalOpen: false,
selectedPeer: undefined
},
editConfiguration: {
modalOpen: false
},
selectPeers: {
modalOpen: false
},
backupRestore: {
modalOpen: false
},
deleteConfiguration: {
modalOpen: false
},
editRawConfigurationFile: {
modalOpen: false
},
peerSearchBarShow: false,
searchStringTimeout: undefined,
searchString: "",
showPeersCount: 10,
showPeersThreshold: 10,
observer: undefined
}
},
watch: {
'$route': {
immediate: true,
handler(){
this.showPeersCount = 20;
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
this.loading = true;
let id = this.$route.params.id;
this.configurationInfo = [];
this.configurationPeers = [];
if (id){
this.getPeers(id)
this.setPeerInterval();
}
}
},
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
this.setPeerInterval();
}
},
beforeRouteLeave(){
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
},
methods:{
toggle(){
this.configurationToggling = true;
fetchGet("/api/toggleWireguardConfiguration/", {
configurationName: this.configurationInfo.Name
}, (res) => {
if (res.status){
this.dashboardConfigurationStore.newMessage("Server",
`${this.configurationInfo.Name} ${res.data ? 'is on':'is off'}`, "success")
}else{
this.dashboardConfigurationStore.newMessage("Server",
res.message, 'danger')
}
this.wireguardConfigurationStore.Configurations.find(x => x.Name === this.configurationInfo.Name).Status = res.data
console.log(this.wireguardConfigurationStore.Configurations.find(x => x.Name === this.configurationInfo.Name).Status)
this.configurationInfo.Status = res.data
this.configurationToggling = false;
})
},
getPeers(id = this.$route.params.id){
fetchGet("/api/getWireguardConfigurationInfo",
{
configurationName: id
}, (res) => {
this.configurationInfo = res.data.configurationInfo;
this.configurationPeers = res.data.configurationPeers;
this.configurationPeers.forEach(x => {
x.restricted = false;
})
res.data.configurationRestrictedPeers.forEach(x => {
x.restricted = true;
this.configurationPeers.push(x)
})
this.loading = false;
if (this.configurationPeers.length > 0){
const sent = this.configurationPeers.map(x => x.total_sent + x.cumu_sent)
.reduce((x,y) => x + y).toFixed(4);
const receive = this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((x,y) => x + y).toFixed(4);
if (
this.historyDataSentDifference[this.historyDataSentDifference.length - 1] !== sent
){
if (this.historyDataSentDifference.length > 0){
this.historySentData = {
labels: [...this.historySentData.labels, dayjs().format("HH:mm:ss A")],
datasets: [
{
label: 'Data Sent',
data: [...this.historySentData.datasets[0].data,
((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000)
.toFixed(4)],
fill: false,
borderColor: '#198754',
tension: 0
}
],
}
}
this.historyDataSentDifference.push(sent)
}
if (
this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1] !== receive
){
if (this.historyDataReceivedDifference.length > 0){
this.historyReceiveData = {
labels: [...this.historyReceiveData.labels, dayjs().format("HH:mm:ss A")],
datasets: [
{
label: 'Data Received',
data: [...this.historyReceiveData.datasets[0].data,
((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000)
.toFixed(4)],
fill: false,
borderColor: '#0d6efd',
tension: 0
}
],
}
}
this.historyDataReceivedDifference.push(receive)
}
}
});
},
setPeerInterval(){
this.dashboardConfigurationStore.Peers.RefreshInterval = setInterval(() => {
this.getPeers()
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
},
},
computed: {
configurationSummary(){
return {
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length,
totalUsage: this.configurationPeers.length > 0 ?
this.configurationPeers.filter(x => !x.restricted)
.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b, 0).toFixed(4) : 0,
totalReceive: this.configurationPeers.length > 0 ?
this.configurationPeers.filter(x => !x.restricted)
.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b, 0).toFixed(4) : 0,
totalSent: this.configurationPeers.length > 0 ?
this.configurationPeers.filter(x => !x.restricted)
.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b, 0).toFixed(4) : 0
}
},
receiveData(){
return this.historyReceiveData
},
sentData(){
return this.historySentData
},
individualDataUsage(){
return {
labels: this.configurationPeers.map(x => {
if (x.name) return x.name
return `Untitled Peer - ${x.id}`
}),
datasets: [{
label: 'Total Data Usage',
data: this.configurationPeers.map(x => x.cumu_data + x.total_data),
backgroundColor: this.configurationPeers.map(x => `#0dcaf0`),
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.formattedValue} GB`
}
}
}
}]
}
},
individualDataUsageChartOption(){
return {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val, index) => {
return `${val} GB`
}
},
grid: {
display: false
},
}
}
}
},
chartOptions() {
return {
responsive: true,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.formattedValue} MB/s`
}
}
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val, index) => {
return `${val} MB/s`
}
},
grid: {
display: false
},
}
}
}
},
searchPeers(){
const fuse = new Fuse(this.configurationPeers, {
keys: ["name", "id", "allowed_ip"]
});
const result = this.wireguardConfigurationStore.searchString ?
this.configurationPeers.filter(x => {
return x.name.includes(this.wireguardConfigurationStore.searchString) ||
x.id.includes(this.wireguardConfigurationStore.searchString) ||
x.allowed_ip.includes(this.wireguardConfigurationStore.searchString)
}) : this.configurationPeers;
if (this.dashboardConfigurationStore.Configuration.Server.dashboard_sort === "restricted"){
return result.sort((a, b) => {
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
< b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort] ){
return 1;
}
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
> b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]){
return -1;
}
return 0;
}).slice(0, this.showPeersCount);
}
return result.sort((a, b) => {
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
< b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort] ){
return -1;
}
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
> b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]){
return 1;
}
return 0;
}).slice(0, this.showPeersCount)
},
}
}
</script>
<template>
<div>
<div v-if="!this.loading" class="container-md" id="peerList">
<div class="d-flex align-items-sm-center flex-column flex-sm-row gap-3">
<div>
<div class="text-muted d-flex align-items-center gap-2">
<h5 class="mb-0">
<ProtocolBadge :protocol="this.configurationInfo.Protocol"></ProtocolBadge>
</h5>
</div>
<div class="d-flex align-items-center gap-3">
<h1 class="mb-0 display-4"><samp>{{this.configurationInfo.Name}}</samp></h1>
</div>
</div>
<div class="ms-sm-auto d-flex gap-2 flex-column">
<div class="card rounded-3 bg-transparent ">
<div class="card-body py-2 d-flex align-items-center">
<small class="text-muted">
<LocaleText t="Status"></LocaleText>
</small>
<div class="dot ms-2" :class="{active: this.configurationInfo.Status}"></div>
<div class="form-check form-switch mb-0 ms-auto pe-0 me-0">
<label class="form-check-label" style="cursor: pointer" :for="'switch' + this.configurationInfo.id">
<LocaleText t="On" v-if="this.configurationInfo.Status && !this.configurationToggling"></LocaleText>
<LocaleText t="Off" v-else-if="!this.configurationInfo.Status && !this.configurationToggling"></LocaleText>
<span v-if="this.configurationToggling"
class="spinner-border spinner-border-sm ms-2" aria-hidden="true">
</span>
</label>
<input class="form-check-input"
style="cursor: pointer"
:disabled="this.configurationToggling"
type="checkbox" role="switch" :id="'switch' + this.configurationInfo.id"
@change="this.toggle()"
v-model="this.configurationInfo.Status"
>
</div>
</div>
</div>
<div class="d-flex gap-2">
<RouterLink
to="create"
class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle ">
<i class="bi bi-plus-lg me-2"></i>
<LocaleText t="Peer"></LocaleText>
</RouterLink>
<button class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle "
@click="editConfiguration.modalOpen = true"
type="button" aria-expanded="false">
<i class="bi bi-gear-fill me-2"></i>
<LocaleText t="Configuration Settings"></LocaleText>
</button>
</div>
</div>
</div>
<hr>
<div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Address"></LocaleText>
</small></p>
{{this.configurationInfo.Address}}
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Listen Port"></LocaleText>
</small></p>
{{this.configurationInfo.ListenPort}}
</div>
</div>
</div>
<div style="word-break: break-all" class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Public Key"></LocaleText>
</small></p>
<samp>{{this.configurationInfo.PublicKey}}</samp>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-2">
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Connected Peers"></LocaleText>
</small></p>
<strong class="h4">
{{configurationSummary.connectedPeers}} / {{configurationPeers.length}}
</strong>
</div>
<i class="bi bi-ethernet ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Usage"></LocaleText>
</small></p>
<strong class="h4">{{configurationSummary.totalUsage}} GB</strong>
</div>
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Received"></LocaleText>
</small></p>
<strong class="h4 text-primary">{{configurationSummary.totalReceive}} GB</strong>
</div>
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Sent"></LocaleText>
</small></p>
<strong class="h4 text-success">{{configurationSummary.totalSent}} GB</strong>
</div>
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-3">
<div class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent " style="height: 270px">
<div class="card-header bg-transparent border-0">
<small class="text-muted">
<LocaleText t="Peers Data Usage"></LocaleText>
</small></div>
<div class="card-body pt-1">
<Bar
:data="individualDataUsage"
:options="individualDataUsageChartOption"
style="width: 100%; height: 200px; max-height: 200px"></Bar>
</div>
</div>
</div>
<div class="col-sm col-lg-3">
<div class="card rounded-3 bg-transparent " style="height: 270px">
<div class="card-header bg-transparent border-0"><small class="text-muted">
<LocaleText t="Real Time Received Data Usage"></LocaleText>
</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="receiveData"
style="width: 100%; height: 200px; max-height: 200px"
></Line>
</div>
</div>
</div>
<div class="col-sm col-lg-3">
<div class="card rounded-3 bg-transparent " style="height: 270px">
<div class="card-header bg-transparent border-0"><small class="text-muted">
<LocaleText t="Real Time Sent Data Usage"></LocaleText>
</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="sentData"
style="width: 100%; height: 200px; max-height: 200px"
></Line>
</div>
</div>
</div>
</div>
<hr>
<div style="margin-bottom: 80px">
<PeerSearch
@search="this.peerSearchBarShow = true"
@jobsAll="this.peerScheduleJobsAll.modalOpen = true"
@jobLogs="this.peerScheduleJobsLogs.modalOpen = true"
@editConfiguration="this.editConfiguration.modalOpen = true"
@selectPeers="this.selectPeers.modalOpen = true"
@backupRestore="this.backupRestore.modalOpen = true"
@deleteConfiguration="this.deleteConfiguration.modalOpen = true"
:configuration="this.configurationInfo"></PeerSearch>
<TransitionGroup name="peerList" tag="div" class="row gx-2 gy-2 z-0 position-relative">
<div class="col-12 col-lg-6 col-xl-4"
:key="peer.id"
v-for="peer in this.searchPeers">
<Peer :Peer="peer"
@share="this.peerShare.selectedPeer = peer.id; this.peerShare.modalOpen = true;"
@refresh="this.getPeers()"
@jobs="peerScheduleJobs.modalOpen = true; peerScheduleJobs.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"
@setting="peerSetting.modalOpen = true; peerSetting.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"
@qrcode="(file) => {this.peerQRCode.peerConfigData = file; this.peerQRCode.modalOpen = true;}"
@configurationFile="(file) => {this.peerConfigurationFile.peerConfigData = file; this.peerConfigurationFile.modalOpen = true;}"
></Peer>
</div>
</TransitionGroup>
</div>
<Transition name="slideUp">
<PeerSearchBar
@close="peerSearchBarShow = false"
v-if="this.peerSearchBarShow"></PeerSearchBar>
</Transition>
<Transition name="zoom">
<PeerSettings v-if="this.peerSetting.modalOpen"
key="settings"
:selectedPeer="this.peerSetting.selectedPeer"
@refresh="this.getPeers()"
@close="this.peerSetting.modalOpen = false">
</PeerSettings>
</Transition>
<Transition name="zoom">
<PeerQRCode :peerConfigData="this.peerQRCode.peerConfigData"
key="qrcode"
@close="this.peerQRCode.modalOpen = false"
v-if="peerQRCode.modalOpen"></PeerQRCode>
</Transition>
<Transition name="zoom">
<PeerJobs
@refresh="this.getPeers()"
v-if="this.peerScheduleJobs.modalOpen"
:selectedPeer="this.peerScheduleJobs.selectedPeer"
@close="this.peerScheduleJobs.modalOpen = false">
</PeerJobs>
</Transition>
<Transition name="zoom">
<PeerJobsAllModal
v-if="this.peerScheduleJobsAll.modalOpen"
@refresh="this.getPeers()"
@allLogs="peerScheduleJobsLogs.modalOpen = true"
@close="this.peerScheduleJobsAll.modalOpen = false"
:configurationPeers="this.configurationPeers"
>
</PeerJobsAllModal>
</Transition>
<Transition name="zoom">
<PeerJobsLogsModal v-if="this.peerScheduleJobsLogs.modalOpen"
@close="this.peerScheduleJobsLogs.modalOpen = false"
:configurationInfo="this.configurationInfo"
>
</PeerJobsLogsModal>
</Transition>
<Transition name="zoom">
<PeerShareLinkModal
v-if="this.peerShare.modalOpen"
@close="this.peerShare.modalOpen = false; this.peerShare.selectedPeer = undefined;"
:peer="this.configurationPeers.find(x => x.id === this.peerShare.selectedPeer)"></PeerShareLinkModal>
</Transition>
<Transition name="zoom">
<EditConfiguration
@editRaw="this.editRawConfigurationFile.modalOpen = true"
@close="this.editConfiguration.modalOpen = false"
@dataChanged="(d) => this.configurationInfo = d"
@backupRestore="this.backupRestore.modalOpen = true"
@deleteConfiguration="this.deleteConfiguration.modalOpen = true"
:configurationInfo="this.configurationInfo"
v-if="this.editConfiguration.modalOpen"></EditConfiguration>
</Transition>
<Transition name="zoom">
<SelectPeers
@refresh="this.getPeers()"
v-if="this.selectPeers.modalOpen"
:configurationPeers="this.configurationPeers"
@close="this.selectPeers.modalOpen = false"
></SelectPeers>
</Transition>
<Transition name="zoom">
<DeleteConfiguration
@backup="backupRestore.modalOpen = true"
@close="deleteConfiguration.modalOpen = false"
v-if="deleteConfiguration.modalOpen"></DeleteConfiguration>
</Transition>
<Transition name="zoom">
<EditRawConfigurationFile
@close="editRawConfigurationFile.modalOpen = false"
v-if="editRawConfigurationFile.modalOpen">
</EditRawConfigurationFile>
</Transition>
<Transition name="zoom">
<ConfigurationBackupRestore
@close="backupRestore.modalOpen = false"
@refreshPeersList="this.getPeers()"
v-if="backupRestore.modalOpen"></ConfigurationBackupRestore>
</Transition>
<Transition name="zoom">
<PeerConfigurationFile
@close="peerConfigurationFile.modalOpen = false"
v-if="peerConfigurationFile.modalOpen"
:configurationFile="peerConfigurationFile.peerConfigData"
></PeerConfigurationFile>
</Transition>
</div>
<PeerIntersectionObserver
:showPeersCount="this.showPeersCount"
:peerListLength="this.searchPeers.length"
@loadMore="this.showPeersCount += this.showPeersThreshold"></PeerIntersectionObserver>
</div>
</template>
<style scoped>
.peerNav .nav-link{
&.active{
background-color: #efefef;
}
}
th, td{
background-color: transparent !important;
}
@media screen and (max-width: 576px) {
.titleBtn{
flex-basis: 100%;
}
}
</style>

View File

@ -54,7 +54,6 @@ const fetchRealtimeTraffic = async () => {
configurationName: route.params.id configurationName: route.params.id
}, (res) => { }, (res) => {
let timestamp = dayjs().format("hh:mm:ss A") let timestamp = dayjs().format("hh:mm:ss A")
if (res.data.sent !== 0 && res.data.recv !== 0){ if (res.data.sent !== 0 && res.data.recv !== 0){
historySentData.value.timestamp.push(timestamp) historySentData.value.timestamp.push(timestamp)
historySentData.value.data.push(res.data.sent) historySentData.value.data.push(res.data.sent)

View File

@ -1,440 +0,0 @@
<script setup async>
import {computed, defineAsyncComponent, onBeforeUnmount, ref, watch} from "vue";
import {useRoute} from "vue-router";
import {fetchGet} from "@/utilities/fetch.js";
import ProtocolBadge from "@/components/protocolBadge.vue";
import LocaleText from "@/components/text/localeText.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import PeerDataUsageCharts from "@/components/configurationComponents/peerListComponents/peerDataUsageCharts.vue";
import PeerSearch from "@/components/configurationComponents/peerSearch.vue";
import Peer from "@/components/configurationComponents/peer.vue";
import PeerListModals from "@/components/configurationComponents/peerListComponents/peerListModals.vue";
import PeerIntersectionObserver from "@/components/configurationComponents/peerIntersectionObserver.vue";
// Async Components
const PeerSearchBar = defineAsyncComponent(() => import("@/components/configurationComponents/peerSearchBar.vue"))
const PeerJobsAllModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsAllModal.vue"))
const PeerJobsLogsModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsLogsModal.vue"))
const EditConfigurationModal = defineAsyncComponent(() => import("@/components/configurationComponents/editConfiguration.vue"))
const SelectPeersModal = defineAsyncComponent(() => import("@/components/configurationComponents/selectPeers.vue"))
const PeerAddModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerAddModal.vue"))
const dashboardStore = DashboardConfigurationStore()
const wireguardConfigurationStore = WireguardConfigurationsStore()
const route = useRoute()
const configurationInfo = ref({})
const configurationPeers = ref([])
const configurationToggling = ref(false)
const configurationModalSelectedPeer = ref({})
const configurationModals = ref({
peerNew: {
modalOpen: false
},
peerSetting: {
modalOpen: false,
},
peerScheduleJobs:{
modalOpen: false,
},
peerQRCode: {
modalOpen: false,
},
peerConfigurationFile: {
modalOpen: false,
},
peerCreate: {
modalOpen: false
},
peerScheduleJobsAll: {
modalOpen: false
},
peerScheduleJobsLogs: {
modalOpen: false
},
peerShare:{
modalOpen: false,
},
editConfiguration: {
modalOpen: false
},
selectPeers: {
modalOpen: false
},
backupRestore: {
modalOpen: false
},
deleteConfiguration: {
modalOpen: false
},
editRawConfigurationFile: {
modalOpen: false
}
})
const peerSearchBar = ref(false)
// Fetch Peer =====================================
const fetchPeerList = async () => {
await fetchGet("/api/getWireguardConfigurationInfo", {
configurationName: route.params.id
}, (res) => {
if (res.status){
configurationInfo.value = res.data.configurationInfo;
configurationPeers.value = res.data.configurationPeers;
configurationPeers.value.forEach(p => {
p.restricted = false
})
res.data.configurationRestrictedPeers.forEach(x => {
x.restricted = true;
configurationPeers.value.push(x)
})
}
})
}
await fetchPeerList()
// Fetch Peer Interval =====================================
const fetchPeerListInterval = ref(undefined)
const setFetchPeerListInterval = () => {
clearInterval(fetchPeerListInterval.value)
fetchPeerListInterval.value = setInterval(async () => {
await fetchPeerList()
}, parseInt(dashboardStore.Configuration.Server.dashboard_refresh_interval))
}
setFetchPeerListInterval()
onBeforeUnmount(() => {
clearInterval(fetchPeerListInterval.value);
fetchPeerListInterval.value = undefined;
})
watch(() => {
return dashboardStore.Configuration.Server.dashboard_refresh_interval
}, () => {
setFetchPeerListInterval()
})
// Toggle Configuration Method =====================================
const toggleConfiguration = async () => {
configurationToggling.value = true;
await fetchGet("/api/toggleWireguardConfiguration/", {
configurationName: configurationInfo.value.Name
}, (res) => {
if (res.status){
dashboardStore.newMessage("Server",
`${configurationInfo.value.Name} ${res.data ? 'is on':'is off'}`, "success")
}else{
dashboardStore.newMessage("Server", res.message, 'danger')
}
wireguardConfigurationStore.Configurations
.find(x => x.Name === configurationInfo.value.Name).Status = res.data
configurationInfo.value.Status = res.data
configurationToggling.value = false;
})
}
// Configuration Summary =====================================
const configurationSummary = computed(() => {
return {
connectedPeers: configurationPeers.value.filter(x => x.status === "running").length,
totalUsage: configurationPeers.value.length > 0 ?
configurationPeers.value.filter(x => !x.restricted)
.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b, 0).toFixed(4) : 0,
totalReceive: configurationPeers.value.length > 0 ?
configurationPeers.value.filter(x => !x.restricted)
.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b, 0).toFixed(4) : 0,
totalSent: configurationPeers.value.length > 0 ?
configurationPeers.value.filter(x => !x.restricted)
.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b, 0).toFixed(4) : 0
}
})
const showPeersCount = ref(10)
const showPeersThreshold = 20;
const searchPeers = computed(() => {
const result = wireguardConfigurationStore.searchString ?
configurationPeers.value.filter(x => {
return x.name.includes(wireguardConfigurationStore.searchString) ||
x.id.includes(wireguardConfigurationStore.searchString) ||
x.allowed_ip.includes(wireguardConfigurationStore.searchString)
}) : configurationPeers.value;
if (dashboardStore.Configuration.Server.dashboard_sort === "restricted"){
return result.sort((a, b) => {
if ( a[dashboardStore.Configuration.Server.dashboard_sort]
< b[dashboardStore.Configuration.Server.dashboard_sort] ){
return 1;
}
if ( a[dashboardStore.Configuration.Server.dashboard_sort]
> b[dashboardStore.Configuration.Server.dashboard_sort]){
return -1;
}
return 0;
}).slice(0, showPeersCount.value);
}
return result.sort((a, b) => {
if ( a[dashboardStore.Configuration.Server.dashboard_sort]
< b[dashboardStore.Configuration.Server.dashboard_sort] ){
return -1;
}
if ( a[dashboardStore.Configuration.Server.dashboard_sort]
> b[dashboardStore.Configuration.Server.dashboard_sort]){
return 1;
}
return 0;
}).slice(0, showPeersCount.value)
})
</script>
<template>
<div class="container-fluid" >
<div class="d-flex align-items-sm-center flex-column flex-sm-row gap-3">
<div>
<div class="text-muted d-flex align-items-center gap-2">
<h5 class="mb-0">
<ProtocolBadge :protocol="configurationInfo.Protocol"></ProtocolBadge>
</h5>
</div>
<div class="d-flex align-items-center gap-3">
<h1 class="mb-0 display-4"><samp>{{configurationInfo.Name}}</samp></h1>
</div>
</div>
<div class="ms-sm-auto d-flex gap-2 flex-column">
<div class="card rounded-3 bg-transparent ">
<div class="card-body py-2 d-flex align-items-center">
<small class="text-muted">
<LocaleText t="Status"></LocaleText>
</small>
<div class="dot ms-2" :class="{active: configurationInfo.Status}"></div>
<div class="form-check form-switch mb-0 ms-auto pe-0 me-0">
<label class="form-check-label" style="cursor: pointer" :for="'switch' + configurationInfo.id">
<LocaleText t="On" v-if="configurationInfo.Status && !configurationToggling"></LocaleText>
<LocaleText t="Off" v-else-if="!configurationInfo.Status && !configurationToggling"></LocaleText>
<span v-if="configurationToggling"
class="spinner-border spinner-border-sm ms-2" aria-hidden="true">
</span>
</label>
<input class="form-check-input"
style="cursor: pointer"
:disabled="configurationToggling"
type="checkbox" role="switch" :id="'switch' + configurationInfo.id"
@change="toggleConfiguration()"
v-model="configurationInfo.Status">
</div>
</div>
</div>
<div class="d-flex gap-2">
<a
role="button"
@click="configurationModals.peerNew.modalOpen = true"
class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle ">
<i class="bi bi-plus-circle me-2"></i>
<LocaleText t="Peer"></LocaleText>
</a>
<button class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle "
@click="configurationModals.editConfiguration.modalOpen = true"
type="button" aria-expanded="false">
<i class="bi bi-gear-fill me-2"></i>
<LocaleText t="Configuration Settings"></LocaleText>
</button>
</div>
</div>
</div>
<hr>
<div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Address"></LocaleText>
</small></p>
{{configurationInfo.Address}}
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Listen Port"></LocaleText>
</small></p>
{{configurationInfo.ListenPort}}
</div>
</div>
</div>
<div style="word-break: break-all" class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body py-2 d-flex flex-column justify-content-center">
<p class="mb-0 text-muted"><small>
<LocaleText t="Public Key"></LocaleText>
</small></p>
<samp>{{configurationInfo.PublicKey}}</samp>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-2">
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Connected Peers"></LocaleText>
</small></p>
<strong class="h4">
{{configurationSummary.connectedPeers}} / {{configurationPeers.length}}
</strong>
</div>
<i class="bi bi-ethernet ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Usage"></LocaleText>
</small></p>
<strong class="h4">{{configurationSummary.totalUsage}} GB</strong>
</div>
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Received"></LocaleText>
</small></p>
<strong class="h4 text-primary">{{configurationSummary.totalReceive}} GB</strong>
</div>
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Total Sent"></LocaleText>
</small></p>
<strong class="h4 text-success">{{configurationSummary.totalSent}} GB</strong>
</div>
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
</div>
<PeerDataUsageCharts
:configurationPeers="configurationPeers"
:configurationInfo="configurationInfo"
></PeerDataUsageCharts>
<hr>
<div style="margin-bottom: 80px">
<PeerSearch
v-if="configurationPeers.length > 0"
@search="peerSearchBar = true"
@jobsAll="configurationModals.peerScheduleJobsAll.modalOpen = true"
@jobLogs="configurationModals.peerScheduleJobsLogs.modalOpen = true"
@editConfiguration="configurationModals.editConfiguration.modalOpen = true"
@selectPeers="configurationModals.selectPeers.modalOpen = true"
@backupRestore="configurationModals.backupRestore.modalOpen = true"
@deleteConfiguration="configurationModals.deleteConfiguration.modalOpen = true"
:configuration="configurationInfo">
</PeerSearch>
<TransitionGroup name="peerList" tag="div" class="row gx-2 gy-2 z-0 position-relative">
<div class="col-12 col-lg-6 col-xl-4"
:key="peer.id"
v-for="peer in searchPeers">
<Peer :Peer="peer"
@share="configurationModals.peerShare.modalOpen = true; configurationModalSelectedPeer = peer"
@refresh="fetchPeerList()"
@jobs="configurationModals.peerScheduleJobs.modalOpen = true; configurationModalSelectedPeer = peer"
@setting="configurationModals.peerSetting.modalOpen = true; configurationModalSelectedPeer = peer"
@qrcode="configurationModalSelectedPeer = peer; configurationModals.peerQRCode.modalOpen = true;"
@configurationFile="configurationModalSelectedPeer = peer; configurationModals.peerConfigurationFile.modalOpen = true;"
></Peer>
</div>
</TransitionGroup>
</div>
<Transition name="slideUp">
<PeerSearchBar @close="peerSearchBar = false" v-if="peerSearchBar"></PeerSearchBar>
</Transition>
<PeerListModals
:configurationModals="configurationModals"
:configurationModalSelectedPeer="configurationModalSelectedPeer"
@refresh="fetchPeerList()"
></PeerListModals>
<TransitionGroup name="zoom">
<Suspense key="PeerAddModal">
<PeerAddModal
v-if="configurationModals.peerNew.modalOpen"
@close="configurationModals.peerNew.modalOpen = false"
@addedPeers="configurationModals.peerNew.modalOpen = false; fetchPeerList()"
></PeerAddModal>
</Suspense>
<PeerJobsAllModal
key="PeerJobsAllModal"
v-if="configurationModals.peerScheduleJobsAll.modalOpen"
@refresh="fetchPeerList()"
@allLogs="configurationModals.peerScheduleJobsLogs.modalOpen = true"
@close="configurationModals.peerScheduleJobsAll.modalOpen = false"
:configurationPeers="configurationPeers"
>
</PeerJobsAllModal>
<PeerJobsLogsModal
key="PeerJobsLogsModal"
v-if="configurationModals.peerScheduleJobsLogs.modalOpen"
@close="configurationModals.peerScheduleJobsLogs.modalOpen = false"
:configurationInfo="configurationInfo">
</PeerJobsLogsModal>
<EditConfigurationModal
key="EditConfigurationModal"
@editRaw="configurationModals.editRawConfigurationFile.modalOpen = true"
@close="configurationModals.editConfiguration.modalOpen = false"
@dataChanged="(d) => configurationInfo = d"
@refresh="fetchPeerList()"
@backupRestore="configurationModals.backupRestore.modalOpen = true"
@deleteConfiguration="configurationModals.deleteConfiguration.modalOpen = true"
:configurationInfo="configurationInfo"
v-if="configurationModals.editConfiguration.modalOpen">
</EditConfigurationModal>
<SelectPeersModal
@refresh="fetchPeerList()"
v-if="configurationModals.selectPeers.modalOpen"
:configurationPeers="configurationPeers"
@close="configurationModals.selectPeers.modalOpen = false"
></SelectPeersModal>
</TransitionGroup>
<PeerIntersectionObserver
:showPeersCount="showPeersCount"
:peerListLength="searchPeers.length"
@loadMore="showPeersCount += showPeersThreshold"></PeerIntersectionObserver>
</div>
</template>
<style scoped>
.peerNav .nav-link{
&.active{
background-color: #efefef;
}
}
th, td{
background-color: transparent !important;
}
@media screen and (max-width: 576px) {
.titleBtn{
flex-basis: 100%;
}
}
</style>

View File

@ -91,13 +91,8 @@ const router = createRouter({
{ {
name: "Peers List", name: "Peers List",
path: 'peers', path: 'peers',
component: () => import('@/components/configurationComponents/peerListNew.vue') component: () => import('@/components/configurationComponents/peerList.vue')
}, }
{
name: "Peers Create",
path: 'create',
component: () => import('@/components/configurationComponents/peerCreate.vue')
},
] ]
}, },

View File

@ -28,21 +28,23 @@ export const fetchGet = async (url, params=undefined, callback=undefined) => {
await fetch(`${getUrl(url)}?${urlSearchParams.toString()}`, { await fetch(`${getUrl(url)}?${urlSearchParams.toString()}`, {
headers: getHeaders() headers: getHeaders()
}) })
.then((x) => { .then((x) => {
const store = DashboardConfigurationStore(); const store = DashboardConfigurationStore();
if (!x.ok){ if (!x.ok){
if (x.status !== 200){ if (x.status !== 200){
if (x.status === 401){ if (x.status === 401){
store.newMessage("WGDashboard", "Sign in session ended, please sign in again", "warning") store.newMessage("WGDashboard", "Sign in session ended, please sign in again", "warning")
}
throw new Error(x.statusText)
} }
throw new Error(x.statusText) }else{
return x.json()
} }
}else{ })
return x.json() .then(x => callback ? callback(x) : undefined).catch(x => {
} console.log("Error:", x)
}).then(x => callback ? callback(x) : undefined).catch(x => { // store.newMessage("WGDashboard", `Error: ${x}`, "danger")
console.log(x) router.push({path: '/signin'})
router.push({path: '/signin'})
}) })
} }
@ -64,7 +66,8 @@ export const fetchPost = async (url, body, callback) => {
return x.json() return x.json()
} }
}).then(x => callback ? callback(x) : undefined).catch(x => { }).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x) console.log("Error:", x)
// store.newMessage("WGDashboard", `Error: ${x}`, "danger")
router.push({path: '/signin'}) router.push({path: '/signin'})
}) })
} }