Added feature #525

This commit is contained in:
Donald Zou
2025-09-07 17:04:22 +08:00
parent 62d3332522
commit 604d53d2f0
8 changed files with 226 additions and 9 deletions

View File

@@ -891,6 +891,31 @@ def API_getConfigurationInfo():
"configurationRestrictedPeers": WireguardConfigurations[configurationName].getRestrictedPeersList()
})
@app.get(f'{APP_PREFIX}/api/getPeerHistoricalEndpoints')
def API_GetPeerHistoricalEndpoints():
configurationName = request.args.get("configurationName")
id = request.args.get('id')
if not configurationName or not id:
return ResponseObject(False, "Please provide configurationName and id")
fp, p = WireguardConfigurations.get(configurationName).searchPeer(id)
if fp:
result = p.getEndpoints()
geo = {}
try:
r = requests.post(f"http://ip-api.com/batch?fields=city,country,lat,lon,query",
data=json.dumps([x['endpoint'] for x in result]))
d = r.json()
except Exception as e:
return ResponseObject(data=result, message="Failed to request IP address geolocation. " + str(e))
return ResponseObject(data={
"endpoints": p.getEndpoints(),
"geolocation": d
})
return ResponseObject(False, "Peer does not exist")
@app.get(f'{APP_PREFIX}/api/getPeerSessions')
def API_GetPeerSessions():
configurationName = request.args.get("configurationName")

View File

@@ -159,6 +159,15 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
sqlalchemy.Column('Info', sqlalchemy.Text),
extend_existing=True
)
self.peersHistoryEndpointTable = sqlalchemy.Table(
f'{dbName}_history_endpoint', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('time',
(sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP)),
extend_existing=True
)
self.metadata.create_all(self.engine)

View File

@@ -236,6 +236,20 @@ class Peer:
return False
return True
def getEndpoints(self):
result = []
with self.configuration.engine.connect() as conn:
result = conn.execute(
db.select(
self.configuration.peersHistoryEndpointTable.c.endpoint
).group_by(
self.configuration.peersHistoryEndpointTable.c.endpoint
).where(
self.configuration.peersHistoryEndpointTable.c.id == self.id
)
).mappings().fetchall()
return list(result)
def getTraffics(self, interval: int = 30, startDate: datetime.datetime = None, endDate: datetime.datetime = None):
if startDate is None and endDate is None:
endDate = datetime.datetime.now()

View File

@@ -452,11 +452,9 @@ class WireguardConfiguration:
self.peersTable.columns.id == i['PublicKey']
)
)
tmpList.append(Peer(tempPeer, self))
except Exception as e:
print(f"[WGDashboard] {self.Name} getPeers() Error: {str(e)}")
else:
with self.engine.connect() as conn:
existingPeers = conn.execute(self.peersTable.select()).mappings().fetchall()

View File

