mirror of
https://github.com/h44z/wg-portal.git
synced 2026-01-29 06:36:24 +00:00
86
frontend/src/helpers/websocket-wrapper.js
Normal file
86
frontend/src/helpers/websocket-wrapper.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { peerStore } from '@/stores/peers';
|
||||
import { interfaceStore } from '@/stores/interfaces';
|
||||
import { authStore } from '@/stores/auth';
|
||||
|
||||
let socket = null;
|
||||
let reconnectTimer = null;
|
||||
let failureCount = 0;
|
||||
|
||||
export const websocketWrapper = {
|
||||
connect() {
|
||||
if (socket) {
|
||||
console.log('WebSocket already connected, re-using existing connection.');
|
||||
return;
|
||||
}
|
||||
|
||||
const protocol = WGPORTAL_BACKEND_BASE_URL.startsWith('https://') ? 'wss://' : 'ws://';
|
||||
const baseUrl = WGPORTAL_BACKEND_BASE_URL.replace(/^https?:\/\//, '');
|
||||
const url = `${protocol}${baseUrl}/ws`;
|
||||
|
||||
socket = new WebSocket(url);
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
failureCount = 0;
|
||||
if (reconnectTimer) {
|
||||
clearInterval(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
failureCount++;
|
||||
socket = null;
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
failureCount++;
|
||||
socket.close();
|
||||
socket = null;
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
switch (message.type) {
|
||||
case 'peer_stats':
|
||||
peerStore().updatePeerTrafficStats(message.data);
|
||||
break;
|
||||
case 'interface_stats':
|
||||
interfaceStore().updateInterfaceTrafficStats(message.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
if (reconnectTimer) {
|
||||
clearInterval(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
failureCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
scheduleReconnect() {
|
||||
if (reconnectTimer) return;
|
||||
if (!authStore().IsAuthenticated) return; // Don't reconnect if not logged in
|
||||
|
||||
reconnectTimer = setInterval(() => {
|
||||
if (failureCount > 2) {
|
||||
console.log('WebSocket connection unavailable, giving up.');
|
||||
clearInterval(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Attempting to reconnect WebSocket...');
|
||||
this.connect();
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||
import { websocketWrapper } from '@/helpers/websocket-wrapper'
|
||||
import router from '../router'
|
||||
import { browserSupportsWebAuthn,startRegistration,startAuthentication } from '@simplewebauthn/browser';
|
||||
import {base64_url_encode} from "@/helpers/encoding";
|
||||
@@ -295,9 +296,11 @@ export const authStore = defineStore('auth',{
|
||||
}
|
||||
}
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
websocketWrapper.connect()
|
||||
} else {
|
||||
this.user = null
|
||||
localStorage.removeItem('user')
|
||||
websocketWrapper.disconnect()
|
||||
}
|
||||
},
|
||||
setWebAuthnCredentials(credentials) {
|
||||
|
||||
@@ -14,6 +14,7 @@ export const interfaceStore = defineStore('interfaces', {
|
||||
configuration: "",
|
||||
selected: "",
|
||||
fetching: false,
|
||||
trafficStats: {},
|
||||
}),
|
||||
getters: {
|
||||
Count: (state) => state.interfaces.length,
|
||||
@@ -24,6 +25,9 @@ export const interfaceStore = defineStore('interfaces', {
|
||||
},
|
||||
GetSelected: (state) => state.interfaces.find((i) => i.Identifier === state.selected) || state.interfaces[0],
|
||||
isFetching: (state) => state.fetching,
|
||||
TrafficStats: (state) => {
|
||||
return (state.selected in state.trafficStats) ? state.trafficStats[state.selected] : { Received: 0, Transmitted: 0 }
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setInterfaces(interfaces) {
|
||||
@@ -34,6 +38,14 @@ export const interfaceStore = defineStore('interfaces', {
|
||||
this.selected = ""
|
||||
}
|
||||
this.fetching = false
|
||||
this.trafficStats = {}
|
||||
},
|
||||
updateInterfaceTrafficStats(interfaceStats) {
|
||||
const id = interfaceStats.EntityId;
|
||||
this.trafficStats[id] = {
|
||||
Received: interfaceStats.BytesReceived,
|
||||
Transmitted: interfaceStats.BytesTransmitted,
|
||||
};
|
||||
},
|
||||
async LoadInterfaces() {
|
||||
this.fetching = true
|
||||
|
||||
@@ -23,6 +23,7 @@ export const peerStore = defineStore('peers', {
|
||||
fetching: false,
|
||||
sortKey: 'IsConnected', // Default sort key
|
||||
sortOrder: -1, // 1 for ascending, -1 for descending
|
||||
trafficStats: {},
|
||||
}),
|
||||
getters: {
|
||||
Find: (state) => {
|
||||
@@ -76,6 +77,9 @@ export const peerStore = defineStore('peers', {
|
||||
Statistics: (state) => {
|
||||
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||
},
|
||||
TrafficStats: (state) => {
|
||||
return (id) => (id in state.trafficStats) ? state.trafficStats[id] : { Received: 0, Transmitted: 0 }
|
||||
},
|
||||
hasStatistics: (state) => state.statsEnabled,
|
||||
|
||||
},
|
||||
@@ -111,6 +115,7 @@ export const peerStore = defineStore('peers', {
|
||||
this.peers = peers
|
||||
this.calculatePages()
|
||||
this.fetching = false
|
||||
this.trafficStats = {}
|
||||
},
|
||||
setPeer(peer) {
|
||||
this.peer = peer
|
||||
@@ -126,11 +131,19 @@ export const peerStore = defineStore('peers', {
|
||||
if (!statsResponse) {
|
||||
this.stats = {}
|
||||
this.statsEnabled = false
|
||||
this.trafficStats = {}
|
||||
} else {
|
||||
this.stats = statsResponse.Stats
|
||||
this.statsEnabled = statsResponse.Enabled
|
||||
}
|
||||
},
|
||||
updatePeerTrafficStats(peerStats) {
|
||||
const id = peerStats.EntityId;
|
||||
this.trafficStats[id] = {
|
||||
Received: peerStats.BytesReceived,
|
||||
Transmitted: peerStats.BytesTransmitted,
|
||||
};
|
||||
},
|
||||
async Reset() {
|
||||
this.setPeers([])
|
||||
this.setStats(undefined)
|
||||
|
||||
@@ -210,6 +210,12 @@ onMounted(async () => {
|
||||
<div class="col-12 col-lg-8">
|
||||
{{ $t('interfaces.interface.headline') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{ $t('modals.interface-edit.mode.' + interfaces.GetSelected.Mode )}} | {{ $t('interfaces.interface.backend') + ": " + calculateBackendName }}<span v-if="!isBackendValid" :title="t('interfaces.interface.wrong-backend')" class="ms-1 me-1"><i class="fa-solid fa-triangle-exclamation"></i></span>)
|
||||
<span v-if="interfaces.GetSelected.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="interfaces.GetSelected.DisabledReason"></i></span>
|
||||
<div v-if="interfaces.GetSelected && (interfaces.TrafficStats.Received > 0 || interfaces.TrafficStats.Transmitted > 0)" class="mt-2">
|
||||
<small class="text-muted">
|
||||
Traffic: <i class="fa-solid fa-arrow-down me-1"></i>{{ humanFileSize(interfaces.TrafficStats.Received) }}/s
|
||||
<i class="fa-solid fa-arrow-up ms-1 me-1"></i>{{ humanFileSize(interfaces.TrafficStats.Transmitted) }}/s
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 text-lg-end">
|
||||
<a class="btn-link" href="#" :title="$t('interfaces.interface.button-show-config')" @click.prevent="viewedInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-eye"></i></a>
|
||||
@@ -451,14 +457,19 @@ onMounted(async () => {
|
||||
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
|
||||
<td v-if="peers.hasStatistics">
|
||||
<div v-if="peers.Statistics(peer.Identifier).IsConnected">
|
||||
<span class="badge rounded-pill bg-success" :title="$t('interfaces.peer-connected')"><i class="fa-solid fa-link"></i></span> <span :title="$t('interfaces.peer-handshake') + ' ' + peers.Statistics(peer.Identifier).LastHandshake">{{ $t('interfaces.peer-connected') }}</span>
|
||||
<span class="badge rounded-pill bg-success" :title="$t('interfaces.peer-connected')"><i class="fa-solid fa-link"></i></span> <small class="text-muted" :title="$t('interfaces.peer-handshake') + ' ' + peers.Statistics(peer.Identifier).LastHandshake"><i class="fa-solid fa-circle-info"></i></small>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="badge rounded-pill bg-light" :title="$t('interfaces.peer-not-connected')"><i class="fa-solid fa-link-slash"></i></span>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="peers.hasStatistics" >
|
||||
<span class="text-center" >{{ humanFileSize(peers.Statistics(peer.Identifier).BytesReceived) }} / {{ humanFileSize(peers.Statistics(peer.Identifier).BytesTransmitted) }}</span>
|
||||
<div class="d-flex flex-column">
|
||||
<span :title="humanFileSize(peers.Statistics(peer.Identifier).BytesReceived) + ' / ' + humanFileSize(peers.Statistics(peer.Identifier).BytesTransmitted)">
|
||||
<i class="fa-solid fa-arrow-down me-1"></i>{{ humanFileSize(peers.TrafficStats(peer.Identifier).Received) }}/s
|
||||
<i class="fa-solid fa-arrow-up ms-1 me-1"></i>{{ humanFileSize(peers.TrafficStats(peer.Identifier).Transmitted) }}/s
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="#" :title="$t('interfaces.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||
|
||||
Reference in New Issue
Block a user