wireguard-ui/templates/base.html

924 lines
42 KiB
HTML

{{define "base.html"}}
<!DOCTYPE html>
<html lang="{{if eq .UILang "en"}}en{{else}}es{{end}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{template "title" .}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="{{.basePath}}/favicon">
<link rel="stylesheet" href="{{.basePath}}/static/plugins/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="{{.basePath}}/static/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
<link rel="stylesheet" href="{{.basePath}}/static/plugins/select2/css/select2.min.css">
<link rel="stylesheet" href="{{.basePath}}/static/plugins/toastr/toastr.min.css">
<link rel="stylesheet" href="{{.basePath}}/static/plugins/jquery-tags-input/dist/jquery.tagsinput.min.css">
<link rel="stylesheet" href="{{.basePath}}/static/dist/css/adminlte.min.css">
<link rel="stylesheet" href="{{.basePath}}/static/custom/css/wgshell.css">
{{template "top_css" .}}
<script>window.APP_BASE_PATH = "{{.basePath}}";</script>
</head>
<body class="wg-body">
<script>
(function(){
try {
window.__WG_UI_THEME_RAW = '{{if .globalSettings}}{{if eq .globalSettings.UITheme "light"}}light{{else if eq .globalSettings.UITheme "auto"}}auto{{else}}dark{{end}}{{else}}dark{{end}}';
window.wgNormalizeShellThemeRaw = function (t) {
var s = String(t === null || t === undefined ? '' : t).toLowerCase().trim();
if (s === 'light' || s === 'auto') return s;
return 'dark';
};
window.wgSyncShellThemeClass = function () {
var r = window.wgNormalizeShellThemeRaw(window.__WG_UI_THEME_RAW);
window.__WG_UI_THEME_RAW = r;
var light = (r === 'light') || (r === 'auto' && typeof window.matchMedia !== 'undefined' && window.matchMedia('(prefers-color-scheme: light)').matches);
document.body.classList.toggle('wg-theme-light', light);
};
window.wgSetShellThemeRaw = function (t) {
window.__WG_UI_THEME_RAW = window.wgNormalizeShellThemeRaw(t);
window.wgSyncShellThemeClass();
};
window.wgSyncShellThemeClass();
if (!window.__wgPreferColorMqBound && typeof window.matchMedia !== 'undefined') {
try {
var mq = window.matchMedia('(prefers-color-scheme: light)');
var onPc = function () {
if (window.__WG_UI_THEME_RAW === 'auto') window.wgSyncShellThemeClass();
};
if (mq.addEventListener) mq.addEventListener('change', onPc);
else if (mq.addListener) mq.addListener(onPc);
window.__wgPreferColorMqBound = true;
} catch (e2) {}
}
} catch (e1) {}
})();
</script>
<div class="shell">
<!-- Sidebar -->
<aside class="sb">
<div class="sb-top">
<a href="{{.basePath}}/" class="logo">
<div class="logo-ico"><img src="{{.basePath}}/static/wireguard.svg" alt="WireGuard" width="32" height="32"></div>
<div>
<div class="ln">WireGuard</div>
<div class="ls">UI</div>
</div>
</a>
{{if .baseData.CurrentUser}}
<div class="sb-mobile-actions" aria-label="{{tr .UILang "nav.mobile_user_aria"}}">
<a href="{{.basePath}}/profile" class="ma-btn" title="{{tr .UILang "nav.profile"}}">
<i class="fas fa-user"></i>
</a>
<a href="{{.basePath}}/logout" class="ma-btn ma-btn-logout" title="{{tr .UILang "nav.logout_sidebar"}}">
<i class="fas fa-sign-out-alt"></i><span>{{tr .UILang "nav.sign_out_mobile"}}</span>
</a>
</div>
{{end}}
</div>
<div class="sb-mid">
<div class="nl">{{tr .UILang "nav.section.main"}}</div>
<a href="{{.basePath}}/dashboard" class="ni ni-row {{if eq .baseData.Active "dashboard"}}act{{end}}">
<i class="fas fa-chart-pie" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.dashboard"}}</span>
<span id="wg-nav-dash-badge" class="nb-min" title="{{tr .UILang "nav.badge_tooltip"}}" {{if le .dashboardNavBadge 0}}hidden{{end}}>{{if gt .dashboardNavBadge 0}}{{.dashboardNavBadge}}{{end}}</span>
</a>
<a href="{{.basePath}}/" class="ni {{if eq .baseData.Active "clients"}}act{{end}}">
<i class="fas fa-user-secret" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.clients"}}</span>
</a>
{{if .baseData.Admin}}
<a href="{{.basePath}}/wg-server" class="ni {{if eq .baseData.Active "wg-server"}}act{{end}}">
<i class="fas fa-server" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.server"}}</span>
</a>
{{end}}
<div class="nl">{{tr .UILang "nav.section.monitor"}}</div>
<a href="{{.basePath}}/traffic" class="ni {{if eq .baseData.Active "traffic"}}act{{end}}">
<i class="fas fa-chart-line" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.traffic"}}</span>
</a>
<a id="wg-nav-link-logs" href="{{.basePath}}/logs" class="ni {{if eq .baseData.Active "logs"}}act{{end}}" {{if not (and .globalSettings .globalSettings.RealtimeStatsEnabled)}}hidden{{end}}>
<i class="fas fa-stream" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.logs"}}</span>
</a>
{{if .baseData.Admin}}
<a href="{{.basePath}}/global-settings" class="ni {{if eq .baseData.Active "global-settings"}}act{{end}}">
<i class="fas fa-cog" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.settings"}}</span>
</a>
{{end}}
{{if and .baseData.Admin (not .loginDisabled)}}
<div class="nl">{{tr .UILang "nav.section.admin"}}</div>
<a href="{{.basePath}}/users-settings" class="ni {{if eq .baseData.Active "users-settings"}}act{{end}}">
<i class="fas fa-users-cog" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.users"}}</span>
</a>
{{end}}
<div class="nl">{{tr .UILang "nav.section.other"}}</div>
<a href="{{.basePath}}/wake_on_lan_hosts" class="ni {{if eq .baseData.Active "wake_on_lan_hosts"}}act{{end}}">
<i class="fas fa-power-off" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.wol"}}</span>
</a>
<a href="{{.basePath}}/about" class="ni {{if eq .baseData.Active "about"}}act{{end}}">
<i class="fas fa-info-circle" style="width:15px;text-align:center"></i>
<span>{{tr .UILang "nav.about"}}</span>
</a>
</div>
<div class="sf">
<a href="{{.basePath}}/profile" class="uchip">
<div class="av"><i class="fas fa-user" style="font-size:12px;color:#fff"></i></div>
<div>
<div class="un">{{if .baseData.CurrentUser}}{{.baseData.CurrentUser}}{{else}}{{tr .UILang "role.guest"}}{{end}}</div>
<div class="ur">{{if .baseData.Admin}}{{tr .UILang "role.admin"}}{{else}}{{tr .UILang "role.user"}}{{end}}</div>
</div>
</a>
{{if .baseData.CurrentUser}}
<div style="padding:8px">
<a class="wg-btn bg" href="{{.basePath}}/logout" style="width:100%;justify-content:center"><i class="fas fa-sign-out-alt"></i> {{tr .UILang "nav.logout_sidebar"}}</a>
</div>
{{end}}
</div>
</aside>
<!-- Main -->
<div class="main">
<header class="topbar">
<div>
<div class="pt">{{template "page_title" .}}</div>
<div class="ps" id="wg-page-desc">{{if .page_subtitle}}{{.page_subtitle}}{{end}}</div>
</div>
<div class="tba">
<span class="sdot" title="WireGuard UI"></span>
<button type="button" class="wg-btn bg" onclick="location.reload()" title="{{tr .UILang "top.reload"}}"><i class="fas fa-sync-alt"></i> {{tr .UILang "top.sync"}}</button>
<form class="form-inline ml-2" id="search-form" style="display:none">
<div class="input-group input-group-sm">
<input class="form-control" placeholder="{{tr .UILang "top.search_placeholder"}}" aria-label="{{tr .UILang "top.search_placeholder"}}" id="search-input">
<div class="input-group-append">
<button class="btn btn-default" type="submit" disabled><i class="fas fa-search"></i></button>
</div>
</div>
<select name="status-selector" id="status-selector" class="custom-select form-control-sm ml-2" style="width:auto;">
<option value="All">{{tr .UILang "top.status.all"}}</option>
<option value="Enabled">{{tr .UILang "top.status.enabled"}}</option>
<option value="Disabled">{{tr .UILang "top.status.disabled"}}</option>
<option value="Connected">{{tr .UILang "top.status.connected"}}</option>
<option value="Disconnected">{{tr .UILang "top.status.disconnected"}}</option>
</select>
</form>
{{if eq .baseData.Active "dashboard"}}
<button type="button" class="wg-btn bp" data-toggle="modal" data-target="#modal_new_client"><i class="fas fa-plus"></i> {{tr .UILang "top.new_client"}}</button>
{{end}}
<button id="apply-config-button" style="display: none;" type="button" class="wg-btn brd">
<i class="fas fa-check"></i> {{tr .UILang "top.apply_config"}}
</button>
</div>
</header>
<div class="cv wg-cv">
{{template "page_content" .}}
</div>
</div>
</div>
<div class="modal fade" id="modal_new_client">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{tr .UILang "modal.new_client.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_new_client" id="frm_new_client">
<div class="modal-body">
<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
<i class="fas fa-info-circle" data-toggle="tooltip"
data-original-title="Specify a list of addresses that will get routed to the
server. These addresses will be included in 'AllowedIPs' of client config">
</i>
</label>
<input type="text" data-role="tagsinput" class="form-control" id="client_allowed_ips"
value="{{ StringsJoin .client_defaults.AllowedIps "," }}">
</div>
<div class="form-group">
<label for="client_extra_allowed_ips" class="control-label">Extra Allowed IPs
<i class="fas fa-info-circle" data-toggle="tooltip"
data-original-title="Specify a list of addresses that will get routed to the
client. These addresses will be included in 'AllowedIPs' of WG server config">
</i>
</label>
<input type="text" data-role="tagsinput" class="form-control" id="client_extra_allowed_ips" value="{{ StringsJoin .client_defaults.ExtraAllowedIps "," }}">
</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" {{ if .client_defaults.UseServerDNS }}checked{{ end }}>
<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" {{ if .client_defaults.EnableAfterCreation }}checked{{ end }}>
<label for="enabled">
Enable after creation
</label>
</div>
</div>
<details>
<summary><strong>Public and Preshared Keys</strong>
<i class="fas fa-info-circle" data-toggle="tooltip"
data-original-title="If you don't want to let the server generate and store the
client's private key, you can manually specify its public and preshared key here
. Note: QR code will not be generated">
</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" placeholder="Autogenerated" 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" placeholder="Autogenerated - enter &quot;-&quot; to skip generation">
</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">Cancel</button>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="modal_apply_config">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{tr .UILang "modal.apply.title"}}</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">
<p>{{tr .UILang "modal.apply.body1"}}</p>
<p class="mb-0 text-muted small">{{tr .UILang "modal.apply.body2"}}</p>
</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="button" class="btn btn-danger" id="apply_config_confirm">{{tr .UILang "modal.apply.confirm"}}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="modal_apply_kernel_manual" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{tr .UILang "modal.kernel.title"}}</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">
<p>{{tr .UILang "modal.kernel.p1"}}</p>
<p class="mb-0">{{tr .UILang "modal.kernel.p2"}}</p>
<details class="mt-3">
<summary style="cursor:pointer;">{{tr .UILang "modal.kernel.details"}}</summary>
<pre id="apply-kernel-detail" style="margin-top:8px;white-space:pre-wrap;word-break:break-word;background:#1f1f1f;color:#ddd;padding:10px;border-radius:6px;border:1px solid #333;font-size:12px;">{{tr .UILang "modal.kernel.none"}}</pre>
</details>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" data-dismiss="modal">{{tr .UILang "modal.kernel.close_btn"}}</button>
<a class="btn btn-primary" href="{{.basePath}}/wg-server">{{tr .UILang "modal.kernel.goto_server"}}</a>
</div>
</div>
</div>
</div>
<script src="{{.basePath}}/static/plugins/jquery/jquery.min.js"></script>
<script src="{{.basePath}}/static/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="{{.basePath}}/static/plugins/select2/js/select2.full.min.js"></script>
<script src="{{.basePath}}/static/plugins/jquery-validation/jquery.validate.min.js"></script>
<script src="{{.basePath}}/static/plugins/toastr/toastr.min.js"></script>
<script src="{{.basePath}}/static/plugins/jquery-tags-input/dist/jquery.tagsinput.min.js"></script>
<script src="{{.basePath}}/static/dist/js/adminlte.min.js"></script>
<script>window.WG_T={{.WGMsgJSON}};window.wgT=function(k){try{var m=window.WG_T;return(m&&typeof m[k]==='string')?m[k]:k;}catch(e){return k;}};</script>
<script src="{{.basePath}}/static/custom/js/helper.js"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
/** Modals inside .shell sat under the body modal-backdrop; focus felt “stuck”. */
$(document).on('show.bs.modal', '.modal', function () {
var $m = $(this);
if ($m.closest('.shell').length && !$m.parent().is('body')) {
$m.appendTo(document.body);
}
});
});
const wgSyncconfAfterApplyEnabled = {{if .syncconfAfterApplyEnabled}}true{{else}}false{{end}};
$(document).ready(function () {
addGlobalStyle(`
.toast-top-right-fix { top: 67px; right: 12px; }
`, 'toastrToastStyleFix')
toastr.options.closeDuration = 100;
toastr.options.positionClass = 'toast-top-right-fix';
wireUnifiedApplyButton();
updateApplyConfigVisibility()
});
// Mobile nav: keep horizontal scroll focused on current tab across full page reloads.
(function () {
var NAV_SCROLL_KEY = 'wg_mobile_nav_scroll_left_v1';
function isMobileNav() {
return window.matchMedia && window.matchMedia('(max-width: 900px)').matches;
}
function navEl() {
return document.querySelector('.sb-mid');
}
function saveNavScroll(v) {
try { localStorage.setItem(NAV_SCROLL_KEY, String(Math.max(0, Math.round(v)))); } catch (e) {}
}
function restoreNavScroll() {
if (!isMobileNav()) return;
var n = navEl();
if (!n) return;
var raw = null;
try { raw = localStorage.getItem(NAV_SCROLL_KEY); } catch (e) {}
var x = Number(raw);
if (isFinite(x) && x >= 0) {
n.scrollLeft = x;
}
var active = n.querySelector('.ni.act');
if (active && active.scrollIntoView) {
// Ensure active tab is visible without snapping hard to an edge.
active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
}
document.addEventListener('DOMContentLoaded', function () {
var n = navEl();
if (!n) return;
restoreNavScroll();
n.addEventListener('scroll', function () {
if (!isMobileNav()) return;
saveNavScroll(n.scrollLeft);
}, { passive: true });
n.querySelectorAll('a.ni').forEach(function (a) {
a.addEventListener('click', function () {
if (!isMobileNav()) return;
var target = this;
var desired = target.offsetLeft - (n.clientWidth * 0.3);
saveNavScroll(desired);
});
});
});
window.addEventListener('beforeunload', function () {
var n = navEl();
if (!n || !isMobileNav()) return;
saveNavScroll(n.scrollLeft);
});
})();
var WG_PENDING_KIND_KEY = 'wg_pending_global_apply_kind';
/** POST staged global settings from localStorage. Resolves true when a POST ran (nav/logs may need refresh after WG apply). opts.skipSuccessToast: suppress duplicate toasts */
function wgPostPendingGlobalSettingsIfNeeded(opts) {
opts = opts || {};
var raw = null;
try {
raw = localStorage.getItem('wg_pending_global_settings');
} catch (e) {}
if (!raw || !raw.length) {
return $.Deferred(function (def) { def.resolve(false); }).promise();
}
var payload = null;
try {
payload = JSON.parse(raw);
} catch (e2) {
try { localStorage.removeItem('wg_pending_global_settings'); } catch (e3) {}
return $.Deferred(function (def) {
def.reject({
status: 400,
responseJSON: { message: wgT('js.pending_global_invalid') }
});
}).promise();
}
var dfd = $.Deferred();
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/global-settings',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(payload),
success: function (resp) {
try { localStorage.removeItem('wg_pending_global_settings'); } catch (e4) {}
try { localStorage.removeItem(WG_PENDING_KIND_KEY); } catch (eKm) {}
if (!opts.skipSuccessToast) {
toastr.success((resp && resp.message) || wgT('js.global_saved_ok'));
}
if (typeof window.wgRefreshGlobalSettingsBaseline === 'function') {
try { window.wgRefreshGlobalSettingsBaseline(); } catch (e5) {}
}
dfd.resolve(true);
},
error: function (xhr) {
dfd.reject(xhr);
}
});
return dfd.promise();
}
/** Session/appearance only: POST /global-settings without modal or wg.conf dump when no client/hash deltas are pending. */
function tryApplyUIPrefsFromConfigOnly() {
var pendingKind = '';
try { pendingKind = String(localStorage.getItem(WG_PENDING_KIND_KEY) || ''); } catch (eK) {}
var hasPen = false;
try { hasPen = !!localStorage.getItem('wg_pending_global_settings'); } catch (eP) {}
if (!hasPen || pendingKind !== 'ui_only') {
return false;
}
$.getJSON('{{.basePath}}/test-hash', function (hd) {
var hashDirty = !!(hd && hd.status);
if (hashDirty) {
$('#modal_apply_config').modal('show');
return;
}
wgPostPendingGlobalSettingsIfNeeded({ skipSuccessToast: true }).done(function (posted) {
toastr.success(wgT('js.session_prefs_ok'));
if (posted) {
wgRefreshNavbarFromHints(function () {
updateApplyConfigVisibility();
if (typeof window.wgRefreshGlobalSettingsBaseline === 'function') {
try { window.wgRefreshGlobalSettingsBaseline(); } catch (eB0) {}
}
});
return;
}
updateApplyConfigVisibility();
}).fail(function (xhr) {
var msg = wgT('js.could_not_apply_prefs');
try {
if (xhr.responseJSON && xhr.responseJSON.message) msg = xhr.responseJSON.message;
} catch (eMsg) {}
toastr.error(msg);
});
}).fail(function () {
$('#modal_apply_config').modal('show');
});
return true;
}
function wireUnifiedApplyButton() {
$('#apply-config-button').off('click.wgUnified').on('click.wgUnified', function (ev) {
ev.preventDefault();
ev.stopImmediatePropagation();
if (tryApplyUIPrefsFromConfigOnly()) {
return;
}
$('#modal_apply_config').modal('show');
});
}
function maybeShowKernelReloadHelp(message) {
var rawMsg = String(message || '');
var msg = rawMsg.toLowerCase();
if (msg.indexOf('kernel reload failed') >= 0) {
var detailEl = document.getElementById('apply-kernel-detail');
if (detailEl) {
detailEl.textContent = rawMsg || wgT('js.kernel_detail_fallback');
}
$("#modal_apply_kernel_manual").modal('show');
return true;
}
return false;
}
function showManualKernelReloadHelp(message) {
var detailEl = document.getElementById('apply-kernel-detail');
if (detailEl) {
detailEl.textContent = String(message || wgT('js.manual_kernel_synconf_off'));
}
$("#modal_apply_kernel_manual").modal('show');
}
function addGlobalStyle(css, id) {
if (!document.querySelector('#' + id)) {
let style = document.createElement('style')
style.type = 'text/css'
style.id = id
style.innerHTML = css
document.head.appendChild(style)
}
}
/** Dark/light/auto theme (prefers-color-scheme) + documentElement lang attribute. */
function wgApplyShellPresentation(theme, lang) {
if (typeof window.wgSetShellThemeRaw === 'function') {
window.wgSetShellThemeRaw(theme);
} else {
var th = (theme != null && String(theme).toLowerCase() === 'light') ? 'light' : 'dark';
document.body.classList.toggle('wg-theme-light', th === 'light');
}
var lng = lang != null ? String(lang).toLowerCase().trim() : 'es';
if (lng !== 'en') {
lng = 'es';
}
document.documentElement.setAttribute('lang', lng);
}
window.wgApplyShellPresentation = wgApplyShellPresentation;
function wgRefreshNavbarFromHints(done) {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/ui-nav-hints',
dataType: 'json'
}).done(function (data) {
wgApplyShellPresentation(data && data.ui_theme, data && data.ui_language);
var showLogs = !!(data && data.show_logs_nav);
var bd = Number((data && data.dashboard_nav_badge !== undefined && data.dashboard_nav_badge !== null) ? data.dashboard_nav_badge : 0);
var $logs = $('#wg-nav-link-logs');
if ($logs.length) {
$logs[0].hidden = !showLogs;
}
var $bd = $('#wg-nav-dash-badge');
if ($bd.length) {
$bd.prop('hidden', bd <= 0);
$bd.text(bd > 0 ? String(bd) : '');
}
}).always(function () {
if (typeof done === 'function') {
done();
}
});
}
function updateApplyConfigVisibility() {
var hasPendingGlobalSettings = false;
try {
hasPendingGlobalSettings = !!localStorage.getItem('wg_pending_global_settings');
} catch (e) {}
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/test-hash',
dataType: 'json',
contentType: "application/json",
success: function(data) {
if (data.status || hasPendingGlobalSettings) {
$("#apply-config-button").show()
} else {
$("#apply-config-button").hide()
}
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
function populateClient(client_id) {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/client/' + client_id,
dataType: 'json',
contentType: "application/json",
success: function (resp) {
$.getJSON('{{.basePath}}/api/wg-peer-stats')
.done(function (traffic) {
renderClientList([resp], traffic || {});
})
.fail(function () {
renderClientList([resp], {});
});
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
function submitNewClient() {
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(",");
const endpoint = $("#client_endpoint").val();
let use_server_dns = false;
let extra_allowed_ips = [];
if ($("#client_extra_allowed_ips").val() !== "") {
extra_allowed_ips = $("#client_extra_allowed_ips").val().split(",");
}
if ($("#use_server_dns").is(':checked')){
use_server_dns = true;
}
let enabled = false;
if ($("#enabled").is(':checked')){
enabled = true;
}
const public_key = $("#client_public_key").val();
const preshared_key = $("#client_preshared_key").val();
const additional_notes = $("#additional_notes").val();
const data = {"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}}/new-client',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function(resp) {
$("#modal_new_client").modal('hide');
toastr.success(wgT('js.client_created_success'));
if (window.location.pathname === "{{.basePath}}/") {
populateClient(resp.id);
}
updateApplyConfigVisibility()
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
function updateIPAllocationSuggestion(forceDefault = false) {
let subnetRange = $("#subnet_ranges").select2('val');
if (forceDefault || !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']);
}
});
}
</script>
<script>
$(".select2").select2()
$("#client_allocated_ips").tagsInput({
'width': '100%',
'height': '75%',
'interactive': true,
'defaultText': wgT('js.tags_placeholder'),
'removeWithBackspace': true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
});
$("#client_allowed_ips").tagsInput({
'width': '100%',
'height': '75%',
'interactive': true,
'defaultText': wgT('js.tags_placeholder'),
'removeWithBackspace': true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
});
$("#client_extra_allowed_ips").tagsInput({
'width': '100%',
'height': '75%',
'interactive': true,
'defaultText': wgT('js.tags_placeholder'),
'removeWithBackspace': true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
});
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function () {
submitNewClient();
}
});
$("#frm_new_client").validate({
rules: {
client_name: {
required: true,
},
},
messages: {
client_name: {
required: wgT('js.validator_name_required')
},
},
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');
}
});
});
$(document).ready(function () {
$("#modal_new_client").on('shown.bs.modal', function (e) {
$("#client_name").val("");
$("#client_email").val("");
$("#client_public_key").val("");
$("#client_preshared_key").val("");
$("#client_allocated_ips").importTags('');
$("#client_extra_allowed_ips").importTags('');
$("#client_endpoint").val('');
$("#client_telegram_userid").val('');
$("#additional_notes").val('');
updateSubnetRangesList("#subnet_ranges");
updateIPAllocationSuggestion(true);
});
});
$('#subnet_ranges').on('select2:select', function (e) {
updateIPAllocationSuggestion();
});
$(document).ready(function () {
$("#apply_config_confirm").click(function () {
var confirmBtn = $("#apply_config_confirm");
confirmBtn.prop("disabled", true);
var pendingKindSnap = '';
var hasPendingGlobalPayload = false;
try { pendingKindSnap = String(localStorage.getItem(WG_PENDING_KIND_KEY) || ''); } catch (eS) {}
try { hasPendingGlobalPayload = !!localStorage.getItem('wg_pending_global_settings'); } catch (ePen) {}
/** ui_only (session/appearance cards): never restart WG. wg_card / both / client-only: restart only if tunnel is actually up (GET tunnel-status). */
function resolveApplyPayloadFromTunnel(tunnelResp) {
if (hasPendingGlobalPayload && pendingKindSnap === 'ui_only') {
return { restart_wireguard: false };
}
var tunnelUp = !!(tunnelResp && tunnelResp.tunnel_running);
return { restart_wireguard: tunnelUp };
}
var applyPayload;
var applyWgConfig = function (reloadAfterApplyForNav) {
var skipKernelRestart = (applyPayload.restart_wireguard === false);
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/api/apply-wg-config',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(applyPayload),
success: function(data) {
$("#modal_apply_config").modal('hide');
toastr.success(wgT('js.apply_config_success'));
var finalizeApplyUI = function () {
updateApplyConfigVisibility();
if (!reloadAfterApplyForNav && skipKernelRestart && !wgSyncconfAfterApplyEnabled) {
showManualKernelReloadHelp(wgT('js.apply_kernel_hint'));
}
};
if (reloadAfterApplyForNav) {
wgRefreshNavbarFromHints(finalizeApplyUI);
return;
}
finalizeApplyUI();
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
maybeShowKernelReloadHelp(responseJson['message']);
toastr.error(responseJson['message']);
},
complete: function () {
confirmBtn.prop("disabled", false);
}
});
};
function runStagedThenApply() {
var staged = wgPostPendingGlobalSettingsIfNeeded({});
staged.done(function (postedGlobalFromServer) {
applyWgConfig(!!postedGlobalFromServer);
}).fail(function (xhr) {
var msg = wgT('js.global_post_fail');
try {
if (xhr.responseJSON && xhr.responseJSON.message) {
msg = xhr.responseJSON.message;
} else if (xhr.responseText) {
var j = JSON.parse(xhr.responseText);
if (j && j.message) msg = j.message;
}
} catch (e6) {}
toastr.error(msg);
confirmBtn.prop("disabled", false);
});
}
$.getJSON('{{.basePath}}/api/wireguard/tunnel-status')
.done(function (ts) {
applyPayload = resolveApplyPayloadFromTunnel(ts);
runStagedThenApply();
})
.fail(function () {
applyPayload = (hasPendingGlobalPayload && pendingKindSnap === 'ui_only')
? { restart_wireguard: false }
: {};
runStagedThenApply();
});
});
});
</script>
{{template "bottom_js" .}}
</body>
</html>
{{end}}