add server selection to VPN invite

This commit is contained in:
Eduardo Silva
2026-01-14 14:19:26 -03:00
parent 0284c2e956
commit 44eb36db14
5 changed files with 180 additions and 71 deletions

View File

@@ -57,6 +57,10 @@ class Worker(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
@property
def server_address(self):
return self.hostname or self.ip_address or ''
@property @property
def display_name(self): def display_name(self):
cluster_settings = ClusterSettings.objects.first() cluster_settings = ClusterSettings.objects.first()
@@ -98,7 +102,6 @@ class Worker(models.Model):
return False return False
class WorkerStatus(models.Model): class WorkerStatus(models.Model):
worker = models.OneToOneField(Worker, on_delete=models.CASCADE) worker = models.OneToOneField(Worker, on_delete=models.CASCADE)
last_seen = models.DateTimeField(auto_now=True) last_seen = models.DateTimeField(auto_now=True)

View File

@@ -129,6 +129,16 @@
<a href="{{ invite_settings.download_5_url }}" target="_blank">{{ invite_settings.download_5_label }}</a> <a href="{{ invite_settings.download_5_url }}" target="_blank">{{ invite_settings.download_5_label }}</a>
{% endif %} {% endif %}
</div> </div>
{% if cluster_settings and servers|length > 1 %}
<div style="text-align: center; margin-bottom: 15px;">
<label for="server_select">Server:</label>
<select id="server_select" style="padding: 5px; border-radius: 4px; border: 1px solid #ccc;">
{% for server in servers %}
<option value="{{ server.address }}">{{ server.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="button-group"> <div class="button-group">
<a href="/invite/download_config/?token={{ peer_invite.uuid }}&password={{ password }}" target="_blank" class="btn btn-primary" id="downloadConfigButton">Download Config</a> <a href="/invite/download_config/?token={{ peer_invite.uuid }}&password={{ password }}" target="_blank" class="btn btn-primary" id="downloadConfigButton">Download Config</a>
<a href="#" id="viewQrButton" class="btn btn-secondary"{% if not peer_invite.peer.private_key %} style="opacity: 0.5; cursor: not-allowed;"{% endif %}>View QR Code</a> <a href="#" id="viewQrButton" class="btn btn-secondary"{% if not peer_invite.peer.private_key %} style="opacity: 0.5; cursor: not-allowed;"{% endif %}>View QR Code</a>
@@ -153,32 +163,64 @@
var qrCodeContainer = document.getElementById("qrCodeContainer"); var qrCodeContainer = document.getElementById("qrCodeContainer");
var hasPrivateKey = {% if peer_invite.peer.private_key %}true{% else %}false{% endif %}; var hasPrivateKey = {% if peer_invite.peer.private_key %}true{% else %}false{% endif %};
viewQrButton.addEventListener("click", function(event) { viewQrButton.addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
if (!hasPrivateKey) { if (!hasPrivateKey) {
return false;
}
if (qrCodeContainer.style.display === "none" || qrCodeContainer.style.display === "") {
// Always refresh image on click to ensure current server selection is respected
qrCodeContainer.innerHTML = '';
var img = document.createElement("img");
var server = document.getElementById("server_select") ? document.getElementById("server_select").value : "";
var url = "/invite/download_config/?token={{ peer_invite.uuid }}&password={{ password }}&format=qrcode";
if (server) {
url += "&server=" + encodeURIComponent(server);
}
img.src = url;
img.alt = "QR Code";
qrCodeContainer.appendChild(img);
qrCodeContainer.style.display = "block";
} else {
qrCodeContainer.style.display = "none";
}
});
downloadConfigButton.addEventListener("click", function (event) {
if (!hasPrivateKey) {
if (!confirm("This configuration does not contain a private key. You must add the private key manually in your client before using it.")) {
event.preventDefault();
return false; return false;
} }
if (qrCodeContainer.style.display === "none" || qrCodeContainer.style.display === "") { }
if (qrCodeContainer.getElementsByTagName("img").length === 0) { var server = document.getElementById("server_select") ? document.getElementById("server_select").value : "";
var img = document.createElement("img"); var url = "/invite/download_config/?token={{ peer_invite.uuid }}&password={{ password }}";
img.src = "/invite/download_config/?token={{ peer_invite.uuid }}&password={{ password }}&format=qrcode"; if (server) {
img.alt = "QR Code"; url += "&server=" + encodeURIComponent(server);
qrCodeContainer.appendChild(img); }
} this.href = url;
qrCodeContainer.style.display = "block";
} else {
qrCodeContainer.style.display = "none";
}
}); });
downloadConfigButton.addEventListener("click", function(event) { // Update href immediately if dropdown changes (optional, but good for UX)
if (!hasPrivateKey) { var serverSelect = document.getElementById("server_select");
if (!confirm("This configuration does not contain a private key. You must add the private key manually in your client before using it.")) { if (serverSelect) {
event.preventDefault(); serverSelect.addEventListener("change", function () {
return false; var server = this.value;
} var url = "/invite/download_config/?token={{ peer_invite.uuid }}&password={{ password }}";
if (server) {
url += "&server=" + encodeURIComponent(server);
}
downloadConfigButton.href = url;
// If QR code is visible, reload it
if (qrCodeContainer.style.display === "block") {
viewQrButton.click(); // Hide
setTimeout(function () { viewQrButton.click(); }, 100); // Show again
} }
}); });
}
}); });
</script> </script>
{% endif %} {% endif %}

