mirror of
https://github.com/eduardogsilva/wireguard_webadmin.git
synced 2025-04-17 07:55:12 +00:00
847 lines
44 KiB
HTML
847 lines
44 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% 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">Throughput: </b> <span id="peer-throughput-{{ peer.public_key }}"></span><br>
|
|
<span class="peer-extra-info"><b>Transfer:</b> <span id="peer-transfer-{{ peer.public_key }}"></span><br></span>
|
|
<span class="peer-extra-info"><b>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>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>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('Are you sure you want to create a new peer?');">Create Peer</a>
|
|
{% else %}
|
|
<a class="btn btn-primary disabled" href="">Create Peer</a>
|
|
{% endif %}
|
|
<button id="toggleExtraInfo" class="btn btn-outline-primary">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> Throughput:</b> <span id="peerThroughput">--</span></p>
|
|
<p><b><i class="fas fa-dolly nav-icon"></i> Transfer:</b> <span id="peerTransfer">--</span></p>
|
|
<p><b><i class="far fa-clock nav-icon"></i> Latest Handshake:</b> <span id="peerHandshake">--</span></p>
|
|
<p><b><i class="far fa-address-card nav-icon"></i> Endpoints:</b> <span id="peerEndpoints">--</span></p>
|
|
<p><b><i class="fas fa-network-wired nav-icon"></i> 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>
|
|
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="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> 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> Back</button><br>
|
|
<div style="text-align: center; margin-top: 10px;">
|
|
<h5>VPN Invite Details</h5>
|
|
<!-- Container com moldura para o texto do invite -->
|
|
<div id="inviteTextContainer">
|
|
<p id="inviteText"></p>
|
|
</div>
|
|
<p id="invitePassword"></p>
|
|
<p>
|
|
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">Enter Email or WhatsApp Number:</label>
|
|
<input type="text" class="form-control" id="inviteContactInput" placeholder="Email or phone number">
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-outline-secondary" id="copyInviteTextButton"><i class="fas fa-copy"></i> Copy Text</button>
|
|
<button class="btn btn-success" id="sendInviteEmailButton"><i class="fas fa-envelope"></i> Email</button>
|
|
<button class="btn btn-success" id="sendInviteWhatsappButton"><i class="fab fa-whatsapp"></i> WhatsApp</button>
|
|
<button class="btn btn-secondary" id="closeInviteButton"><i class="far fa-trash-alt"></i> 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> Close</button>
|
|
<a href="#" class="btn btn-info" id="downloadConfigButton"><i class="fas fa-download"></i> Config</a>
|
|
<a href="#" class="btn btn-info" id="qrcodeButton"><i class="fas fa-qrcode"></i> QR Code</a>
|
|
<a href="#" class="btn btn-info" id="inviteButton"><i class="fas fa-share"></i> VPN Invite</a>
|
|
<a href="#" class="btn btn-outline-primary" id="editPeerButton"><i class="far fa-edit"></i> Edit</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% else %}
|
|
<div class="alert alert-warning" role="alert">
|
|
<h4 class="alert-heading">No WireGuard Instances Found</h4>
|
|
<p>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">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 = '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 = '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("Hide extras");
|
|
$(".div-peer-text-information").removeClass('position-absolute');
|
|
} else {
|
|
$(this).text("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("Access Password: <strong>" + inviteData.password + "</strong> (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 || "Unknown error") + "</div>");
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
var message = "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'>Invite text copied to clipboard.</div>");
|
|
}, function(err) {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>Failed to copy text.</div>");
|
|
});
|
|
} else {
|
|
$("#inviteMessage").html("<div class='alert alert-danger'>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'>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'>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'>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 = "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'>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("Access Password: <strong>" + inviteData.password + "</strong> (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 || "Error refreshing invite.") + "</div>");
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
var message = "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'>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'>Error closing invite: " + error + "</div>");
|
|
}
|
|
});
|
|
} else {
|
|
$(".invite-content").hide();
|
|
$(".info-content").show();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|