1
0
mirror of https://github.com/donaldzou/WGDashboard.git synced 2024-11-06 16:00:28 +01:00

Made some progress ;)

This commit is contained in:
Donald Zou 2024-05-20 22:28:52 +08:00
parent 41e05ddf9c
commit c7ca20b45a
16 changed files with 250 additions and 81 deletions

View File

@ -35,9 +35,6 @@ from icmplib import ping, traceroute
# Import other python files
import threading
from sqlalchemy.orm import mapped_column, declarative_base, Session
from sqlalchemy import FLOAT, INT, VARCHAR, select, MetaData, DATETIME
from sqlalchemy import create_engine, inspect
from flask.json.provider import DefaultJSONProvider
DASHBOARD_VERSION = 'v4.0'
@ -141,7 +138,6 @@ class WireguardConfiguration:
self.Status = self.getStatus()
else:
self.Name = data["ConfigurationName"]
for i in dir(self):
@ -168,7 +164,7 @@ class WireguardConfiguration:
# print(self.__parser.sections())
self.__parser.write(configFile)
self.Peers = []
self.Peers: list[Peer] = []
# Create tables in database
self.__createDatabase()
@ -313,6 +309,30 @@ class WireguardConfiguration:
return True, i
return False, None
def deletePeers(self, listOfPublicKeys):
numOfDeletedPeers = 0
numOfFailedToDeletePeers = 0
for p in listOfPublicKeys:
found, pf = self.searchPeer(p)
if found:
try:
subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove",
shell=True, stderr=subprocess.STDOUT)
cursor.execute("DELETE FROM %s WHERE id = ?" % self.Name, (pf.id,))
numOfDeletedPeers += 1
except Exception as e:
numOfFailedToDeletePeers += 1
if not self.__wgSave():
return ResponseObject(False, "Failed to save configuration through WireGuard")
self.__getPeers()
if numOfDeletedPeers == len(listOfPublicKeys):
return ResponseObject(True, f"Deleted {numOfDeletedPeers} peer(s)")
return ResponseObject(False,
f"Deleted {numOfDeletedPeers} peer(s) successfully. Failed to delete {numOfFailedToDeletePeers} peer(s)")
def __savePeers(self):
for i in self.Peers:
d = i.toJson()
@ -329,6 +349,13 @@ class WireguardConfiguration:
)
sqldb.commit()
def __wgSave(self) -> tuple[bool, str] | tuple[bool, None]:
try:
subprocess.check_output(f"wg-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
return True, None
except subprocess.CalledProcessError as e:
return False, str(e)
def getPeersLatestHandshake(self):
try:
latestHandshake = subprocess.check_output(f"wg show {self.Name} latest-handshakes",
@ -383,22 +410,25 @@ class WireguardConfiguration:
data_usage[i][0],))
total_sent = 0
total_receive = 0
_, p = self.searchPeer(data_usage[i][0])
if p.total_receive != round(total_receive, 4) or p.total_sent != round(total_sent, 4):
cursor.execute(
"UPDATE %s SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?"
% self.Name, (round(total_receive, 4), round(total_sent, 4),
round(total_receive + total_sent, 4), data_usage[i][0],))
now = datetime.now()
now_string = now.strftime("%d/%m/%Y %H:%M:%S")
cursor.execute(f'''
INSERT INTO %s_transfer
(id, total_receive, total_sent, total_data,
cumu_receive, cumu_sent, cumu_data, time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''' % self.Name, (data_usage[i][0], round(total_receive, 4), round(total_sent, 4),
round(total_receive + total_sent, 4), round(cumulative_receive, 4),
round(cumulative_sent, 4),
round(cumulative_sent + cumulative_receive, 4), now_string,))
sqldb.commit()
# cursor.execute(f'''
# INSERT INTO %s_transfer
# (id, total_receive, total_sent, total_data,
# cumu_receive, cumu_sent, cumu_data, time)
# VALUES (?, ?, ?, ?, ?, ?, ?, ?)
# ''' % self.Name, (data_usage[i][0], round(total_receive, 4), round(total_sent, 4),
# round(total_receive + total_sent, 4), round(cumulative_receive, 4),
# round(cumulative_sent, 4),
# round(cumulative_sent + cumulative_receive, 4), now_string,))
# sqldb.commit()
except Exception as e:
print("Error" + str(e))
@ -837,9 +867,14 @@ def auth_req():
and "getDashboardConfiguration" not in request.path and "getDashboardTheme" not in request.path
and "isTotpEnabled" not in request.path
):
resp = Flask.make_response(app, "Not Authorized" + request.path)
resp.status_code = 401
return resp
response = Flask.make_response(app, {
"status": False,
"message": None,
"data": None
})
response.content_type = "application/json"
response.status_code = 401
return response
@app.route('/api/validateAuthentication', methods=["GET"])
@ -999,6 +1034,19 @@ def API_updatePeerSettings(configName):
return ResponseObject(False, "Peer does not exist")
@app.route('/api/deletePeers/<configName>', methods=['POST'])
def API_deletePeers(configName: str) -> ResponseObject:
data = request.get_json()
peers = data['peers']
if configName in WireguardConfigurations.keys():
if len(peers) == 0:
return ResponseObject(False, "Please specify more than one peer")
configuration = WireguardConfigurations.get(configName)
return configuration.deletePeers(peers)
return ResponseObject(False, "Configuration does not exist")
@app.route('/api/addPeers/<configName>', methods=['POST'])
def API_addPeers(configName):
data = request.get_json()
@ -1055,8 +1103,6 @@ def API_addPeers(configName):
return ResponseObject()
else:
if config.searchPeer(public_key)[0] is True:
return ResponseObject(False, f"This peer already exist.")
@ -1231,7 +1277,7 @@ def backGroundThread():
if __name__ == "__main__":
engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'))
# engine = create_engine("sqlite:///" + os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'))
sqldb = sqlite3.connect(os.path.join(CONFIGURATION_PATH, 'db', 'wgdashboard.db'), check_same_thread=False)
sqldb.row_factory = sqlite3.Row
cursor = sqldb.cursor()