View File

@@ -190,25 +190,35 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fas fa-times"></i> {% trans 'Close' %}</button> {% if cluster_settings and servers|length > 1 %}
<a href="#" class="btn btn-info" id="downloadConfigButton"><i class="fas fa-download"></i> {% trans 'Config' %}</a> <div class="mr-auto form-inline">
<a href="#" class="btn btn-info" id="qrcodeButton"><i class="fas fa-qrcode"></i> {% trans 'QR Code' %}</a> <label class="mr-2" for="server_select">{% trans 'Server' %}:</label>
<a href="#" class="btn btn-info" id="inviteButton"><i class="fas fa-share"></i> {% trans 'VPN Invite' %}</a> <select class="form-control" id="server_select">
<a href="#" class="btn btn-outline-primary" id="editPeerButton"><i class="far fa-edit"></i> {% trans 'Edit' %}</a> {% for server in servers %}
</div> <option value="{{ server.address }}">{{ server.name }}</option>
{% endfor %}
</select>
</div> </div>
{% endif %}
<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>
</div>
</div>
{% else %} {% else %}
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
<h4 class="alert-heading">{% trans 'No WireGuard Instances Found' %}</h4> <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> <p>{% trans 'There are no WireGuard instances configured. You can add a new instance by clicking the button below.' %}</p>
</div> </div>
<p> <p>
<a href="/server/manage/" class="btn btn-primary">{% trans 'Add WireGuard Instance' %}</a> <a href="/server/manage/" class="btn btn-primary">{% trans 'Add WireGuard Instance' %}</a>
</p> </p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@@ -279,21 +289,46 @@
</script> </script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
$("#qrcodeButton").on("click", function(e) { $("#qrcodeButton").on("click", function (e) {
e.preventDefault(); e.preventDefault();
if ($(this).hasClass('disabled')) { if ($(this).hasClass('disabled')) {
return false; return false;
} }
var uuid = $("#peerPreviewModal").data("peer-uuid"); var uuid = $("#peerPreviewModal").data("peer-uuid");
$("#qrCodeImg").attr("src", "/tools/download_peer_config/?uuid=" + uuid + "&format=qrcode"); var server = $('#server_select').val(); // Get selected server
$(".info-content").hide(); var url = "/tools/download_peer_config/?uuid=" + uuid + "&format=qrcode";
$(".invite-content").hide(); if (server) {
$(".qr-code-content").show(); url += "&server=" + encodeURIComponent(server);
}); }
$("#backButton").on("click", function(e) { $("#qrCodeImg").attr("src", url);
$(".info-content").hide();
$(".invite-content").hide();
$(".qr-code-content").show();
});
$("#server_select").on("change", function () {
var uuid = $('#peerPreviewModal').data('peer-uuid');
var server = $(this).val();
// Update download config button
var downloadUrl = '/tools/download_peer_config/?uuid=' + uuid;
if (server) {
downloadUrl += '&server=' + encodeURIComponent(server);
}
$('#downloadConfigButton').attr('href', downloadUrl);
// Update QR code button (href is used for storing base url, actual action is click)
var qrUrl = '/tools/download_peer_config/?uuid=' + uuid + '&format=qrcode';
if (server) {
qrUrl += '&server=' + encodeURIComponent(server);
}
$('#qrcodeButton').attr('href', qrUrl);
});
$("#backButton").on("click", function (e) {
e.preventDefault(); e.preventDefault();
$(".qr-code-content").hide(); $(".qr-code-content").hide();
$(".info-content").show(); $(".info-content").show();
@@ -329,19 +364,29 @@
var peerAllowedIPs = peerElem.querySelector('[id^="peer-allowed-ips-"]').innerHTML; var peerAllowedIPs = peerElem.querySelector('[id^="peer-allowed-ips-"]').innerHTML;
var peerLocation = peerElem.querySelector('[id^="peer-location-"]').innerText; var peerLocation = peerElem.querySelector('[id^="peer-location-"]').innerText;
// Update the modal fields with the card values // Update the modal fields with the card values
$('#peerPreviewModalLabel').text(peerNameFromCard); $('#peerPreviewModalLabel').text(peerNameFromCard);
$('#peerThroughput').html(peerThroughput); $('#peerThroughput').html(peerThroughput);
$('#peerTransfer').text(peerTransfer); $('#peerTransfer').text(peerTransfer);
$('#peerHandshake').text(peerHandshake); $('#peerHandshake').text(peerHandshake);
$('#peerEndpoints').text(peerEndpoints); $('#peerEndpoints').text(peerEndpoints);
$('#peerLocation').text(peerLocation); $('#peerLocation').text(peerLocation);
$('#peerAllowedIPs').html(peerAllowedIPs); $('#peerAllowedIPs').html(peerAllowedIPs);
$('#editPeerButton').attr('href', '/peer/manage/?peer=' + uuid); $('#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'); var server = $('#server_select').val();
$('#graphImg').attr('src', '/rrd/graph/?peer=' + uuid).show(); var downloadUrl = '/tools/download_peer_config/?uuid=' + uuid;
$('#peerPreviewModal').data('peer-uuid', uuid); var qrUrl = '/tools/download_peer_config/?uuid=' + uuid + '&format=qrcode';
if (server) {
downloadUrl += '&server=' + encodeURIComponent(server);
qrUrl += '&server=' + encodeURIComponent(server);
}
$('#downloadConfigButton').attr('href', downloadUrl);
$('#qrcodeButton').attr('href', qrUrl);
$('#graphImg').attr('src', '/rrd/graph/?peer=' + uuid).show();
$('#peerPreviewModal').data('peer-uuid', uuid);
$.ajax({ $.ajax({
url: '/api/peer_info/', url: '/api/peer_info/',

View File

@@ -2,6 +2,7 @@ from django.http import Http404
from django.shortcuts import render from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
from cluster.models import ClusterSettings, Worker
from vpn_invite.models import PeerInvite, InviteSettings from vpn_invite.models import PeerInvite, InviteSettings
@@ -13,12 +14,24 @@ def view_public_vpn_invite(request):
except: except:
raise Http404 raise Http404
# Initialize context with default values cluster_settings = ClusterSettings.objects.filter(name='cluster_settings', enabled=True).first()
servers = []
if cluster_settings:
if cluster_settings.primary_enable_wireguard:
servers.append({'name': 'Primary Server', 'address': ''})
for worker in Worker.objects.filter(enabled=True):
listen_port = peer_invite.peer.wireguard_instance.listen_port
worker_address = f"{worker.server_address}:{listen_port}"
servers.append({'name': worker.display_name, 'address': worker_address})
context = { context = {
'peer_invite': peer_invite, 'peer_invite': peer_invite,
'invite_settings': invite_settings, 'invite_settings': invite_settings,
'authenticated': False, 'authenticated': False,
'error': '' 'error': '',
'cluster_settings': cluster_settings,
'servers': servers
} }
if request.method == 'POST': if request.method == 'POST':

View File

@@ -27,7 +27,7 @@ def clean_command_field(command_field):
return cleaned_field return cleaned_field
def generate_peer_config(peer_uuid): def generate_peer_config(peer_uuid, server_address=None):
peer = get_object_or_404(Peer, uuid=peer_uuid) peer = get_object_or_404(Peer, uuid=peer_uuid)
wg_instance = peer.wireguard_instance wg_instance = peer.wireguard_instance
@@ -46,6 +46,11 @@ def generate_peer_config(peer_uuid):
dns_entries = [wg_instance.dns_primary, wg_instance.dns_secondary] dns_entries = [wg_instance.dns_primary, wg_instance.dns_secondary]
dns_line = ", ".join(filter(None, dns_entries)) dns_line = ", ".join(filter(None, dns_entries))
if server_address:
endpoint = server_address
else:
endpoint = f"{wg_instance.hostname}:{wg_instance.listen_port}"
config_lines = [ config_lines = [
"[Interface]", "[Interface]",
f"PrivateKey = {peer.private_key}", f"PrivateKey = {peer.private_key}",
@@ -53,7 +58,7 @@ def generate_peer_config(peer_uuid):
f"DNS = {dns_line}" if dns_line else "", f"DNS = {dns_line}" if dns_line else "",
"\n[Peer]", "\n[Peer]",
f"PublicKey = {wg_instance.public_key}", f"PublicKey = {wg_instance.public_key}",
f"Endpoint = {wg_instance.hostname}:{wg_instance.listen_port}", f"Endpoint = {endpoint}",
f"AllowedIPs = {allowed_ips_line}", f"AllowedIPs = {allowed_ips_line}",
f"PresharedKey = {peer.pre_shared_key}" if peer.pre_shared_key else "", f"PresharedKey = {peer.pre_shared_key}" if peer.pre_shared_key else "",
f"PersistentKeepalive = {peer.persistent_keepalive}", f"PersistentKeepalive = {peer.persistent_keepalive}",
@@ -194,8 +199,9 @@ def download_config_or_qrcode(request):
raise Http404 raise Http404
format_type = request.GET.get('format', 'conf') format_type = request.GET.get('format', 'conf')
server_address = request.GET.get('server')
config_content = generate_peer_config(peer.uuid) config_content = generate_peer_config(peer.uuid, server_address=server_address)
if format_type == 'qrcode': if format_type == 'qrcode':
qr = qrcode.QRCode( qr = qrcode.QRCode(