1072 lines
48 KiB
HTML
1072 lines
48 KiB
HTML
{{define "title"}}{{ tr .UILang "meta.title_clients" }}{{end}}
|
|
|
|
{{define "top_css"}}
|
|
<style>
|
|
.paused-client {
|
|
transition: transform .2s;
|
|
cursor: pointer;
|
|
}
|
|
i[class^="paused-client"]:hover { transform: scale(1.5); }
|
|
/* Inner margin from the panel edge (.container-fluid) */
|
|
.wg-clients-toolbar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;padding:14px 0;border-bottom:1px solid var(--bdr,rgba(255,255,255,.08))}
|
|
.wg-client-search{flex:1;min-width:220px;max-width:420px}
|
|
.wg-client-search input{width:100%;background:var(--ele);border:1px solid var(--bdr2);border-radius:10px;padding:8px 10px;color:var(--t1);font-size:12px;outline:none}
|
|
.wg-client-search input:focus{border-color:rgba(239,83,80,.35)}
|
|
.wg-client-controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
|
|
.wg-filter-pills{display:inline-flex;gap:6px;flex-wrap:wrap}
|
|
.wg-filter-pills .wg-pill{border:1px solid rgba(255,255,255,.12);background:transparent;color:var(--t2,#9e9e9e);padding:6px 12px;border-radius:999px;font-size:11px;font-weight:600;cursor:pointer;line-height:1.2}
|
|
.wg-filter-pills .wg-pill:hover{color:var(--t1,#eee);border-color:rgba(239,83,80,.35)}
|
|
.wg-filter-pills .wg-pill.active{background:rgba(239,83,80,.14);border-color:rgba(239,83,80,.45);color:#EF5350}
|
|
</style>
|
|
{{end}}
|
|
|
|
{{define "username"}}
|
|
{{ .username }}
|
|
{{end}}
|
|
|
|
{{define "page_title"}}{{ tr .UILang "page.clients" }}{{end}}
|
|
|
|
{{define "page_content"}}
|
|
<section class="content wg-clients-page" style="max-width:100%">
|
|
<div class="container-fluid" style="padding:0;width:100%;max-width:1100px">
|
|
{{if .globalSettings}}<input type="hidden" id="wg-global-dns" value="{{StringsJoin .globalSettings.DNSServers ","}}">
|
|
<input type="hidden" id="wg-global-keepalive-sec" value="{{.globalSettings.PersistentKeepalive}}">{{end}}
|
|
<div class="wg-clients-toolbar">
|
|
<div class="wg-client-search">
|
|
<input type="text" id="client-search-input" autocomplete="off" placeholder="{{ tr .UILang "clients.search_ph" }}">
|
|
</div>
|
|
<div class="wg-client-controls">
|
|
<div class="wg-filter-pills" id="wg-client-pills" role="tablist">
|
|
<button type="button" class="wg-pill" data-filter="enabled">{{ tr .UILang "clients.enabled" }}</button>
|
|
<button type="button" class="wg-pill" data-filter="disabled">{{ tr .UILang "clients.disabled" }}</button>
|
|
</div>
|
|
<button type="button" class="wg-btn bp" data-toggle="modal" data-target="#modal_new_client"><i class="fas fa-plus"></i> {{ tr .UILang "clients.new_btn" }}</button>
|
|
</div>
|
|
</div>
|
|
<div id="client-list" class="wg-client-stack"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="modal fade" id="modal_email_client">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">{{ tr .UILang "clients.email_modal_title" }}</h4>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="{{ tr .UILang "aria.close" }}">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<form name="frm_email_client" id="frm_email_client">
|
|
<div class="modal-body">
|
|
<input type="hidden" id="e_client_id" name="e_client_id">
|
|
<div class="form-group">
|
|
<label for="e_client_email" class="control-label">{{ tr .UILang "clients.email_label" }}</label>
|
|
<input type="text" class="form-control" id="e_client_email" name="e_client_email">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-between">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{ tr .UILang "modal.apply.cancel" }}</button>
|
|
<button type="submit" class="btn btn-success">{{ tr .UILang "clients.email_modal_send" }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- /.modal-content -->
|
|
</div>
|
|
<!-- /.modal-dialog -->
|
|
</div>
|
|
<!-- /.modal -->
|
|
|
|
<div class="modal fade" id="modal_qr_client">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">{{ tr .UILang "clients.modal_qr" }}</h4>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="{{ tr .UILang "aria.close" }}">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="qr_client_id" name="qr_client_id">
|
|
<img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" />
|
|
<!-- do not include FwMark in any client configs: it is INVALID. -->
|
|
</div>
|
|
</div>
|
|
<!-- /.modal-content -->
|
|
</div>
|
|
<!-- /.modal-dialog -->
|
|
</div>
|
|
<!-- /.modal -->
|
|
|
|
<div class="modal fade" id="modal_telegram_client">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">{{ tr .UILang "clients.modal_telegram_title" }}</h4>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="{{ tr .UILang "aria.close" }}">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<form name="frm_telegram_client" id="frm_telegram_client">
|
|
<div class="modal-body">
|
|
<input type="hidden" id="tg_client_id" name="tg_client_id">
|
|
<div class="form-group">
|
|
<label for="tg_client_userid" class="control-label">{{ tr .UILang "clients.telegram_userid" }}</label>
|
|
<input type="text" class="form-control" id="tg_client_userid" name="tg_client_userid">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-between">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{ tr .UILang "modal.apply.cancel" }}</button>
|
|
<button type="submit" class="btn btn-success">{{ tr .UILang "clients.email_modal_send" }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- /.modal-content -->
|
|
</div>
|
|
<!-- /.modal-dialog -->
|
|
</div>
|
|
<!-- /.modal -->
|
|
|
|
<div class="modal fade" id="modal_edit_client">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">{{ tr .UILang "clients.modal_edit_client" }}</h4>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="{{ tr .UILang "aria.close" }}">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<form name="frm_edit_client" id="frm_edit_client">
|
|
<div class="modal-body">
|
|
<input type="hidden" id="_client_id" name="_client_id">
|
|
<div class="form-group">
|
|
<label for="_client_name" class="control-label">Name</label>
|
|
<input type="text" class="form-control" id="_client_name" name="_client_name">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_client_email" class="control-label">Email</label>
|
|
<input type="text" class="form-control" id="_client_email" name="client_email">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_subnet_ranges" class="control-label">Subnet range</label>
|
|
<select id="_subnet_ranges" class="select2"
|
|
data-placeholder="Select a subnet range" style="width: 100%;">
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_client_allocated_ips" class="control-label">IP Allocation</label>
|
|
<input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_client_allowed_ips" class="control-label">Allowed IPs</label>
|
|
<input type="text" data-role="tagsinput" class="form-control" id="_client_allowed_ips">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_client_extra_allowed_ips" class="control-label">Extra Allowed IPs</label>
|
|
<input type="text" data-role="tagsinput" class="form-control"
|
|
id="_client_extra_allowed_ips">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_client_endpoint" class="control-label">Endpoint</label>
|
|
<input type="text" class="form-control" id="_client_endpoint" name="client_endpoint">
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="icheck-primary d-inline">
|
|
<input type="checkbox" id="_use_server_dns">
|
|
<label for="_use_server_dns">
|
|
Use server DNS
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="icheck-primary d-inline">
|
|
<input type="checkbox" id="_enabled">
|
|
<label for="_enabled">
|
|
Enable this client
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<details>
|
|
<summary><strong>Public and Preshared Keys</strong>
|
|
<i class="fas fa-info-circle" data-toggle="tooltip"
|
|
data-original-title="Update the server stored
|
|
client Public and Preshared keys.">
|
|
</i>
|
|
</summary>
|
|
<div class="form-group" style="margin-top: 1rem">
|
|
<label for="_client_public_key" class="control-label">
|
|
Public Key
|
|
</label>
|
|
<input type="text" class="form-control" id="_client_public_key" name="_client_public_key" aria-invalid="false">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_client_preshared_key" class="control-label">
|
|
Preshared Key
|
|
</label>
|
|
<input type="text" class="form-control" id="_client_preshared_key" name="_client_preshared_key">
|
|
</div>
|
|
</details>
|
|
<details style="margin-top: 0.5rem;">
|
|
<summary><strong>Additional configuration</strong>
|
|
</summary>
|
|
<div class="form-group" style="margin-top: 0.5rem;">
|
|
<label for="_client_telegram_userid" class="control-label">Telegram userid</label>
|
|
<input type="text" class="form-control" id="_client_telegram_userid" name="_client_telegram_userid">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_additional_notes" class="control-label">Notes</label>
|
|
<textarea class="form-control" style="min-height: 6rem;" id="_additional_notes" name="_additional_notes" placeholder="Additional notes about this client"></textarea>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="modal-footer justify-content-between">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{ tr .UILang "modal.apply.cancel" }}</button>
|
|
<button type="submit" class="btn btn-success" id="edit_client_save" disabled>{{ tr .UILang "clients.save_btn" }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- /.modal-content -->
|
|
</div>
|
|
<!-- /.modal-dialog -->
|
|
</div>
|
|
<!-- /.modal -->
|
|
|
|
<div class="modal fade" id="modal_pause_client">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content bg-warning">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">{{ tr .UILang "clients.modal_disable" }}</h4>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="{{ tr .UILang "aria.close" }}">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
</div>
|
|
<div class="modal-footer justify-content-between">
|
|
<button type="button" class="btn btn-outline-dark" data-dismiss="modal">{{ tr .UILang "modal.apply.cancel" }}</button>
|
|
<button type="button" class="btn btn-outline-dark" id="pause_client_confirm">{{ tr .UILang "clients.modal_apply_btn" }}</button>
|
|
</div>
|
|
</div>
|
|
<!-- /.modal-content -->
|
|
</div>
|
|
<!-- /.modal-dialog -->
|
|
</div>
|
|
<!-- /.modal -->
|
|
|
|
<div class="modal fade" id="modal_remove_client">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content bg-danger">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">{{ tr .UILang "clients.modal_remove" }}</h4>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="{{ tr .UILang "aria.close" }}">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
</div>
|
|
<div class="modal-footer justify-content-between">
|
|
<button type="button" class="btn btn-outline-dark" data-dismiss="modal">{{ tr .UILang "modal.apply.cancel" }}</button>
|
|
<button type="button" class="btn btn-outline-dark" id="remove_client_confirm">{{ tr .UILang "clients.modal_apply_btn" }}</button>
|
|
</div>
|
|
</div>
|
|
<!-- /.modal-content -->
|
|
</div>
|
|
<!-- /.modal-dialog -->
|
|
</div>
|
|
<!-- /.modal -->
|
|
{{end}}
|
|
|
|
{{define "bottom_js"}}
|
|
<script>
|
|
function wgClientMsg(key, name) {
|
|
var s = String(wgT(key) || '');
|
|
if (name != null && name !== undefined) s = s.replace(/\{name\}/g, String(name));
|
|
return s;
|
|
}
|
|
|
|
function populateClientList() {
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'GET',
|
|
url: '{{.basePath}}/api/clients',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
success: function (data) {
|
|
$.getJSON('{{.basePath}}/api/wg-peer-stats')
|
|
.done(function (stats) {
|
|
renderClientList(data, stats || {});
|
|
})
|
|
.fail(function () {
|
|
renderClientList(data, {});
|
|
});
|
|
},
|
|
error: function (jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setClientStatus(clientID, status) {
|
|
const data = {"id": clientID, "status": status};
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/client/set-status',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
data: JSON.stringify(data),
|
|
success: function () {
|
|
if (typeof updateApplyConfigVisibility === 'function') {
|
|
updateApplyConfigVisibility();
|
|
}
|
|
},
|
|
error: function (jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
}
|
|
|
|
function resumeClient(clientID) {
|
|
setClientStatus(clientID, true);
|
|
var divElement = document.getElementById("paused_" + clientID);
|
|
if (divElement) {
|
|
divElement.style.display = "none";
|
|
divElement.style.visibility = "hidden";
|
|
divElement.style.pointerEvents = "none";
|
|
}
|
|
var card = document.getElementById("client_" + clientID);
|
|
if (card) {
|
|
card.setAttribute("data-wg-enabled", "true");
|
|
}
|
|
wgSyncToggleFromCard(clientID)
|
|
}
|
|
|
|
function pauseClient(clientID) {
|
|
setClientStatus(clientID, false);
|
|
var divElement = document.getElementById("paused_" + clientID);
|
|
if (divElement) {
|
|
divElement.style.display = "flex";
|
|
divElement.style.visibility = "visible";
|
|
divElement.style.pointerEvents = "auto";
|
|
}
|
|
var card = document.getElementById("client_" + clientID);
|
|
if (card) {
|
|
card.setAttribute("data-wg-enabled", "false");
|
|
card.setAttribute("data-wg-vpn", "0");
|
|
card.setAttribute("data-wg-online", "0");
|
|
}
|
|
wgSyncToggleFromCard(clientID)
|
|
}
|
|
|
|
/** Sync checkbox + badges in helper-rendered cards */
|
|
window.wgSyncToggleFromCard = function (clientID) {
|
|
var card = document.getElementById("client_" + clientID);
|
|
if (!card) return;
|
|
var en = card.getAttribute("data-wg-enabled") === "true";
|
|
card.querySelectorAll(".wg-cc-toggle").forEach(function (cb) {
|
|
cb.checked = en;
|
|
});
|
|
if (typeof window.wgApplyPeerTrafficStats !== "function") return;
|
|
$.getJSON('{{.basePath}}/api/wg-peer-stats').done(window.wgApplyPeerTrafficStats);
|
|
}
|
|
|
|
/** Toggle in card header — uses existing resume/pause APIs */
|
|
window.wgPeerToggleEnable = function (ev, el) {
|
|
if (ev) ev.stopPropagation();
|
|
var id = el.getAttribute("data-clientid");
|
|
if (!id) return;
|
|
var want = el.checked;
|
|
if (want) resumeClient(id); else pauseClient(id);
|
|
}
|
|
|
|
// updateIPAllocationSuggestion function for automatically fill
|
|
// the IP Allocation input with suggested ip addresses
|
|
// FOR CHANGING A SUBNET OF AN EXISTING CLIENT
|
|
function updateIPAllocationSuggestionExisting() {
|
|
let subnetRange = $("#_subnet_ranges").select2('val');
|
|
|
|
if (!subnetRange || subnetRange.length === 0) {
|
|
subnetRange = '__default_any__'
|
|
}
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'GET',
|
|
url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`,
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
success: function(data) {
|
|
const allocated_ips = $("#_client_allocated_ips").val().split(",");
|
|
allocated_ips.forEach(function (item, index) {
|
|
$('#_client_allocated_ips').removeTag(escape(item));
|
|
})
|
|
data.forEach(function (item, index) {
|
|
$('#_client_allocated_ips').addTag(item);
|
|
})
|
|
},
|
|
error: function(jqXHR, exception) {
|
|
const allocated_ips = $("#_client_allocated_ips").val().split(",");
|
|
allocated_ips.forEach(function (item, index) {
|
|
$('#_client_allocated_ips').removeTag(escape(item));
|
|
})
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateSearchList() {
|
|
$.getJSON("{{.basePath}}/api/subnet-ranges", null, function(data) {
|
|
$("#status-selector option").remove();
|
|
$("#status-selector").append(
|
|
$("<option></option>").text("All").val("All"),
|
|
$("<option></option>").text("Enabled").val("Enabled"),
|
|
$("<option></option>").text("Disabled").val("Disabled")
|
|
);
|
|
$.each(data, function(index, item) {
|
|
$("#status-selector").append(
|
|
$("<option></option>")
|
|
.text(item)
|
|
.val(item)
|
|
);
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
<script>
|
|
window.__wgModalTrigger = null;
|
|
function wgModalTrigger(ev) {
|
|
var $t = $(ev.relatedTarget);
|
|
if ($t && $t.length) return $t;
|
|
if (window.__wgModalTrigger) {
|
|
var $x = $(window.__wgModalTrigger);
|
|
window.__wgModalTrigger = null;
|
|
if ($x && $x.length) return $x;
|
|
}
|
|
return $();
|
|
}
|
|
function wgStashModalTrigger(el) {
|
|
if (el && (el.nodeType === 1)) window.__wgModalTrigger = el;
|
|
}
|
|
/* mousedown runs before Bootstrap's click/open — fixes missing relatedTarget */
|
|
$(document).on('mousedown pointerdown', '#client-list [data-toggle="modal"]', function () {
|
|
wgStashModalTrigger(this);
|
|
});
|
|
$(document).on('click', '#client-list [data-toggle="modal"]', function () {
|
|
wgStashModalTrigger(this);
|
|
});
|
|
|
|
/** Poll WG counters so download/upload chips update without a full reload */
|
|
var WG_TRAFFIC_POLL_MS = 4000;
|
|
var wgTrafficPollTimer = null;
|
|
function wgStartPeerTrafficPolling() {
|
|
if (wgTrafficPollTimer) {
|
|
clearInterval(wgTrafficPollTimer);
|
|
wgTrafficPollTimer = null;
|
|
}
|
|
wgTrafficPollTimer = setInterval(function () {
|
|
if (typeof window.wgApplyPeerTrafficStats !== 'function') return;
|
|
$.getJSON('{{.basePath}}/api/wg-peer-stats').done(window.wgApplyPeerTrafficStats);
|
|
}, WG_TRAFFIC_POLL_MS);
|
|
}
|
|
|
|
// load client list
|
|
$(document).ready(function () {
|
|
updateSearchList();
|
|
populateClientList();
|
|
wgStartPeerTrafficPolling();
|
|
})
|
|
|
|
// override :contains to be case-insensitive
|
|
$(document).ready(function () {
|
|
jQuery.expr[':'].contains = function(a, i, m) {
|
|
return jQuery(a).text().toUpperCase()
|
|
.indexOf(m[3].toUpperCase()) >= 0;
|
|
};
|
|
})
|
|
|
|
function applyClientFilters() {
|
|
var q = ($('#client-search-input').val() || '').trim().toUpperCase();
|
|
var active = $('#wg-client-pills .wg-pill.active').data('filter') || '';
|
|
$('.wg-client-card').each(function () {
|
|
var $card = $(this);
|
|
var enabled = $card.attr('data-wg-enabled') === 'true';
|
|
var matchesState = (active === 'enabled' && enabled) || (active === 'disabled' && !enabled) || (!active);
|
|
if (!matchesState) {
|
|
$card.hide();
|
|
return;
|
|
}
|
|
if (!q) {
|
|
$card.show();
|
|
return;
|
|
}
|
|
var text = ($card.text() || '').toUpperCase();
|
|
$card.toggle(text.indexOf(q) !== -1);
|
|
});
|
|
}
|
|
|
|
$(document).on('click', '#wg-client-pills .wg-pill', function () {
|
|
var which = $(this).data('filter');
|
|
if ($(this).hasClass('active')) {
|
|
$(this).removeClass('active');
|
|
} else {
|
|
$('#wg-client-pills .wg-pill').removeClass('active');
|
|
$(this).addClass('active');
|
|
}
|
|
$('#status-selector').val('All');
|
|
applyClientFilters();
|
|
});
|
|
|
|
$('#client-search-input').on('keyup input', function () {
|
|
$("#status-selector").val("All");
|
|
applyClientFilters();
|
|
})
|
|
|
|
$("#status-selector").on('change', function () {
|
|
$('#client-search-input').val("");
|
|
$('#wg-client-pills .wg-pill').removeClass('active');
|
|
switch ($("#status-selector").val()) {
|
|
case "All":
|
|
$('.wg-client-card').show();
|
|
break;
|
|
case "Enabled":
|
|
$('.wg-client-card').hide();
|
|
$('.wg-client-card[data-wg-enabled="true"]').show();
|
|
break;
|
|
case "Disabled":
|
|
$('.wg-client-card').hide();
|
|
$('.wg-client-card[data-wg-enabled="false"]').show();
|
|
break;
|
|
default:
|
|
$('.wg-client-card').hide();
|
|
const selectedSR = $("#status-selector").val();
|
|
$(".fa-subnetrange").each(function () {
|
|
const srs = $(this).parent().text().trim().split(',');
|
|
for (const sr of srs) {
|
|
if (sr === selectedSR) {
|
|
$(this).closest('.wg-client-card').show();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
|
|
// modal_pause_client modal event
|
|
$("#modal_pause_client").on('show.bs.modal', function (event) {
|
|
const button = wgModalTrigger(event);
|
|
const client_id = button.data('clientid');
|
|
if (!client_id) return;
|
|
const client_name = button.data('clientname');
|
|
const modal = $(this);
|
|
const card = document.getElementById("client_" + client_id);
|
|
const currentlyEnabled = !!(card && card.getAttribute("data-wg-enabled") === "true");
|
|
const targetStatus = false;
|
|
modal.find('.modal-body').text(wgClientMsg('clients.about_disable_fmt', client_name));
|
|
modal.find('#pause_client_confirm').val(client_id).data('target-status', targetStatus);
|
|
// enable only when this modal action would produce a real state change
|
|
modal.find('#pause_client_confirm').prop('disabled', currentlyEnabled === targetStatus);
|
|
})
|
|
|
|
// pause_client_confirm button event
|
|
$(document).ready(function () {
|
|
$("#pause_client_confirm").click(function () {
|
|
const client_id = $(this).val();
|
|
if ($(this).prop('disabled')) return;
|
|
pauseClient(client_id);
|
|
$("#modal_pause_client").modal('hide');
|
|
});
|
|
});
|
|
|
|
// modal_remove_client modal event
|
|
$("#modal_remove_client").on('show.bs.modal', function (event) {
|
|
const button = wgModalTrigger(event);
|
|
const client_id = button.data('clientid');
|
|
if (!client_id) return;
|
|
const client_name = button.data('clientname');
|
|
const modal = $(this);
|
|
modal.find('.modal-body').text(wgClientMsg('clients.about_remove_fmt', client_name));
|
|
modal.find('#remove_client_confirm').val(client_id);
|
|
})
|
|
|
|
// remove_client_confirm button event
|
|
$(document).ready(function () {
|
|
$("#remove_client_confirm").click(function () {
|
|
const client_id = $(this).val();
|
|
const data = {"id": client_id};
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/remove-client',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
data: JSON.stringify(data),
|
|
success: function(data) {
|
|
$("#modal_remove_client").modal('hide');
|
|
toastr.success(wgT('clients.removed_ok'));
|
|
const divElement = document.getElementById('client_' + client_id);
|
|
divElement.style.display = "none";
|
|
updateApplyConfigVisibility()
|
|
},
|
|
error: function(jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Edit client modal event
|
|
// This fills the modal dialogue with data from the DB when we open the edit menu
|
|
var wgEditClientSnap = null;
|
|
var wgEditClientTimer = null;
|
|
|
|
function wgEditClientPayload() {
|
|
const extraRaw = $("#_client_extra_allowed_ips").val();
|
|
return JSON.stringify({
|
|
id: $("#_client_id").val() || "",
|
|
name: $("#_client_name").val() || "",
|
|
email: $("#_client_email").val() || "",
|
|
telegram_userid: $("#_client_telegram_userid").val() || "",
|
|
allocated_ips: ($("#_client_allocated_ips").val() || "").split(",").map(function (x) { return x.trim(); }).filter(Boolean),
|
|
allowed_ips: ($("#_client_allowed_ips").val() || "").split(",").map(function (x) { return x.trim(); }).filter(Boolean),
|
|
extra_allowed_ips: (extraRaw || "").split(",").map(function (x) { return x.trim(); }).filter(Boolean),
|
|
endpoint: $("#_client_endpoint").val() || "",
|
|
use_server_dns: $("#_use_server_dns").is(':checked'),
|
|
enabled: $("#_enabled").is(':checked'),
|
|
public_key: $("#_client_public_key").val() || "",
|
|
preshared_key: $("#_client_preshared_key").val() || "",
|
|
additional_notes: $("#_additional_notes").val() || "",
|
|
subnet_range: $("#_subnet_ranges").val() || ""
|
|
});
|
|
}
|
|
|
|
function wgEditClientRefreshSaveBtn() {
|
|
var btn = $("#edit_client_save");
|
|
if (!btn.length) return;
|
|
var dirty = String(wgEditClientSnap || "") !== wgEditClientPayload();
|
|
btn.prop("disabled", !dirty);
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
$("#modal_edit_client").on('show.bs.modal', function (event) {
|
|
let modal = $(this);
|
|
modal.find("#edit_client_save").prop("disabled", true);
|
|
const button = wgModalTrigger(event);
|
|
const client_id = button.data('clientid');
|
|
if (!client_id) return;
|
|
|
|
// IP Allocation tag input
|
|
modal.find("#_client_allocated_ips").tagsInput({
|
|
'width': '100%',
|
|
'height': '75%',
|
|
'interactive': true,
|
|
'defaultText': wgT('js.tags_placeholder'),
|
|
'removeWithBackspace': true,
|
|
'minChars': 0,
|
|
'minInputWidth': '100%',
|
|
'placeholderColor': '#666666'
|
|
});
|
|
|
|
// AllowedIPs tag input
|
|
modal.find("#_client_allowed_ips").tagsInput({
|
|
'width': '100%',
|
|
'height': '75%',
|
|
'interactive': true,
|
|
'defaultText': wgT('js.tags_placeholder'),
|
|
'removeWithBackspace': true,
|
|
'minChars': 0,
|
|
'minInputWidth': '100%',
|
|
'placeholderColor': '#666666'
|
|
});
|
|
|
|
modal.find("#_client_extra_allowed_ips").tagsInput({
|
|
'width': '100%',
|
|
'height': '75%',
|
|
'interactive': true,
|
|
'defaultText': wgT('js.tags_placeholder'),
|
|
'removeWithBackspace' : true,
|
|
'minChars': 0,
|
|
'minInputWidth': '100%',
|
|
'placeholderColor': '#666666'
|
|
})
|
|
|
|
// update client modal data
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'GET',
|
|
url: '{{.basePath}}/api/client/' + client_id,
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
success: function (resp) {
|
|
const client = resp.Client;
|
|
|
|
modal.find(".modal-title").text(wgClientMsg('clients.edit_title_fmt', client.name));
|
|
modal.find("#_client_id").val(client.id);
|
|
modal.find("#_client_telegram_userid").val(client.telegram_userid);
|
|
modal.find("#_client_name").val(client.name);
|
|
modal.find("#_client_email").val(client.email);
|
|
|
|
let preselectedEl
|
|
if (client.subnet_ranges && client.subnet_ranges.length > 0) {
|
|
preselectedEl = client.subnet_ranges[0]
|
|
}
|
|
|
|
updateSubnetRangesList("#_subnet_ranges", preselectedEl);
|
|
|
|
modal.find("#_client_allocated_ips").importTags('');
|
|
client.allocated_ips.forEach(function (obj) {
|
|
modal.find("#_client_allocated_ips").addTag(obj);
|
|
});
|
|
|
|
modal.find("#_client_allowed_ips").importTags('');
|
|
client.allowed_ips.forEach(function (obj) {
|
|
modal.find("#_client_allowed_ips").addTag(obj);
|
|
});
|
|
|
|
modal.find("#_client_extra_allowed_ips").importTags('');
|
|
client.extra_allowed_ips.forEach(function (obj) {
|
|
modal.find("#_client_extra_allowed_ips").addTag(obj);
|
|
});
|
|
|
|
modal.find("#_client_endpoint").val(client.endpoint);
|
|
|
|
modal.find("#_use_server_dns").prop("checked", client.use_server_dns);
|
|
modal.find("#_enabled").prop("checked", client.enabled);
|
|
|
|
modal.find("#_client_public_key").val(client.public_key);
|
|
modal.find("#_client_preshared_key").val(client.preshared_key);
|
|
|
|
modal.find("#_additional_notes").val(client.additional_notes);
|
|
|
|
// handle subnet range select
|
|
$('#_subnet_ranges').on('select2:select', function (e) {
|
|
updateIPAllocationSuggestionExisting();
|
|
});
|
|
|
|
// snapshot once data is fully loaded into controls
|
|
setTimeout(function () {
|
|
wgEditClientSnap = wgEditClientPayload();
|
|
wgEditClientRefreshSaveBtn();
|
|
}, 30);
|
|
},
|
|
error: function (jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
});
|
|
|
|
$("#modal_edit_client").on('shown.bs.modal', function () {
|
|
if (wgEditClientTimer) clearInterval(wgEditClientTimer);
|
|
wgEditClientTimer = setInterval(wgEditClientRefreshSaveBtn, 500);
|
|
});
|
|
$("#modal_edit_client").on('hidden.bs.modal', function () {
|
|
if (wgEditClientTimer) clearInterval(wgEditClientTimer);
|
|
wgEditClientTimer = null;
|
|
wgEditClientSnap = null;
|
|
$("#edit_client_save").prop("disabled", true);
|
|
});
|
|
});
|
|
|
|
// regenerateQRCode function for regenerating QR Code adding/removing some parts of configuration because of compatibility issues with some clients
|
|
function regenerateQRCode() {
|
|
const client_id = $("#qr_client_id").val();
|
|
const QRCodeImg = $("#qr_code");
|
|
const QRCodeA = $("#qr_code_a");
|
|
QRCodeImg.hide();
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'GET',
|
|
url: '{{.basePath}}/api/client/' + client_id,
|
|
data: {
|
|
|
|
},
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
success: function (resp) {
|
|
const client = resp.Client;
|
|
|
|
$(".modal-title").text(wgClientMsg('clients.scan_qr_fmt', client.name));
|
|
QRCodeImg.attr('src', resp.QRCode).show();
|
|
QRCodeA.attr('download', resp.Client.name);
|
|
QRCodeA.attr('href', resp.QRCode).show();
|
|
},
|
|
error: function (jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
}
|
|
|
|
// submitEmailClient function for sending an email with the configuration to the client
|
|
function submitEmailClient() {
|
|
const client_id = $("#e_client_id").val();
|
|
const email = $("#e_client_email").val();
|
|
const data = {"id": client_id, "email": email};
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/email-client',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
data: JSON.stringify(data),
|
|
success: function(resp) {
|
|
$("#modal_email_client").modal('hide');
|
|
toastr.success(wgT('clients.sent_email_ok'));
|
|
},
|
|
error: function(jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
}
|
|
|
|
// submitTelegramClient function for sending a telegram message with the configuration to the client
|
|
function submitTelegramClient() {
|
|
const client_id = $("#tg_client_id").val();
|
|
const userid = $("#tg_client_userid").val();
|
|
const data = {"id": client_id, "userid": userid};
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/send-telegram-client',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
data: JSON.stringify(data),
|
|
success: function(resp) {
|
|
$("#modal_telegram_client").modal('hide');
|
|
toastr.success(wgT('clients.sent_telegram_ok'));
|
|
},
|
|
error: function(jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
}
|
|
|
|
// submitEditClient function for updating an existing client
|
|
// This sends dialogue data to the back-end when user presses "Save"
|
|
// See e.g. routes.go:UpdateClient for where data is processed/verified.
|
|
function submitEditClient() {
|
|
if ($("#edit_client_save").prop("disabled")) {
|
|
return;
|
|
}
|
|
const client_id = $("#_client_id").val();
|
|
const name = $("#_client_name").val();
|
|
const email = $("#_client_email").val();
|
|
const telegram_userid = $("#_client_telegram_userid").val();
|
|
const allocated_ips = $("#_client_allocated_ips").val().split(",");
|
|
const allowed_ips = $("#_client_allowed_ips").val().split(",");
|
|
let use_server_dns = false;
|
|
let extra_allowed_ips = [];
|
|
const public_key = $("#_client_public_key").val();
|
|
const preshared_key = $("#_client_preshared_key").val();
|
|
|
|
if( $("#_client_extra_allowed_ips").val() !== "" ) {
|
|
extra_allowed_ips = $("#_client_extra_allowed_ips").val().split(",");
|
|
}
|
|
|
|
const endpoint = $("#_client_endpoint").val();
|
|
|
|
if ($("#_use_server_dns").is(':checked')){
|
|
use_server_dns = true;
|
|
}
|
|
|
|
let enabled = false;
|
|
|
|
if ($("#_enabled").is(':checked')){
|
|
enabled = true;
|
|
}
|
|
|
|
const additional_notes = $("#_additional_notes").val();
|
|
|
|
const data = {"id": client_id, "name": name, "email": email, "telegram_userid": telegram_userid, "allocated_ips": allocated_ips,
|
|
"allowed_ips": allowed_ips, "extra_allowed_ips": extra_allowed_ips, "endpoint": endpoint,
|
|
"use_server_dns": use_server_dns, "enabled": enabled, "public_key": public_key, "preshared_key": preshared_key, "additional_notes": additional_notes};
|
|
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/update-client',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
data: JSON.stringify(data),
|
|
success: function(resp) {
|
|
$("#modal_edit_client").modal('hide');
|
|
toastr.success(wgT('clients.updated_ok'));
|
|
if (typeof updateApplyConfigVisibility === 'function') {
|
|
updateApplyConfigVisibility();
|
|
}
|
|
// Keep the flow: after edit, enable the global Apply config toolbar (outside the modal).
|
|
// Reload the row so card changes show without writing wg.conf yet.
|
|
populateClient(client_id);
|
|
},
|
|
error: function(jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
}
|
|
|
|
// submitHandler
|
|
function submitHandler(form) {
|
|
const formId = $(form).attr('id');
|
|
if (formId === "frm_edit_client") {
|
|
submitEditClient();
|
|
} else if (formId === "frm_email_client") {
|
|
submitEmailClient();
|
|
} else if (formId === "frm_telegram_client") {
|
|
submitTelegramClient();
|
|
}
|
|
}
|
|
|
|
$("#modal_email_client").on('show.bs.modal', function (event) {
|
|
let modal = $(this);
|
|
const button = wgModalTrigger(event);
|
|
const client_id = button.data('clientid');
|
|
if (!client_id) return;
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'GET',
|
|
url: '{{.basePath}}/api/client/' + client_id,
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
success: function (resp) {
|
|
const client = resp.Client;
|
|
|
|
modal.find(".modal-title").text(wgClientMsg('clients.send_config_fmt', client.name));
|
|
modal.find("#e_client_id").val(client.id);
|
|
modal.find("#e_client_email").val(client.email);
|
|
},
|
|
error: function (jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
});
|
|
|
|
$("#modal_qr_client").on('show.bs.modal', function (event) {
|
|
let modal = $(this);
|
|
const button = wgModalTrigger(event);
|
|
const client_id = button.data('clientid');
|
|
if (!client_id) return;
|
|
|
|
modal.find("#qr_client_id").val(client_id);
|
|
regenerateQRCode();
|
|
});
|
|
|
|
$("#modal_telegram_client").on('show.bs.modal', function (event) {
|
|
let modal = $(this);
|
|
const button = wgModalTrigger(event);
|
|
const client_id = button.data('clientid');
|
|
if (!client_id) return;
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'GET',
|
|
url: '{{.basePath}}/api/client/' + client_id,
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
success: function (resp) {
|
|
const client = resp.Client;
|
|
|
|
modal.find(".modal-title").text(wgClientMsg('clients.send_config_fmt', client.name));
|
|
modal.find("#tg_client_id").val(client.id);
|
|
modal.find("#tg_client_userid").val(client.telegram_userid);
|
|
},
|
|
error: function (jqXHR, exception) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
});
|
|
|
|
$(document).ready(function () {
|
|
$.validator.setDefaults({
|
|
submitHandler: function (form) {
|
|
submitHandler(form);
|
|
}
|
|
});
|
|
// Edit client form validation
|
|
$("#frm_edit_client").validate({
|
|
rules: {
|
|
client_name: {
|
|
required: true,
|
|
},
|
|
},
|
|
messages: {
|
|
client_name: {
|
|
required: wgT('clients.validator_name')
|
|
},
|
|
},
|
|
errorElement: 'span',
|
|
errorPlacement: function (error, element) {
|
|
error.addClass('invalid-feedback');
|
|
element.closest('.form-group').append(error);
|
|
},
|
|
highlight: function (element, errorClass, validClass) {
|
|
$(element).addClass('is-invalid');
|
|
},
|
|
unhighlight: function (element, errorClass, validClass) {
|
|
$(element).removeClass('is-invalid');
|
|
}
|
|
});
|
|
// Email client form validation
|
|
$("#frm_email_client").validate({
|
|
rules: {
|
|
e_client_email: {
|
|
required: true,
|
|
email: true,
|
|
},
|
|
},
|
|
messages: {
|
|
e_client_email: {
|
|
required: wgT('clients.validator_email')
|
|
},
|
|
},
|
|
errorElement: 'span',
|
|
errorPlacement: function (error, element) {
|
|
error.addClass('invalid-feedback');
|
|
element.closest('.form-group').append(error);
|
|
},
|
|
highlight: function (element, errorClass, validClass) {
|
|
$(element).addClass('is-invalid');
|
|
},
|
|
unhighlight: function (element, errorClass, validClass) {
|
|
$(element).removeClass('is-invalid');
|
|
}
|
|
});
|
|
// Telegram client form validation
|
|
$("#frm_telegram_client").validate({
|
|
rules: {
|
|
tg_client_userid: {
|
|
required: true,
|
|
number: true,
|
|
},
|
|
},
|
|
messages: {
|
|
tg_client_userid: {
|
|
required: wgT('clients.validator_tg_req'),
|
|
number: wgT('clients.validator_tg_num')
|
|
},
|
|
},
|
|
errorElement: 'span',
|
|
errorPlacement: function (error, element) {
|
|
error.addClass('invalid-feedback');
|
|
element.closest('.form-group').append(error);
|
|
},
|
|
highlight: function (element, errorClass, validClass) {
|
|
$(element).addClass('is-invalid');
|
|
},
|
|
unhighlight: function (element, errorClass, validClass) {
|
|
$(element).removeClass('is-invalid');
|
|
}
|
|
});
|
|
//
|
|
});
|
|
</script>
|
|
{{end}}
|