458 lines
19 KiB
Plaintext
458 lines
19 KiB
Plaintext
<%- include('top') %>
|
|
<script type="text/javascript">app.auth.forceLogin();</script>
|
|
|
|
<div class="container mt-4 mb-5">
|
|
<!-- Page Header -->
|
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
|
<div>
|
|
<h3 class="mb-1"><i class="fa-solid fa-shield-halved text-primary me-2"></i>WireGuard Gateway</h3>
|
|
<p class="text-muted small mb-0">Manage VPN exit nodes, client peer profiles, QR codes, and routing configurations.</p>
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-outline-primary btn-sm me-2" onclick="openAddSiteModal()"><i class="fa-solid fa-plus me-1"></i> Add Exit Node</button>
|
|
<button class="btn btn-primary btn-sm" onclick="openAddPeerModal()"><i class="fa-solid fa-user-plus me-1"></i> Add Client Peer</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gateway Gateway Info Card -->
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header bg-dark text-light border-secondary">
|
|
<i class="fa-solid fa-server me-2"></i>Gateway Status & Base Configuration
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<div class="p-3 border rounded bg-light dark-bg-subtle">
|
|
<div class="text-muted small fw-semibold">ENDPOINT ADDRESS</div>
|
|
<div class="font-monospace fw-bold text-primary mt-1" id="gw-endpoint">Loading...</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="p-3 border rounded bg-light dark-bg-subtle">
|
|
<div class="text-muted small fw-semibold">SERVER PUBLIC KEY</div>
|
|
<div class="font-monospace text-truncate mt-1" id="gw-pubkey" style="max-width: 100%;">Loading...</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="p-3 border rounded bg-light dark-bg-subtle">
|
|
<div class="text-muted small fw-semibold">DNS SERVERS</div>
|
|
<div class="font-monospace mt-1" id="gw-dns">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Client Peers Table Card -->
|
|
<div class="card shadow-sm mb-4" id="peers-card">
|
|
<div class="card-header bg-dark text-light border-secondary d-flex justify-content-between align-items-center">
|
|
<span><i class="fa-solid fa-laptop me-2"></i>Client Devices & Peer Profiles</span>
|
|
<button class="btn btn-sm btn-outline-light" onclick="loadPeers()"><i class="fa-solid fa-rotate me-1"></i> Refresh</button>
|
|
</div>
|
|
<div class="card-header actionMessage" style="display:none"></div>
|
|
<div id="peers-table-wrap" class="table-responsive">
|
|
<div class="text-center py-4 text-muted"><i class="fa-solid fa-spinner fa-spin me-2"></i>Loading peers...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Exit Nodes Table Card -->
|
|
<div class="card shadow-sm mb-4" id="sites-card">
|
|
<div class="card-header bg-dark text-light border-secondary d-flex justify-content-between align-items-center">
|
|
<span><i class="fa-solid fa-globe me-2"></i>Site Exit Nodes (Home / Remote Subnets)</span>
|
|
<button class="btn btn-sm btn-outline-light" onclick="loadSites()"><i class="fa-solid fa-rotate me-1"></i> Refresh</button>
|
|
</div>
|
|
<div class="card-header actionMessage" style="display:none"></div>
|
|
<div id="sites-table-wrap" class="table-responsive">
|
|
<div class="text-center py-4 text-muted"><i class="fa-solid fa-spinner fa-spin me-2"></i>Loading exit nodes...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Add / Edit Exit Node -->
|
|
<div class="modal fade" id="wg-site-modal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="wg-site-modal-title"><i class="fa-solid fa-globe me-2"></i>Add Exit Node</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="site-edit-id">
|
|
<div id="site-modal-msg" style="display:none;" class="alert mb-3"></div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Node / Site Name</label>
|
|
<input type="text" class="form-control" id="site-name" placeholder="e.g. 718it-home-gateway">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Endpoint IP / Hostname & Port</label>
|
|
<input type="text" class="form-control font-monospace" id="site-endpoint" placeholder="e.g. 203.0.113.5:51820">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">WireGuard Public Key</label>
|
|
<input type="text" class="form-control font-monospace" id="site-pubkey" placeholder="Base64 public key">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Routable Subnet (CIDR)</label>
|
|
<input type="text" class="form-control font-monospace" id="site-subnet" placeholder="e.g. 192.168.1.0/24 or 0.0.0.0/0">
|
|
</div>
|
|
<div class="mb-3 form-check">
|
|
<input type="checkbox" class="form-check-input" id="site-exitall">
|
|
<label class="form-check-label small" for="site-exitall">Default Exit Node for Full Internet Traffic</label>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Optional Notes</label>
|
|
<input type="text" class="form-control" id="site-note" placeholder="Site location, router hardware, etc.">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="submitSite()"><span id="site-submit-label">Save Exit Node</span></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Add Client Peer -->
|
|
<div class="modal fade" id="wg-peer-modal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fa-solid fa-user-plus me-2"></i>Create Client Peer Profile</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="peer-modal-msg" style="display:none;" class="alert mb-3"></div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Device / Client Name</label>
|
|
<input type="text" class="form-control" id="peer-name" placeholder="e.g. william-phone or laptop-work">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Default Exit Routing</label>
|
|
<select class="form-select" id="peer-exit"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Notes</label>
|
|
<input type="text" class="form-control" id="peer-note" placeholder="Device description">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="submitPeer()">Create Profile</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Edit Peer -->
|
|
<div class="modal fade" id="wg-peer-edit-modal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fa-solid fa-pen me-2"></i>Edit Client Peer</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="peer-edit-id">
|
|
<div id="peer-edit-msg" style="display:none;" class="alert mb-3"></div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Device / Client Name</label>
|
|
<input type="text" class="form-control" id="peer-edit-name">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Exit Node Routing</label>
|
|
<select class="form-select" id="peer-edit-exit"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold small">Notes</label>
|
|
<input type="text" class="form-control" id="peer-edit-note">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="savePeerEdit()">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: QR Code Preview -->
|
|
<div class="modal fade" id="wg-qr-modal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content text-center">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="wg-qr-title"><i class="fa-solid fa-qrcode me-2"></i>WireGuard QR Code</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body py-4">
|
|
<div id="wg-qr-loading" class="py-4 text-muted"><i class="fa-solid fa-spinner fa-spin me-2"></i>Generating QR Code...</div>
|
|
<img id="wg-qr-img" src="" class="img-fluid rounded border p-2 bg-white shadow-sm" style="display:none; max-width: 260px;" alt="QR Code">
|
|
<p class="text-muted small mt-3 mb-0">Scan with the mobile WireGuard app or download the configuration file below.</p>
|
|
</div>
|
|
<div class="modal-footer justify-content-between">
|
|
<button type="button" class="btn btn-outline-primary" id="wg-download-conf-btn"><i class="fa-solid fa-download me-1"></i> Download .conf</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="text/javascript">
|
|
var wgSites = [];
|
|
var wgPeers = [];
|
|
var siteModal, peerModal, peerEditModal, qrModal;
|
|
|
|
$(document).ready(function(){
|
|
siteModal = new bootstrap.Modal(document.getElementById('wg-site-modal'));
|
|
peerModal = new bootstrap.Modal(document.getElementById('wg-peer-modal'));
|
|
peerEditModal = new bootstrap.Modal(document.getElementById('wg-peer-edit-modal'));
|
|
qrModal = new bootstrap.Modal(document.getElementById('wg-qr-modal'));
|
|
loadAll();
|
|
});
|
|
|
|
function esc(s) {
|
|
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
function loadAll() {
|
|
app.api.get('wireguard/gateway-info', function(err, info){
|
|
if(!err && info) {
|
|
$('#gw-endpoint').text(info.endpoint || '(not configured)');
|
|
$('#gw-pubkey').text(info.publicKey || '(not configured)');
|
|
$('#gw-dns').text(info.dns || '(not configured)');
|
|
}
|
|
});
|
|
loadSites(function(){ loadPeers(); });
|
|
}
|
|
|
|
function loadSites(cb) {
|
|
app.api.get('wireguard/sites', function(err, data){
|
|
wgSites = (!err && data && data.results) || [];
|
|
renderSitesTable();
|
|
if (cb) cb();
|
|
});
|
|
}
|
|
|
|
function loadPeers() {
|
|
app.api.get('wireguard/peers', function(err, data){
|
|
wgPeers = (!err && data && data.results) || [];
|
|
renderPeersTable();
|
|
});
|
|
}
|
|
|
|
function renderSitesTable() {
|
|
var $w = $('#sites-table-wrap');
|
|
if (!wgSites.length) {
|
|
$w.html('<div class="text-center py-4 text-muted"><i class="fa-solid fa-globe me-2"></i>No exit nodes configured.<br><small>Add a site to give clients a routable exit point.</small></div>');
|
|
return;
|
|
}
|
|
var html = '<table class="table table-striped table-hover mb-0 align-middle"><thead><tr>'
|
|
+ '<th>Name</th><th>Endpoint</th><th>Subnet</th><th class="text-end">Actions</th>'
|
|
+ '</tr></thead><tbody>';
|
|
wgSites.forEach(function(s){
|
|
var exitBadge = s.exitAll
|
|
? '<span class="badge bg-success ms-2">Full Exit</span>'
|
|
: '<span class="badge bg-info ms-2">Split</span>';
|
|
html += '<tr>'
|
|
+ '<td><strong class="text-dark dark-text-light">' + esc(s.name) + '</strong><br><span class="text-muted small">' + (s.siteId ? 'Site ' + esc(s.siteId) : '') + '</span></td>'
|
|
+ '<td><code class="small text-primary">' + esc(s.endpoint) + '</code></td>'
|
|
+ '<td><code class="small">' + esc(s.subnet || '—') + '</code>' + exitBadge + '</td>'
|
|
+ '<td class="text-end">'
|
|
+ '<button class="btn btn-sm btn-outline-secondary me-1" onclick="openEditSiteModal(\'' + esc(s.id) + '\')" title="Edit"><i class="fa-solid fa-pen"></i></button>'
|
|
+ '<button class="btn btn-sm btn-outline-danger" onclick="deleteSite(\'' + esc(s.id) + '\')" title="Remove"><i class="fa-solid fa-trash"></i></button>'
|
|
+ '</td></tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
$w.html(html);
|
|
}
|
|
|
|
function renderPeersTable() {
|
|
var $w = $('#peers-table-wrap');
|
|
if (!wgPeers.length) {
|
|
$w.html('<div class="text-center py-4 text-muted"><i class="fa-solid fa-laptop me-2"></i>No client peers created yet.<br><small>Create a profile to generate a WireGuard QR code or .conf file.</small></div>');
|
|
return;
|
|
}
|
|
var html = '<table class="table table-striped table-hover mb-0 align-middle"><thead><tr>'
|
|
+ '<th>Device / Peer Name</th><th>Assigned IP</th><th>Exit Node Routing</th><th>Public Key</th><th class="text-end">Actions</th>'
|
|
+ '</tr></thead><tbody>';
|
|
wgPeers.forEach(function(p){
|
|
html += '<tr>'
|
|
+ '<td><strong class="text-dark dark-text-light">' + esc(p.name) + '</strong>' + (p.note ? '<br><span class="text-muted small">' + esc(p.note) + '</span>' : '') + '</td>'
|
|
+ '<td><span class="badge bg-primary font-monospace">' + esc(p.assignedIP || 'Auto') + '</span></td>'
|
|
+ '<td>' + siteLabel(p.exitSiteId) + '</td>'
|
|
+ '<td><code class="small text-muted" title="' + esc(p.publicKey) + '">' + esc(p.publicKey.slice(0, 16)) + '...</code></td>'
|
|
+ '<td class="text-end">'
|
|
+ '<button class="btn btn-sm btn-outline-success me-1" onclick="showQr(\'' + esc(p.id) + '\',\'' + esc(p.name) + '\')" title="QR Code"><i class="fa-solid fa-qrcode"></i></button>'
|
|
+ '<button class="btn btn-sm btn-outline-info me-1" onclick="downloadConf(\'' + esc(p.id) + '\')" title="Download .conf"><i class="fa-solid fa-download"></i></button>'
|
|
+ '<button class="btn btn-sm btn-outline-secondary me-1" onclick="openEditPeerModal(\'' + esc(p.id) + '\')" title="Edit"><i class="fa-solid fa-pen"></i></button>'
|
|
+ '<button class="btn btn-sm btn-outline-danger" onclick="deletePeer(\'' + esc(p.id) + '\')" title="Remove"><i class="fa-solid fa-trash"></i></button>'
|
|
+ '</td></tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
$w.html(html);
|
|
}
|
|
|
|
function siteLabel(exitSiteId) {
|
|
if (!exitSiteId) return '<span class="badge bg-success">Full Tunnel (Gateway)</span>';
|
|
var site = wgSites.find(function(s){ return s.id === exitSiteId; });
|
|
return site
|
|
? '<span class="badge bg-purple text-light" style="background:#7c3aed;"><i class="fa-solid fa-location-dot me-1"></i>' + esc(site.name) + '</span>'
|
|
: '<span class="badge bg-secondary">Unknown</span>';
|
|
}
|
|
|
|
function populateExitSelect(selectId, selectedId) {
|
|
var $sel = $('#' + selectId).empty();
|
|
$sel.append('<option value="">— Full tunnel via gateway (default) —</option>');
|
|
wgSites.forEach(function(s){
|
|
var opt = $('<option>').val(s.id).text(s.name + (s.endpoint ? ' (' + s.endpoint + ')' : ''));
|
|
if (s.id === selectedId) opt.prop('selected', true);
|
|
$sel.append(opt);
|
|
});
|
|
}
|
|
|
|
function openAddSiteModal() {
|
|
$('#site-edit-id').val('');
|
|
$('#site-name, #site-endpoint, #site-pubkey, #site-subnet, #site-note').val('');
|
|
$('#site-exitall').prop('checked', false);
|
|
$('#wg-site-modal-title').html('<i class="fa-solid fa-globe me-2"></i>Add Exit Node');
|
|
$('#site-submit-label').text('Add Exit Node');
|
|
$('#site-modal-msg').hide();
|
|
siteModal.show();
|
|
}
|
|
|
|
function openEditSiteModal(id) {
|
|
var site = wgSites.find(function(s){ return s.id === id; });
|
|
if (!site) return;
|
|
$('#site-edit-id').val(site.id);
|
|
$('#site-name').val(site.name);
|
|
$('#site-endpoint').val(site.endpoint);
|
|
$('#site-pubkey').val(site.publicKey);
|
|
$('#site-subnet').val(site.subnet);
|
|
$('#site-note').val(site.note);
|
|
$('#site-exitall').prop('checked', !!site.exitAll);
|
|
$('#wg-site-modal-title').html('<i class="fa-solid fa-pen me-2"></i>Edit Exit Node');
|
|
$('#site-submit-label').text('Save Changes');
|
|
$('#site-modal-msg').hide();
|
|
siteModal.show();
|
|
}
|
|
|
|
function submitSite() {
|
|
var id = $('#site-edit-id').val();
|
|
var payload = {
|
|
name: $('#site-name').val().trim(),
|
|
endpoint: $('#site-endpoint').val().trim(),
|
|
publicKey: $('#site-pubkey').val().trim(),
|
|
subnet: $('#site-subnet').val().trim() || '0.0.0.0/0',
|
|
note: $('#site-note').val().trim(),
|
|
exitAll: $('#site-exitall').is(':checked'),
|
|
};
|
|
if (!payload.name || !payload.endpoint || !payload.publicKey) {
|
|
return showMsg('#site-modal-msg', 'Name, endpoint, and public key are required.', 'danger');
|
|
}
|
|
var path = id ? 'wireguard/sites/' + id : 'wireguard/sites';
|
|
var action = id ? app.api.patch : app.api.post;
|
|
action(path, payload, function(err){
|
|
if (err) return showMsg('#site-modal-msg', err.message || err, 'danger');
|
|
siteModal.hide();
|
|
loadSites(function(){ loadPeers(); });
|
|
});
|
|
}
|
|
|
|
async function deleteSite(id) {
|
|
var site = wgSites.find(function(s){ return s.id === id; });
|
|
if (!site) return;
|
|
var ok = await app.messages.confirm('Remove exit node "' + site.name + '"? Any peers using it will fall back to full tunnel.', $('#sites-card'), 'danger');
|
|
if (!ok) return;
|
|
app.api.delete('wireguard/sites/' + id, function(err){
|
|
if (err) return app.messages.action('Error: ' + (err.message || err), $('#sites-card'), 'danger');
|
|
loadSites(function(){ loadPeers(); });
|
|
});
|
|
}
|
|
|
|
function openAddPeerModal() {
|
|
$('#peer-name, #peer-note').val('');
|
|
populateExitSelect('peer-exit', '');
|
|
$('#peer-modal-msg').hide();
|
|
peerModal.show();
|
|
}
|
|
|
|
function submitPeer() {
|
|
var payload = {
|
|
name: $('#peer-name').val().trim(),
|
|
exitSiteId: $('#peer-exit').val(),
|
|
note: $('#peer-note').val().trim(),
|
|
};
|
|
if (!payload.name) return showMsg('#peer-modal-msg', 'Device name is required.', 'danger');
|
|
app.api.post('wireguard/peers', payload, function(err, peer){
|
|
if (err) return showMsg('#peer-modal-msg', err.message || err, 'danger');
|
|
peerModal.hide();
|
|
loadPeers();
|
|
if (peer && peer.id) showQr(peer.id, peer.name);
|
|
});
|
|
}
|
|
|
|
function openEditPeerModal(id) {
|
|
var peer = wgPeers.find(function(p){ return p.id === id; });
|
|
if (!peer) return;
|
|
$('#peer-edit-id').val(peer.id);
|
|
$('#peer-edit-name').val(peer.name);
|
|
$('#peer-edit-note').val(peer.note || '');
|
|
populateExitSelect('peer-edit-exit', peer.exitSiteId);
|
|
$('#peer-edit-msg').hide();
|
|
peerEditModal.show();
|
|
}
|
|
|
|
function savePeerEdit() {
|
|
var id = $('#peer-edit-id').val();
|
|
var payload = {
|
|
name: $('#peer-edit-name').val().trim(),
|
|
exitSiteId: $('#peer-edit-exit').val(),
|
|
note: $('#peer-edit-note').val().trim(),
|
|
};
|
|
app.api.patch('wireguard/peers/' + id, payload, function(err){
|
|
if (err) return showMsg('#peer-edit-msg', err.message || err, 'danger');
|
|
peerEditModal.hide();
|
|
loadPeers();
|
|
});
|
|
}
|
|
|
|
async function deletePeer(id) {
|
|
var peer = wgPeers.find(function(p){ return p.id === id; });
|
|
if (!peer) return;
|
|
var ok = await app.messages.confirm('Remove peer "' + peer.name + '"? Their VPN access will be revoked immediately.', $('#peers-card'), 'danger');
|
|
if (!ok) return;
|
|
app.api.delete('wireguard/peers/' + id, function(err){
|
|
if (err) return app.messages.action('Error: ' + (err.message || err), $('#peers-card'), 'danger');
|
|
loadPeers();
|
|
});
|
|
}
|
|
|
|
function showQr(peerId, peerName) {
|
|
$('#wg-qr-title').html('<i class="fa-solid fa-qrcode me-2"></i>' + esc(peerName || 'Peer') + ' Profile');
|
|
$('#wg-qr-img').hide();
|
|
$('#wg-qr-loading').show();
|
|
$('#wg-download-conf-btn').off('click').on('click', function(){ downloadConf(peerId); });
|
|
qrModal.show();
|
|
app.api.get('wireguard/peers/' + peerId + '/qr', function(err, data){
|
|
$('#wg-qr-loading').hide();
|
|
if (err || !data || !data.qr) return;
|
|
$('#wg-qr-img').attr('src', data.qr).show();
|
|
});
|
|
}
|
|
|
|
function downloadConf(peerId) {
|
|
var token = app.auth.getToken ? app.auth.getToken() : '';
|
|
var url = '/api/wireguard/peers/' + peerId + '/conf' + (token ? '?token=' + encodeURIComponent(token) : '');
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
function showMsg(selector, text, type) {
|
|
$(selector)
|
|
.removeClass('alert-success alert-danger alert-warning')
|
|
.addClass('alert alert-' + (type || 'danger'))
|
|
.text(text)
|
|
.show();
|
|
}
|
|
</script>
|
|
|
|
<%- include('bottom') %>
|