wireguard-ui/templates/clients.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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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}}