View File

@ -5,3 +5,5 @@ pyotp
flask
icmplib
sqlalchemy
flask[async]
aiosqlite

View File

@ -936,3 +936,7 @@ pre.index-alert {
.theme-switch-btn{
width: 100%;
}
.dropdown-item.disabled, .dropdown-item:disabled{
opacity: 0.7;
}

View File

@ -1,6 +1,7 @@
<script>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'
import "animate.css"
import PeerSettingsDropdown from "@/components/configurationComponents/peerSettingsDropdown.vue";
export default {
name: "peer",
@ -76,6 +77,7 @@ export default {
<PeerSettingsDropdown
@qrcode="(file) => this.$emit('qrcode', file)"
@setting="this.$emit('setting')"
@refresh="this.$emit('refresh')"
:Peer="Peer"
v-if="this.subMenuOpened"
ref="target"

View File

@ -100,13 +100,14 @@ export default {
<template>
<div class="container">
<div class="mb-4 d-flex align-items-center gap-4">
<RouterLink to="peers">
<div class="mb-4">
<RouterLink to="peers" is="div" class="d-flex align-items-center gap-4 text-decoration-none">
<h3 class="mb-0 text-body">
<i class="bi bi-chevron-left"></i>
</h3>
<h3 class="text-body mb-0">Add Peers</h3>
</RouterLink>
<h3 class="text-body mb-0">New Configuration</h3>
</div>
<div class="d-flex flex-column gap-2">
<BulkAdd :saving="saving" :data="this.data" :availableIp="this.availableIp"></BulkAdd>

View File

@ -73,6 +73,7 @@ export default {
},
data(){
return {
configurationToggling: false,
loading: false,
error: null,
configurationInfo: [],
@ -151,7 +152,24 @@ export default {
clearInterval(this.interval)
},
methods:{
getPeers(id){
toggle(){
this.configurationToggling = true;
fetchGet("/api/toggleWireguardConfiguration/", {
configurationName: this.configurationInfo.Name
}, (res) => {
if (res.status){
this.dashboardConfigurationStore.newMessage("Server",
`${this.configurationInfo.Name} is
${res.data ? 'is on':'is off'}`, "Success")
}else{
this.dashboardConfigurationStore.newMessage("Server",
res.message, 'danger')
}
this.configurationInfo.Status = res.data
this.configurationToggling = false;
})
},
getPeers(id = this.$route.params.id){
fetchGet("/api/getWireguardConfigurationInfo",
{
configurationName: id
@ -207,7 +225,7 @@ export default {
},
setInterval(){
this.interval = setInterval(() => {
this.getPeers(this.$route.params.id)
this.getPeers()
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
}
},
@ -338,14 +356,39 @@ export default {
<template>
<div v-if="!this.loading">
<div class="d-flex align-items-center">
<div>
<small CLASS="text-muted">CONFIGURATION</small>
<div class="d-flex align-items-center gap-3">
<h1 class="mb-0"><samp>{{this.configurationInfo.Name}}</samp></h1>
<div class="dot active ms-0"></div>
</div>
</div>
<div class="card rounded-3 bg-transparent shadow-sm ms-auto">
<div class="card-body py-2 d-flex align-items-center">
<div>
<p class="mb-0 text-muted"><small>Status</small></p>
<div class="form-check form-switch ms-auto">
<label class="form-check-label" style="cursor: pointer" :for="'switch' + this.configurationInfo.id">
{{this.configurationToggling ? 'Turning ':''}}
{{this.configurationInfo.Status ? "On":"Off"}}
<span v-if="this.configurationToggling"
class="spinner-border spinner-border-sm" 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 class="dot ms-5" :class="{active: this.configurationInfo.Status}"></div>
</div>
</div>
</div>
<div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
@ -419,36 +462,36 @@ export default {
</div>
<div class="row gx-2 gy-2 mb-5">
<div class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card rounded-3 bg-transparent shadow-sm" style="height: 270px">
<div class="card-header bg-transparent border-0"><small class="text-muted">Peers Total Data Usage</small></div>
<div class="card-body pt-1">
<Bar
:data="individualDataUsage"
:options="individualDataUsageChartOption"
style="height: 200px; width: 100%"></Bar>
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 shadow-sm">
<div class="card rounded-3 bg-transparent shadow-sm" style="height: 270px">
<div class="card-header bg-transparent border-0"><small class="text-muted">Real Time Received Data Usage</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="receiveData"
style="width: 100%; height: 200px"
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 shadow-sm">
<div class="card rounded-3 bg-transparent shadow-sm" style="height: 270px">
<div class="card-header bg-transparent border-0"><small class="text-muted">Real Time Sent Data Usage</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="sentData"
style="width: 100%; height: 200px"
style="width: 100%; height: 200px; max-height: 200px"
></Line>
</div>
</div>
@ -457,10 +500,11 @@ export default {
<div class="mb-4">
<div class="d-flex align-items-center gap-3 mb-2 ">
<h3>Peers</h3>
<RouterLink
to="create"
class="text-decoration-none ms-auto">
<i class="bi bi-plus-circle-fill me-2"></i>Add Peer</RouterLink>
class="text-decoration-none ms-auto btn btn-primary rounded-3">
<i class="bi bi-plus-circle-fill me-2"></i>Peers</RouterLink>
</div>
<PeerSearch></PeerSearch>
<TransitionGroup name="list" tag="div" class="row gx-2 gy-2 z-0">
@ -468,6 +512,7 @@ export default {
:key="peer.id"
v-for="peer in this.searchPeers">
<Peer :Peer="peer"
@refresh="this.getPeers()"
@setting="peerSetting.modalOpen = true; peerSetting.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"
@qrcode="(file) => {this.peerQRCode.peerConfigData = file; this.peerQRCode.modalOpen = true;}"
></Peer>
@ -477,7 +522,7 @@ export default {
<Transition name="fade">
<PeerSettings v-if="this.peerSetting.modalOpen"
:selectedPeer="this.peerSetting.selectedPeer"
@refresh="this.getPeers(this.$route.params.id)"
@refresh="this.getPeers()"
@close="this.peerSetting.modalOpen = false">
</PeerSettings>
@ -487,10 +532,6 @@ export default {
@close="this.peerQRCode.modalOpen = false"
v-if="peerQRCode.modalOpen"></PeerQRCode>
</Transition>
<!-- <Transition name="fade">-->
<!-- -->
<!-- </Transition>-->
</div>
</template>

View File

@ -53,15 +53,7 @@ export default {
}
},
mounted() {
let fadeIn = "animate__fadeInUp";
let fadeOut = "animate__fadeOutDown"
this.$el.querySelectorAll(".dropdown").forEach(x => {
x.addEventListener('show.bs.dropdown', (e) => {
console.log(e.target.parentNode.children)
console.log(e.target.closest("ul.dropdown-menu"))
})
})
}
}
</script>
@ -73,34 +65,43 @@ export default {
<i class="bi bi-filter-circle me-2"></i>
Sort
</button>
<ul class="dropdown-menu mt-2 shadow">
<ul class="dropdown-menu mt-2 shadow rounded-3">
<li v-for="(value, key) in this.sort">
<a class="dropdown-item d-flex" role="button" @click="this.updateSort(key)">
<span class="me-auto">{{value}}</span>
<i class="bi bi-check"
<i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_sort === key"></i>
</a></li>
</a>
</li>
</ul>
</div>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle rounded-3" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-arrow-repeat me-2"></i>Refresh Interval
</button>
<ul class="dropdown-menu shadow mt-2">
<ul class="dropdown-menu shadow mt-2 rounded-3">
<li v-for="(value, key) in this.interval">
<a class="dropdown-item d-flex" role="button" @click="updateRefreshInterval(key)">
<span class="me-auto">{{value}}</span>
<i class="bi bi-check"
<i class="bi bi-check text-primary"
v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i>
</a></li>
</ul>
</div>
<!-- <button class="btn btn-outline-secondary btn-sm rounded-3" type="button"-->
<!-- @click="this.store.Peers.Selecting = !this.store.Peers.Selecting"-->
<!-- >-->
<!-- <i class="bi bi-app-indicator me-2"></i>-->
<!-- Select-->
<!-- </button>-->
<div class="ms-auto d-flex align-items-center">
<label class="d-flex me-2 text-muted" for="searchPeers"><i class="bi bi-search me-1"></i></label>
<input class="form-control form-control-sm rounded-3"
id="searchPeers"
v-model="this.wireguardConfigurationStore.searchString">
</div>
</div>
</template>

View File

@ -1,5 +1,5 @@
<script>
import {fetchGet} from "@/utilities/fetch.js";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
@ -11,6 +11,11 @@ export default {
props: {
Peer: Object
},
data(){
return{
deleteBtnDisabled: false
}
},
methods: {
downloadPeer(){
fetchGet("/api/downloadPeer/"+this.$route.params.id, {
@ -40,6 +45,16 @@ export default {
this.dashboardStore.newMessage("Server", res.message, "danger")
}
})
},
deletePeer(){
this.deleteBtnDisabled = true
fetchPost(`/api/deletePeers/${this.$route.params.id}`, {
peers: [this.Peer.id]
}, (res) => {
this.dashboardStore.newMessage("Server", res.message, res.status ? "success":"danger")
this.$emit("refresh")
this.deleteBtnDisabled = false
})
}
}
}
@ -90,9 +105,10 @@ export default {
</li>
<li>
<a class="dropdown-item d-flex fw-bold text-danger"
@click="this.deletePeer()"
:class="{disabled: this.deleteBtnDisabled}"
role="button">
<i class="me-auto bi bi-trash"></i> Delete
<i class="me-auto bi bi-trash"></i> {{!this.deleteBtnDisabled ? "Delete":"Deleting..."}}
</a>
</li>
</ul>
@ -102,4 +118,8 @@ export default {
.dropdown-menu{
right: 1rem;
}
.dropdown-item.disabled, .dropdown-item:disabled{
opacity: 0.7;
}
</style>

View File

@ -8,13 +8,23 @@ export default {
components: {ConfigurationCard},
async setup(){
const wireguardConfigurationsStore = WireguardConfigurationsStore();
await wireguardConfigurationsStore.getConfigurations();
return {wireguardConfigurationsStore}
},
data(){
return {
configurationLoaded: false
}
},
async mounted() {
await this.wireguardConfigurationsStore.getConfigurations();
this.configurationLoaded = true;
}
}
</script>
<template>
<div class="mt-4">
<div class="container">
<div class="d-flex mb-4 ">
@ -24,13 +34,21 @@ export default {
<i class="bi bi-plus-circle-fill ms-2"></i>
</RouterLink>
</div>
<p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0">You don't have any WireGuard configurations yet. Please check the configuration folder or change it in "Settings". By default the folder is "/etc/wireguard".</p>
<Transition name="fade" mode="out-in">
<div v-if="this.configurationLoaded">
<p class="text-muted" v-if="this.wireguardConfigurationsStore.Configurations.length === 0">
You don't have any WireGuard configurations yet. Please check the configuration folder or change it in "Settings". By default the folder is "/etc/wireguard".
</p>
<div class="d-flex gap-3 flex-column" v-else >
<div class="d-flex gap-3 flex-column" v-else>
<ConfigurationCard v-for="c in this.wireguardConfigurationsStore.Configurations" :key="c.Name" :c="c"></ConfigurationCard>
</div>
</div>
</Transition>
</div>
</div>
</template>
<style scoped>

View File

@ -13,12 +13,18 @@ export default {
}
},
data(){
return{
configurationToggling: false
}
},
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
return {dashboardConfigurationStore}
},
methods: {
toggle(){
this.configurationToggling = true;
fetchGet("/api/toggleWireguardConfiguration/", {
configurationName: this.c.Name
}, (res) => {
@ -30,6 +36,7 @@ export default {
res.message, 'danger')
}
this.c.Status = res.data
this.configurationToggling = false;
})
}
}
@ -53,10 +60,15 @@ export default {
<samp style="line-break: anywhere">{{c.PublicKey}}</samp>
</small>
<div class="form-check form-switch ms-auto">
<label class="form-check-label" :for="'switch' + c.PrivateKey">
<label class="form-check-label" style="cursor: pointer" :for="'switch' + c.PrivateKey">
{{this.configurationToggling ? 'Turning ':''}}
{{c.Status ? "On":"Off"}}
<span v-if="this.configurationToggling"
class="spinner-border spinner-border-sm" aria-hidden="true"></span>
</label>
<input class="form-check-input"
style="cursor: pointer"
:disabled="this.configurationToggling"
type="checkbox" role="switch" :id="'switch' + c.PrivateKey"
@change="this.toggle()"
v-model="c.Status"

View File

@ -117,8 +117,10 @@ router.beforeEach(async (to, from, next) => {
if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){
await wireguardConfigurationsStore.getConfigurations();
}
dashboardConfigurationStore.Redirect = undefined;
next()
}else{
dashboardConfigurationStore.Redirect = to;
next("/signin")
}
}else {

View File

@ -4,8 +4,12 @@ import {v4} from "uuid";
export const DashboardConfigurationStore = defineStore('DashboardConfigurationStore', {
state: () => ({
Redirect: undefined,
Configuration: undefined,
Messages: []
Messages: [],
Peers: {
Selecting: false
}
}),
actions: {
async getConfiguration(){

View File

@ -19,6 +19,7 @@ export const WireguardConfigurationsStore = defineStore('WireguardConfigurations
},
checkCIDR(ip){
return isCidr(ip) !== 0
}
},
}
});

View File

@ -7,7 +7,12 @@ export const fetchGet = async (url, params=undefined, callback=undefined) => {
})
.then(x => x.json())
.then(x => callback ? callback(x) : undefined)
.catch(x => {
// let router = useRouter()
// if (x.status === 401){
// router.push('/signin')
// }
})
}
export const fetchPost = async (url, body, callback) => {

View File

@ -8,7 +8,9 @@ export default {
<div class="mt-5 text-body">
<RouterView v-slot="{ Component, route }">
<Transition name="fade2" mode="out-in">
<Suspense>
<Component :is="Component" :key="route.path"></Component>
</Suspense>
</Transition>
</RouterView>
</div>

View File

@ -40,9 +40,13 @@ export default {
this.$refs["signInBtn"].classList.add("signedIn")
if (response.message){
this.$router.push('/welcome')
}else{
if (this.store.Redirect !== undefined){
this.$router.push(this.store.Redirect)
}else{
this.$router.push('/')
}
}
}else{
this.loginError = true;
this.loginErrorMessage = response.message;
@ -71,7 +75,7 @@ export default {
</script>
<template>
<div class="container-fluid login-container-fluid d-flex main" :data-bs-theme="this.theme">
<div class="container-fluid login-container-fluid d-flex main flex-column" :data-bs-theme="this.theme">
<div class="login-box m-auto" style="width: 500px;">
<h4 class="mb-0 text-body">Welcome to</h4>
<span class="dashboardLogo display-3">WGDashboard</span>
@ -117,6 +121,10 @@ export default {
</form>
</div>
</div>
<small class="text-muted pb-3 d-block w-100 text-center">
WGDashboard v4.0 | Developed with by
<a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
</small>
</div>
</template>