@@ -1,8 +1,7 @@
<script setup lang="ts">
import LocaleText from "@/components/text/localeText.vue";
import {computed, ref, watch} from "vue";
import dayjs from "dayjs";
import {GetLocale} from "@/utilities/locale.js"
import {ref} from "vue";
import {
Chart,
LineElement,
@@ -32,6 +31,7 @@ Chart.register(
);
import PeerSessions from "@/components/peerDetailsModalComponents/peerSessions.vue";
import PeerTraffics from "@/components/peerDetailsModalComponents/peerTraffics.vue";
import PeerEndpoints from "@/components/peerDetailsModalComponents/peerEndpoints.vue";
const props = defineProps(['selectedPeer'])
const selectedDate = ref(undefined)
defineEmits(['close'])
@@ -160,7 +160,12 @@ defineEmits(['close'])
@selectDate="args => selectedDate = args"
:selectedPeer="selectedPeer"></PeerSessions>
</div>
<div class="col-12">
<PeerEndpoints
:selectedPeer="selectedPeer"
>
</PeerEndpoints>
</div>
</div>

View File

@@ -0,0 +1,164 @@
<script setup lang="ts">
import LocaleText from "@/components/text/localeText.vue";
import { fetchGet } from "@/utilities/fetch.js"
import {onMounted, ref} from "vue";
const props = defineProps(['selectedPeer'])
const loaded = ref(false)
const endpoints = ref(undefined)
const mapAvailable = ref(undefined)
import "ol/ol.css"
import Map from 'ol/Map.js';
import OSM from 'ol/source/OSM.js';
import TileLayer from 'ol/layer/Tile.js';
import View from 'ol/View.js';
import {Feature} from "ol";
import {fromLonLat} from "ol/proj"
import {LineString, Point} from "ol/geom"
import {linear} from 'ol/easing';
import {Circle, Fill, Stroke, Style, Text} from "ol/style.js";
import {Vector} from "ol/layer"
import {Vector as SourceVector} from "ol/source"
const map = ref(undefined)
const loadEndpoints = async () => {
await fetchGet("/api/getPeerHistoricalEndpoints", {
id: props.selectedPeer.id,
configurationName: props.selectedPeer.configuration.Name
}, async (res) => {
if (res.status){
endpoints.value = res.data
}
loaded.value = true
if (endpoints.value.geolocation){
try{
await fetch("https://tile.openstreetmap.org/",
{ signal: AbortSignal.timeout(1500) })
mapAvailable.value = true;
map.value = new Map({
target: 'map',
layers: [
new TileLayer({
source: new OSM(),
}),
],
view: new View({
center: fromLonLat([17.64, 16.35]),
zoom: 0,
}),
});
if (endpoints.value.geolocation){
const vectorSource = new SourceVector();
let geo = endpoints.value.geolocation.filter(x => x.lat && x.lon)
geo.forEach(data => {
vectorSource.addFeature(new Feature({
geometry: new Point(fromLonLat([data.lon, data.lat])),
}))
})
vectorSource.addFeature(new Feature({
}))
map.value.addLayer(new Vector({
source: vectorSource,
style: () => {
return new Style({
image: new Circle({
radius: 10,
fill: new Fill({ color: '#0d6efd' }),
stroke: new Stroke({ color: 'white', width: 5 }),
})
});
}
}));
}
}catch (e) {
console.log(e)
mapAvailable.value = false
}
}
})
}
onMounted(() => loadEndpoints())
const getGeolocation = (endpoint) => {
if (endpoints.value.geolocation){
let geo = endpoints.value.geolocation.find(x => x.query === endpoint)
if (geo){
let c = [geo.city, geo.country]
if (c.filter(x => x !== undefined).length === 0) c.push('Private Address')
return c.filter(x => x !== undefined).join(", ")
}
}
return undefined
}
const setMapCenter = (endpoint) => {
if (endpoints.value.geolocation){
let geo = endpoints.value.geolocation.find(x => x.query === endpoint)
if (geo && geo.lon && geo.lat){
map.value.getView().animate({zoom: 4}, {center: fromLonLat([
geo.lon, geo.lat
])}, {easing: linear})
}
}
}
</script>
<template>
<div class="card rounded-3 bg-transparent">
<div class="card-header text-muted">
<LocaleText t="Peer Historical Endpoints"></LocaleText>
</div>
<div class="card-body">
<div class="bg-body-tertiary p-3 d-flex rounded-3">
<div class="m-auto" v-if="!loaded">
<span class="spinner-border spinner-border-sm me-2"></span><LocaleText t="Loading..."></LocaleText>
</div>
<div v-else class="w-100 d-flex flex-column gap-3">
<div class="bg-body d-flex w-100 rounded-3" style="height: 500px" id="map">
<div class="m-auto" v-if="!mapAvailable">
<div v-if="mapAvailable === undefined">
<span class="spinner-border spinner-border-sm me-2"></span>
<LocaleText t="Loading Map..."></LocaleText>
</div>
<div v-if="mapAvailable === false" class="text-muted">
<LocaleText t="Map is not available"></LocaleText>
</div>
</div>
</div>
<table class="table table-hover">
<thead>
<tr>
<th>Endpoint</th>
<th v-if="endpoints.geolocation">Geolocation</th>
</tr>
</thead>
<tbody>
<tr v-for="endpoint in endpoints.endpoints"
@click="setMapCenter(endpoint.endpoint)"
style="cursor: pointer">
<td>
{{ endpoint.endpoint }}
</td>
<td v-if="endpoints.geolocation">
{{ getGeolocation(endpoint.endpoint) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -78,8 +78,11 @@ const emits = defineEmits(['selectDate'])
<button class="btn btn-sm rounded-3" v-if="offsetMonth !== 0" @click="offsetMonth = 0; $emit('selectDate', day)">
<LocaleText t="Today"></LocaleText>
</button>
<h5 class="mx-auto mb-0">
{{ today.format('YYYY-MM')}}
<h5 class="mx-auto mb-0 text-center">
<small class="text-muted" style="font-size: 0.9rem">
<LocaleText t="Peer Historical Sessions"></LocaleText>
</small><br>
{{ today.format('YYYY / MM')}}
</h5>
<button class="btn btn-sm rounded-3" v-if="offsetMonth !== 0" @click="offsetMonth = 0; $emit('selectDate', day)">
<LocaleText t="Today"></LocaleText>

View File

@@ -20,7 +20,6 @@ const getTraffic = async () => {
endDate: date.value.format("YYYY-MM-DD")
}, (res) => {
traffics.value = res.data
console.log(traffics.value)
})
}
const interval = ref(undefined)