mirror of
https://github.com/eduardogsilva/wireguard_webadmin.git
synced 2025-04-19 00:45:16 +00:00
846 lines
44 KiB
HTML
846 lines
44 KiB
HTML
{% extends "base.html" %}
|
|
{% load i18n %}
|
|
{% block page_custom_head %}
|
|
<style>
|
|
.peer-extra-info {
|
|
display: none;
|
|
}
|
|
.callout.position-relative {
|
|
padding: 0 !important;
|
|
}
|
|
@keyframes blink {
|
|
50% { opacity: 0; }
|
|
}
|
|
.blinking-icon {
|
|
animation: blink 1s step-start infinite;
|
|
}
|
|
#inviteTextContainer {
|
|
border: 1px solid #ccc;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
background-color: #f9f9f9;
|
|
}
|
|
|
|
#inviteText {
|
|
white-space: pre-line;
|
|
text-align: left;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
|
|
{% block content %}
|
|
{% if wireguard_instances %}
|
|
<div class="card card-primary card-outline">
|
|
<div class="card-body">
|
|
<ul class="nav nav-tabs" role="tablist">
|
|
{% for wgconf in wireguard_instances %}
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if wgconf == current_instance %}active{% endif %}" href="/peer/list/?uuid={{ wgconf.uuid }}" role="tab">
|
|
wg{{ wgconf.instance_id }} {% if wgconf.name %}({{ wgconf.name }}){% endif %}
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<div class="tab-content" id="custom-content-below-tabContent">
|
|
<div class="tab-pane fade show active" id="custom-content-below-home" role="tabpanel" aria-labelledby="custom-content-below-home-tab">
|
|
<div class="row" style="padding-top: 15px">
|
|
{% for peer in peer_list %}
|
|
<div class="col-xl-6" id="peer-{{ peer.public_key }}" data-uuid="{{ peer.uuid }}">
|
|
<div class="callout position-relative">
|
|
{% comment %}background: linear-gradient(to right, white 50%, transparent 50%);{% endcomment %}
|
|
<div class="position-absolute p-3 div-peer-text-information" style="top: 0; left: 0; background: linear-gradient(to right, white, transparent); width: 100%; height: 100%;">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<h5 id="peer-name-{{ peer.public_key }}">
|
|
<a href="#" onclick="openPeerModal('{{ peer.uuid }}');" style="text-decoration: none">
|
|
{{ peer }}
|
|
</a>
|
|
</h5>
|
|
<span>
|
|
{% if user_acl.user_level >= 30 %}
|
|
<div class="d-inline-flex flex-column">
|
|
<a href="/peer/sort/?peer={{ peer.uuid }}&direction=up" style="line-height:0px">
|
|
<i class="fas fa-sort-up"></i>
|
|
</a>
|
|
<div style="overflow:hidden;margin-top: -9px">
|
|
<a href="/peer/sort/?peer={{ peer.uuid }}&direction=down" style="position:relative;top:-11px">
|
|
<i class="fas fa-sort-down"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
<b class="peer-extra-info">{% trans 'Throughput' %}: </b> <span id="peer-throughput-{{ peer.public_key }}"></span><br>
|
|
<span class="peer-extra-info"><b>{% trans 'Transfer' %}:</b> <span id="peer-transfer-{{ peer.public_key }}"></span><br></span>
|
|
<span class="peer-extra-info"><b>{% trans 'Latest Handshake' %}:</b> <span id="peer-latest-handshake-{{ peer.public_key }}"></span></span>
|
|
<span class="peer-extra-info"><span style="display: none;" id="peer-stored-latest-handshake-{{ peer.public_key }}">{% if peer.peerstatus.last_handshake %}{{ peer.peerstatus.last_handshake|date:"U" }}{% else %}0{% endif %}</span><br></span>
|
|
<span class="peer-extra-info"><b>{% trans 'Endpoints' %}:</b> <span id="peer-endpoints-{{ peer.public_key }}"></span><br></span>
|
|
<span class="peer-extra-info" id="peer-extra-info-allowed-ips-{{ peer.public_key }}">
|
|
<b>{% trans 'Allowed IPs' %}:</b>
|
|
<span id="peer-allowed-ips-{{ peer.public_key }}">
|
|
{% for address in peer.peerallowedip_set.all %}
|
|
{% if address.priority == 0 and address.config_file == 'server' %}
|
|
{{ address }}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% for address in peer.peerallowedip_set.all %}
|
|
{% if address.priority >= 1 and address.config_file == 'server' %}
|
|
{{ address }}
|
|
{% endif %}
|
|
{% endfor %}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<canvas class="" id="chart-{{ peer.public_key }}" width="800" height="130" style="min-height: 85px"></canvas>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% if add_peer_enabled %}
|
|
<a class="btn btn-primary" href="/peer/manage/?instance={{ current_instance.uuid }}" onclick="return confirm('{% trans 'Are you sure you want to create a new peer?' %}');">{% trans 'Create Peer' %}</a>
|
|
{% else %}
|
|
<a class="btn btn-primary disabled" href="">{% trans 'Create Peer' %}</a>
|
|
{% endif %}
|
|
<button id="toggleExtraInfo" class="btn btn-outline-primary">{% trans 'Show extras' %}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Peer Preview Modal -->
|
|
<div class="modal fade" id="peerPreviewModal" tabindex="-1" aria-labelledby="peerPreviewModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="peerPreviewModalLabel">Peer Preview</h5>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Info content section -->
|
|
<div class="info-content">
|
|
<p><b><i class="fas fa-arrows-alt-v nav-icon"></i> {% trans 'Throughput' %}:</b> <span id="peerThroughput">--</span></p>
|
|
<p><b><i class="fas fa-dolly nav-icon"></i> {% trans 'Transfer' %}:</b> <span id="peerTransfer">--</span></p>
|
|
<p><b><i class="far fa-clock nav-icon"></i> {% trans 'Latest Handshake' %}:</b> <span id="peerHandshake">--</span></p>
|
|
<p><b><i class="far fa-address-card nav-icon"></i> {% trans 'Endpoints' %}:</b> <span id="peerEndpoints">--</span></p>
|
|
<p><b><i class="fas fa-network-wired nav-icon"></i> {% trans 'Allowed IPs' %}:</b> <span id="peerAllowedIPs">--</span></p>
|
|
|
|
<!-- Traffic Graph -->
|
|
<div class="graph-container" style="margin-top:20px;">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<label>
|
|
<i class="fas fa-chart-area nav-icon"></i>
|
|
{% trans 'Peer Traffic' %}
|
|
</label>
|
|
<div class="btn-group" role="group" aria-label="Graph interval">
|
|
<a href="#" data-period="1h" class="btn btn-outline-primary btn-xs">1h</a>
|
|
<a href="#" data-period="3h" class="btn btn-outline-primary btn-xs">3h</a>
|
|
<a href="#" data-period="6h" class="btn btn-outline-primary btn-xs">6h</a>
|
|
<a href="#" data-period="1d" class="btn btn-outline-primary btn-xs">1d</a>
|
|
<a href="#" data-period="7d" class="btn btn-outline-primary btn-xs">7d</a>
|
|
<a href="#" data-period="30d" class="btn btn-outline-primary btn-xs">1m</a>
|
|
<a href="#" data-period="90d" class="btn btn-outline-primary btn-xs">3m</a>
|
|
<a href="#" data-period="180d" class="btn btn-outline-primary btn-xs">6m</a>
|
|
<a href="#" data-period="365d" class="btn btn-outline-primary btn-xs">1y</a>
|
|
</div>
|
|
</div>
|
|
<center style="margin-top:10px;">
|
|
<img id="graphImg" src="" class="img-fluid" alt="{% trans 'No traffic history, please wait a few minutes' %}" style="display:block;">
|
|
</center>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- QR Code content section (initially hidden) -->
|
|
<div class="qr-code-content" style="display:none; ">
|
|
<button class="btn btn-secondary" id="backButton"><i class="fas fa-times"></i> {% trans 'Close QR Code' %}</button><br>
|
|
<div style="text-align: center;">
|
|
<img id="qrCodeImg" src="" alt="QR Code" class="img-fluid" style="max-width: 400px" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- VPN Invite content section (initially hidden) -->
|
|
<div class="invite-content" style="display:none;">
|
|
<button class="btn btn-secondary" id="backFromInviteButton"><i class="fas fa-arrow-left"></i> {% trans 'Back' %}</button><br>
|
|
<div style="text-align: center; margin-top: 10px;">
|
|
<h5>{% trans 'VPN Invite Details' %}</h5>
|
|
<div id="inviteTextContainer">
|
|
<p id="inviteText"></p>
|
|
</div>
|
|
<p id="invitePassword"></p>
|
|
<p>
|
|
{% trans 'Expires on' %}: <span id="inviteExpiration"></span>
|
|
<i class="fas fa-sync-alt" id="refreshInviteButton" style="cursor: pointer;" title="Refresh Invite"></i>
|
|
</p>
|
|
<div class="form-group">
|
|
<label for="inviteContactInput">{% trans 'Enter Email or WhatsApp Number' %}:</label>
|
|
<input type="text" class="form-control" id="inviteContactInput" placeholder="{% trans 'Email or phone number' %}">
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-outline-secondary" id="copyInviteTextButton"><i class="fas fa-copy"></i> {% trans 'Copy Text' %}</button>
|
|
<button class="btn btn-success" id="sendInviteEmailButton"><i class="fas fa-envelope"></i> {% trans 'Email' %}</button>
|
|
<button class="btn btn-success" id="sendInviteWhatsappButton"><i class="fab fa-whatsapp"></i> {% trans 'WhatsApp' %}</button>
|
|
<button class="btn btn-secondary" id="closeInviteButton"><i class="far fa-trash-alt"></i> {% trans 'Delete' %}</button>
|
|
</div>
|
|
<div id="inviteMessage" style="margin-top: 10px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fas fa-times"></i> {% trans 'Close' %}</button>
|
|
<a href="#" class="btn btn-info" id="downloadConfigButton"><i class="fas fa-download"></i> {% trans 'Config' %}</a>
|
|
<a href="#" class="btn btn-info" id="qrcodeButton"><i class="fas fa-qrcode"></i> {% trans 'QR Code' %}</a>
|
|
<a href="#" class="btn btn-info" id="inviteButton"><i class="fas fa-share"></i> {% trans 'VPN Invite' %}</a>
|
|
<a href="#" class="btn btn-outline-primary" id="editPeerButton"><i class="far fa-edit"></i> {% trans 'Edit' %}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% else %}
|
|
<div class="alert alert-warning" role="alert">
|
|
<h4 class="alert-heading">{% trans 'No WireGuard Instances Found' %}</h4>
|
|
<p>{% trans 'There are no WireGuard instances configured. You can add a new instance by clicking the button below.' %}</p>
|
|
</div>
|
|
<p>
|
|
<a href="/server/manage/" class="btn btn-primary">{% trans 'Add WireGuard Instance' %}</a>
|
|
</p>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
|
|
{% block custom_page_scripts %}
|
|
|
|
<script>
|
|
// Global object to store Chart.js instances for each peer.
|
|
var charts = {};
|
|
|
|
// Initialize charts for each peer once the DOM is ready.
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// For each canvas element matching id pattern "chart-<peer_public_key>"
|
|
document.querySelectorAll('canvas[id^="chart-"]').forEach(function(canvas) {
|
|
var peerId = canvas.id.replace('chart-', '');
|
|
// Create a new Chart instance
|
|
charts[peerId] = new Chart(canvas.getContext('2d'), {
|
|
type: 'line',
|
|
data: {
|
|
// X-axis labels can be blank since we are only showing the last 10 points.
|
|
labels: Array(10).fill(''),
|
|
datasets: [
|
|
{
|
|
label: 'Download',
|
|
data: Array(10).fill(0),
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
fill: false,
|
|
tension: 0.1,
|
|
lineTension: 0.4,
|
|
pointRadius: 0
|
|
},
|
|
{
|
|
label: 'Upload',
|
|
data: Array(10).fill(0),
|
|
borderColor: 'rgba(255, 99, 132, 1)',
|
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
fill: false,
|
|
tension: 0.1,
|
|
lineTension: 0.4,
|
|
pointRadius: 0
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
responsive: true,
|
|
scales: {
|
|
xAxes: [{
|
|
display: false,
|
|
ticks: { display: false },
|
|
gridLines: { display: false }
|
|
}],
|
|
yAxes: [{
|
|
display: false,
|
|
ticks: { display: false, beginAtZero: true },
|
|
gridLines: { display: false }
|
|
}]
|
|
},
|
|
animation: {
|
|
duration: 0
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
$("#qrcodeButton").on("click", function(e) {
|
|
e.preventDefault();
|
|
var uuid = $("#peerPreviewModal").data("peer-uuid");
|
|
$("#qrCodeImg").attr("src", "/tools/download_peer_config/?uuid=" + uuid + "&format=qrcode");
|
|
$(".info-content").hide();
|
|
$(".invite-content").hide();
|
|
$(".qr-code-content").show();
|
|
});
|
|
|
|
$("#backButton").on("click", function(e) {
|
|
e.preventDefault();
|
|
$(".qr-code-content").hide();
|
|
$(".info-content").show();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
function openPeerModal(uuid) {
|
|
$(".qr-code-content").hide();
|
|
$(".invite-content").hide();
|
|
$(".info-content").show();
|
|
$("#qrCodeImg").attr("src", "");
|
|
$('#graphImg').attr('src', '').hide();
|
|
// Find the peer element by its data-uuid attribute
|
|
var peerElem = document.querySelector('[data-uuid="' + uuid + '"]');
|
|
if (peerElem) {
|
|
var peerNameFromCard = peerElem.querySelector('h5').innerText;
|
|
var peerThroughput = peerElem.querySelector('[id^="peer-throughput-"]').innerHTML;
|
|
var peerTransfer = peerElem.querySelector('[id^="peer-transfer-"]').innerText;
|
|
var peerHandshake = peerElem.querySelector('[id^="peer-latest-handshake-"]').innerText;
|
|
var peerEndpoints = peerElem.querySelector('[id^="peer-endpoints-"]').innerText;
|
|
var peerAllowedIPs = peerElem.querySelector('[id^="peer-allowed-ips-"]').innerHTML;
|
|
|
|
// Update the modal fields with the card values
|
|
$('#peerPreviewModalLabel').text(peerNameFromCard);
|
|
$('#peerThroughput').html(peerThroughput);
|
|
$('#peerTransfer').text(peerTransfer);
|
|
$('#peerHandshake').text(peerHandshake);
|
|
$('#peerEndpoints').text(peerEndpoints);
|
|
$('#peerAllowedIPs').html(peerAllowedIPs);
|
|
$('#editPeerButton').attr('href', '/peer/manage/?peer=' + uuid);
|
|
$('#downloadConfigButton').attr('href', '/tools/download_peer_config/?uuid=' + uuid);
|
|
$('#qrcodeButton').attr('href', '/tools/download_peer_config/?uuid=' + uuid + '&format=qrcode');
|
|
$('#graphImg').attr('src', '/rrd/graph/?peer=' + uuid).show();
|
|
$('#peerPreviewModal').data('peer-uuid', uuid);
|
|
|
|
$.ajax({
|
|
url: '/api/peer_info/',
|
|
data: { uuid: uuid },
|
|
type: 'GET',
|
|
dataType: 'json',
|
|
success: function(data) {
|
|
if (data.name) {
|
|
$('#peerPreviewModalLabel').text(data.name);
|
|
}
|
|
// Future additional peer information can be handled here.
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error("Error fetching peer info:", error);
|
|
}
|
|
});
|
|
$('#peerPreviewModal').modal('show');
|
|
} else {
|
|
console.error('Peer element not found for uuid: ' + uuid);
|
|
}
|
|
}
|
|
|
|
$(document).on('click', '.graph-container .btn-group a', function(e) {
|
|
e.preventDefault();
|
|
var period = $(this).data('period');
|
|
var uuid = $('#peerPreviewModal').data('peer-uuid');
|
|
var newSrc = '/rrd/graph/?peer=' + uuid + '&period=' + period;
|
|
$('#graphImg').attr('src', newSrc);
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
var previousMeasurements = {};
|
|
var toastShownThisCycle = false;
|
|
|
|
const updateThroughput = (peerId, peerInfo) => {
|
|
const throughputElement = document.getElementById(`peer-throughput-${peerId}`);
|
|
const currentTime = Date.now() / 1000; // current timestamp in seconds
|
|
let formattedThroughput = '';
|
|
|
|
if (previousMeasurements[peerId]) {
|
|
const prev = previousMeasurements[peerId];
|
|
const timeDiff = currentTime - prev.timestamp; // time difference in seconds
|
|
|
|
// For the peer: download corresponds to tx and upload to rx
|
|
let downloadDiff = peerInfo.transfer.tx - prev.transfer.tx;
|
|
let uploadDiff = peerInfo.transfer.rx - prev.transfer.rx;
|
|
|
|
// If counters have been reset (current value < previous), show a toast (only once per cycle)
|
|
if (downloadDiff < 0 || uploadDiff < 0) {
|
|
if (!toastShownThisCycle) {
|
|
$(document).Toasts('create', {
|
|
class: 'bg-info',
|
|
title: 'info',
|
|
body: 'Throughput discarded due to counter reset',
|
|
delay: 10000,
|
|
autohide: true
|
|
});
|
|
toastShownThisCycle = true;
|
|
}
|
|
downloadDiff = 0;
|
|
uploadDiff = 0;
|
|
}
|
|
|
|
// Calculate throughput in bytes per second
|
|
const downloadThroughput = downloadDiff / timeDiff;
|
|
const uploadThroughput = uploadDiff / timeDiff;
|
|
|
|
// Convert bytes per second to bits per second
|
|
const downloadBps = downloadThroughput * 8;
|
|
const uploadBps = uploadThroughput * 8;
|
|
|
|
// Calculate Mbps and Kbps values
|
|
const downloadMbps = downloadBps / 1000000;
|
|
const uploadMbps = uploadBps / 1000000;
|
|
const downloadKbps = downloadBps / 1000;
|
|
const uploadKbps = uploadBps / 1000;
|
|
|
|
// Determine display unit and formatting
|
|
let downloadDisplay, uploadDisplay;
|
|
if (downloadMbps < 1) {
|
|
// Below 1 Mbps, display in Kbps
|
|
if (downloadKbps < 100) {
|
|
downloadDisplay = downloadKbps.toFixed(2) + ' Kbps';
|
|
} else {
|
|
downloadDisplay = downloadKbps.toFixed(0) + ' Kbps';
|
|
}
|
|
} else {
|
|
// 1 Mbps and above: if above 10 Mbps, show no decimals; else, show 2 decimals
|
|
downloadDisplay = (downloadMbps > 10 ? downloadMbps.toFixed(0) : downloadMbps.toFixed(2)) + ' Mbps';
|
|
}
|
|
if (uploadMbps < 1) {
|
|
if (uploadKbps < 10) {
|
|
uploadDisplay = uploadKbps.toFixed(2) + ' Kbps';
|
|
} else {
|
|
uploadDisplay = uploadKbps.toFixed(0) + ' Kbps';
|
|
}
|
|
} else {
|
|
uploadDisplay = (uploadMbps > 10 ? uploadMbps.toFixed(0) : uploadMbps.toFixed(2)) + ' Mbps';
|
|
}
|
|
|
|
// Highlight values above a threshold
|
|
const threshold = 100.0;
|
|
if (downloadMbps > threshold) {
|
|
downloadDisplay = `<strong>${downloadDisplay}</strong>`;
|
|
}
|
|
if (uploadMbps > threshold) {
|
|
uploadDisplay = `<strong>${uploadDisplay}</strong>`;
|
|
}
|
|
|
|
formattedThroughput = `<i class="fas fa-arrow-down"></i> ${downloadDisplay}, <i class="fas fa-arrow-up"></i> ${uploadDisplay}`;
|
|
throughputElement.innerHTML = formattedThroughput;
|
|
|
|
// Update Chart.js graphs with raw Mbps values (always using Mbps for consistency)
|
|
if (charts[peerId]) {
|
|
var chart = charts[peerId];
|
|
chart.data.datasets[0].data.push(downloadMbps);
|
|
if (chart.data.datasets[0].data.length > 10) {
|
|
chart.data.datasets[0].data.shift();
|
|
}
|
|
chart.data.datasets[1].data.push(uploadMbps);
|
|
if (chart.data.datasets[1].data.length > 10) {
|
|
chart.data.datasets[1].data.shift();
|
|
}
|
|
chart.update();
|
|
}
|
|
} else {
|
|
// First cycle: no previous measurement available.
|
|
formattedThroughput = `<i class="fas fa-arrow-down"></i> -.-- Kbps, <i class="fas fa-arrow-up"></i> -.-- Kbps`;
|
|
throughputElement.innerHTML = formattedThroughput;
|
|
}
|
|
|
|
previousMeasurements[peerId] = {
|
|
timestamp: currentTime,
|
|
transfer: {
|
|
tx: peerInfo.transfer.tx,
|
|
rx: peerInfo.transfer.rx
|
|
}
|
|
};
|
|
|
|
return formattedThroughput;
|
|
};
|
|
|
|
// Convert bytes to human-readable format with abbreviated units
|
|
const convertBytes = (bytes) => {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
};
|
|
|
|
// Fetch Wireguard status and update UI
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const fetchWireguardStatus = async () => {
|
|
try {
|
|
const response = await fetch('/api/wireguard_status/');
|
|
let data = await response.json();
|
|
|
|
// If latest-handshakes is 0, use the stored value
|
|
for (const [interfaceName, peers] of Object.entries(data)) {
|
|
for (const [peerId, peerInfo] of Object.entries(peers)) {
|
|
const peerElementId = `peer-stored-latest-handshake-${peerId}`;
|
|
const storedHandshakeElement = document.getElementById(peerElementId);
|
|
if (peerInfo['latest-handshakes'] === '0' && storedHandshakeElement) {
|
|
peerInfo['latest-handshakes'] = storedHandshakeElement.textContent;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateUI(data);
|
|
} catch (error) {
|
|
console.error('Error fetching Wireguard status:', error);
|
|
}
|
|
};
|
|
|
|
fetchWireguardStatus();
|
|
setInterval(fetchWireguardStatus, {{ current_instance.peer_list_refresh_interval }} * 1000);
|
|
});
|
|
|
|
const updateUI = (data) => {
|
|
// Reset the toast flag for this update cycle
|
|
toastShownThisCycle = false;
|
|
|
|
for (const [interfaceName, peers] of Object.entries(data)) {
|
|
for (const [peerId, peerInfo] of Object.entries(peers)) {
|
|
const peerDiv = document.getElementById(`peer-${peerId}`);
|
|
if (peerDiv) {
|
|
updatePeerInfo(peerDiv, peerId, peerInfo);
|
|
updateCalloutClass(peerDiv, peerInfo['latest-handshakes']);
|
|
// Calculate throughput and update the card
|
|
const throughputHTML = updateThroughput(peerId, peerInfo);
|
|
|
|
// If the modal is active for this peer, update its fields as well.
|
|
const peerUuid = peerDiv.getAttribute("data-uuid");
|
|
if ($('#peerPreviewModal').is(':visible') && $('#peerPreviewModal').data('peer-uuid') === peerUuid) {
|
|
$('#peerThroughput').html(throughputHTML);
|
|
$('#peerTransfer').text(`${convertBytes(peerInfo.transfer.tx)} TX, ${convertBytes(peerInfo.transfer.rx)} RX`);
|
|
$('#peerHandshake').text(
|
|
peerInfo['latest-handshakes'] !== '0'
|
|
? new Date(parseInt(peerInfo['latest-handshakes']) * 1000).toLocaleString()
|
|
: '0'
|
|
);
|
|
$('#peerEndpoints').text(peerInfo.endpoints);
|
|
const allowedIpsModalElement = document.getElementById('peerAllowedIPs');
|
|
checkAllowedIps(allowedIpsModalElement, peerInfo['allowed-ips']);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const updatePeerInfo = (peerDiv, peerId, peerInfo) => {
|
|
const escapedPeerId = peerId.replace(/([!"#$%&'()*+,.\/:;<=>?@[\]^`{|}~])/g, '\\$1');
|
|
const transfer = peerDiv.querySelector(`#peer-transfer-${escapedPeerId}`);
|
|
const latestHandshake = peerDiv.querySelector(`#peer-latest-handshake-${escapedPeerId}`);
|
|
const endpoints = peerDiv.querySelector(`#peer-endpoints-${escapedPeerId}`);
|
|
const allowedIps = peerDiv.querySelector(`#peer-allowed-ips-${escapedPeerId}`);
|
|
|
|
transfer.textContent = `${convertBytes(peerInfo.transfer.tx)} TX, ${convertBytes(peerInfo.transfer.rx)} RX`;
|
|
latestHandshake.textContent = `${peerInfo['latest-handshakes'] !== '0' ? new Date(parseInt(peerInfo['latest-handshakes']) * 1000).toLocaleString() : '0'}`;
|
|
endpoints.textContent = `${peerInfo.endpoints}`;
|
|
checkAllowedIps(allowedIps, peerInfo['allowed-ips']);
|
|
};
|
|
|
|
const checkAllowedIps = (allowedIpsElement, allowedIpsApiResponse) => {
|
|
const apiIps = allowedIpsApiResponse[0].split(' ');
|
|
const htmlIpsText = allowedIpsElement.textContent.trim();
|
|
const htmlIpsArray = htmlIpsText.match(/\b(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}\b/g) || [];
|
|
|
|
allowedIpsElement.innerHTML = '';
|
|
let allowedIpsIssue = false;
|
|
|
|
htmlIpsArray.forEach((ip, index, array) => {
|
|
const ipSpan = document.createElement('span');
|
|
ipSpan.textContent = ip;
|
|
allowedIpsElement.appendChild(ipSpan);
|
|
|
|
if (!apiIps.includes(ip)) {
|
|
ipSpan.style.color = 'red';
|
|
ipSpan.style.textDecoration = 'underline';
|
|
ipSpan.title = '{% trans 'This address does not appear in the wg show command output, likely indicating that another peer has an IP overlapping this network or that the configuration file is outdated.' %}';
|
|
allowedIpsIssue = true;
|
|
}
|
|
|
|
if (index < array.length - 1) {
|
|
allowedIpsElement.appendChild(document.createTextNode(', '));
|
|
}
|
|
});
|
|
|
|
if (allowedIpsIssue) {
|
|
const peerId = allowedIpsElement.id.replace('peer-allowed-ips-', '');
|
|
const h5Element = document.getElementById('peer-name-' + peerId);
|
|
if (h5Element && !h5Element.querySelector('.fa-exclamation-triangle')) {
|
|
const icon = document.createElement('i');
|
|
icon.className = 'fas fa-exclamation-triangle text-danger blinking-icon';
|
|
icon.title = '{% trans 'At least one address does not appear in the wg show command output, which may indicate that another peer is using an overlapping IP or that the configuration file is outdated.' %}';
|
|
h5Element.appendChild(icon);
|
|
}
|
|
}
|
|
};
|
|
|
|
const updateCalloutClass = (peerDiv, latestHandshake) => {
|
|
const calloutDiv = peerDiv.querySelector('.callout');
|
|
calloutDiv.classList.remove('callout-success', 'callout-info', 'callout-warning', 'callout-danger');
|
|
const handshakeAge = Date.now() / 1000 - parseInt(latestHandshake);
|
|
|
|
if (latestHandshake === '0') {
|
|
calloutDiv.classList.add('callout-danger');
|
|
} else if (handshakeAge < 600) {
|
|
calloutDiv.classList.add('callout-success');
|
|
} else if (handshakeAge < 1800) {
|
|
calloutDiv.classList.add('callout-info');
|
|
} else if (handshakeAge < 21600) {
|
|
calloutDiv.classList.add('callout-warning');
|
|
}
|
|
calloutDiv.style.transition = 'all 5s';
|
|
};
|
|
</script>
|
|
|
|
<script>
|
|
$(document).ready(function(){
|
|
$("#toggleExtraInfo").click(function(){
|
|
$(".peer-extra-info").toggle();
|
|
if($(".peer-extra-info").is(":visible")){
|
|
$(this).text("{% trans 'Hide extras' %}");
|
|
$(".div-peer-text-information").removeClass('position-absolute');
|
|
} else {
|
|
$(this).text("{% trans 'Show extras' %}");
|
|
$(".div-peer-text-information").addClass('position-absolute');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- VPN Invite functionality with adjustments -->
|
|
<script>
|
|
$(document).ready(function(){
|
|
var inviteData = null; // Store invite details
|
|
|
|
// Function to detect mobile device
|
|
function isMobileDevice() {
|
|
return /Mobi|Android/i.test(navigator.userAgent);
|
|
}
|
|
|
|
// Handler for VPN Invite button click
|
|
$("#inviteButton").on("click", function(e) {
|
|
e.preventDefault();
|
|
var peerUuid = $('#peerPreviewModal').data('peer-uuid');
|
|
// Hide other content sections
|
|
$(".info-content").hide();
|
|
$(".qr-code-content").hide();
|
|
$(".invite-content").show();
|
|
|
|
// Clear previous invite message and input field
|
|
$("#inviteMessage").html("");
|
|
$("#inviteContactInput").val("");
|
|
|
|
// Create the invite by calling the API endpoint
|
|
$.ajax({
|
|
url: '/api/peer_invite/',
|
|
data: { peer: peerUuid },
|
|
type: 'GET',
|
|
dataType: 'json',
|
|
success: function(response) {
|
|
if(response.status === "success") {
|
|
inviteData = response.invite_data;
|
|
// Populate invite details in the modal
|
|
$("#inviteText").text(inviteData.text_body);
|
|
$("#invitePassword").html("{% trans 'Access Password' %}: <strong>" + inviteData.password + "</strong> {% trans '(Share this password via a separate secure channel)' %}");
|
|
$("#inviteExpiration").text(new Date(inviteData.expiration).toLocaleString());
|
|
} else {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>" + (response.message || "{% trans 'Unknown error' %}") + "</div>");
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
var message = "{% trans 'Error creating invite.' %}";
|
|
try {
|
|
var resp = xhr.responseJSON;
|
|
message = resp && resp.message ? resp.message : xhr.statusText;
|
|
} catch(err) {
|
|
message = xhr.statusText;
|
|
}
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>" + message + "</div>");
|
|
}
|
|
});
|
|
});
|
|
|
|
// Back button in the invite section
|
|
$("#backFromInviteButton").on("click", function(e) {
|
|
e.preventDefault();
|
|
$(".invite-content").hide();
|
|
$(".info-content").show();
|
|
});
|
|
|
|
// Validate email function
|
|
function isValidEmail(email) {
|
|
var re = /^\S+@\S+\.\S+$/;
|
|
return re.test(email);
|
|
}
|
|
|
|
// Validate phone number function (simple check)
|
|
function isValidPhone(phone) {
|
|
var re = /^\+?\d{10,15}$/;
|
|
return re.test(phone);
|
|
}
|
|
|
|
// Handler for copying the invite text to clipboard
|
|
$("#copyInviteTextButton").on("click", function(e) {
|
|
e.preventDefault();
|
|
var textToCopy = $("#inviteText").text();
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(textToCopy).then(function() {
|
|
$("#inviteMessage").html("<div class='alert alert-success'>{% trans 'Invite text copied to clipboard.' %}</div>");
|
|
}, function(err) {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>{% trans 'Failed to copy text.' %}</div>");
|
|
});
|
|
} else {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>{% trans 'Clipboard API not supported.' %}</div>");
|
|
}
|
|
});
|
|
|
|
// Handler for sending invite via WhatsApp with device detection
|
|
$("#sendInviteWhatsappButton").on("click", function(e) {
|
|
e.preventDefault();
|
|
var contact = $("#inviteContactInput").val().trim();
|
|
if(!isValidPhone(contact)) {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>{% trans 'Please enter a valid phone number for WhatsApp.' %}</div>");
|
|
return;
|
|
}
|
|
if(inviteData && inviteData.whatsapp_body) {
|
|
var whatsappUrl;
|
|
if(isMobileDevice()){
|
|
whatsappUrl = "https://api.whatsapp.com/send?phone=" + encodeURIComponent(contact) + "&text=" + encodeURIComponent(inviteData.whatsapp_body);
|
|
} else {
|
|
whatsappUrl = "https://web.whatsapp.com/send?phone=" + encodeURIComponent(contact) + "&text=" + encodeURIComponent(inviteData.whatsapp_body);
|
|
}
|
|
window.open(whatsappUrl, '_blank');
|
|
} else {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>{% trans 'Invite data is not available.' %}</div>");
|
|
}
|
|
});
|
|
|
|
// Handler for sending invite via Email
|
|
$("#sendInviteEmailButton").on("click", function(e, textStatus, xhr) {
|
|
e.preventDefault();
|
|
var contact = $("#inviteContactInput").val().trim();
|
|
if(!isValidEmail(contact)) {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>{% trans 'Please enter a valid email address.' %}</div>");
|
|
return;
|
|
}
|
|
if(inviteData && inviteData.uuid) {
|
|
// Send invite email via API call
|
|
$.ajax({
|
|
url: '/api/peer_invite/',
|
|
data: { invite: inviteData.uuid, action: 'email', address: contact },
|
|
type: 'GET',
|
|
dataType: 'json',
|
|
success: function(response, textStatus, xhr) {
|
|
var message = response.message;
|
|
if (!message) {
|
|
message = xhr.statusText;
|
|
}
|
|
if(response.status === "success") {
|
|
$("#inviteMessage").html("<div class='alert alert-success'>" + message + "</div>");
|
|
} else {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>" + message + "</div>");
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
var message = "{% trans 'Error sending email.' %}";
|
|
try {
|
|
var resp = xhr.responseJSON;
|
|
if (resp && resp.message) {
|
|
message = resp.message;
|
|
} else {
|
|
message = xhr.statusText;
|
|
}
|
|
} catch(err) {
|
|
message = xhr.statusText;
|
|
}
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>" + message + "</div>");
|
|
}
|
|
});
|
|
} else {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>{% trans 'Invite data is not available.' %}</div>");
|
|
}
|
|
});
|
|
|
|
// Handler for refreshing the invite (update expiration and content)
|
|
$("#refreshInviteButton").on("click", function(e) {
|
|
e.preventDefault();
|
|
if(inviteData && inviteData.uuid) {
|
|
$.ajax({
|
|
url: '/api/peer_invite/',
|
|
data: { invite: inviteData.uuid, action: 'refresh' },
|
|
type: 'GET',
|
|
dataType: 'json',
|
|
success: function(response) {
|
|
if(response.status === "success") {
|
|
// Update the invite details
|
|
inviteData = response.invite_data;
|
|
$("#inviteText").text(inviteData.text_body);
|
|
$("#invitePassword").html("{% trans 'Access Password' %}: <strong>" + inviteData.password + "</strong> {% trans '(Share this password via a separate secure channel)' %}");
|
|
$("#inviteExpiration").text(new Date(inviteData.expiration).toLocaleString());
|
|
$("#inviteMessage").html("<div class='alert alert-success'>" + (response.message || xhr.statusText) + "</div>");
|
|
} else {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>" + (response.message || "{% trans 'Error refreshing invite.' %}") + "</div>");
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
var message = "{% trans 'Error refreshing invite.' %}";
|
|
try {
|
|
var resp = xhr.responseJSON;
|
|
if (resp && resp.message) {
|
|
message = resp.message;
|
|
} else {
|
|
message = xhr.statusText;
|
|
}
|
|
} catch(err) {
|
|
message = xhr.statusText;
|
|
}
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>" + message + "</div>");
|
|
}
|
|
});
|
|
} else {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>{% trans 'No invite data available to refresh.' %}</div>");
|
|
}
|
|
});
|
|
|
|
// Handler for Close Invite button (which deletes the invite)
|
|
$("#closeInviteButton").on("click", function(e) {
|
|
e.preventDefault();
|
|
if(inviteData && inviteData.uuid) {
|
|
$.ajax({
|
|
url: '/api/peer_invite/',
|
|
data: { invite: inviteData.uuid, action: 'delete' },
|
|
type: 'GET',
|
|
dataType: 'json',
|
|
success: function(response) {
|
|
// Hide invite section and show info content regardless of API response
|
|
$(".invite-content").hide();
|
|
$(".info-content").show();
|
|
inviteData = null;
|
|
},
|
|
error: function(xhr, status, error) {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>{% trans 'Error closing invite:' %} " + error + "</div>");
|
|
}
|
|
});
|
|
} else {
|
|
$(".invite-content").hide();
|
|
$(".info-content").show();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|