455 lines
24 KiB
HTML
455 lines
24 KiB
HTML
{{define "title"}}{{ tr .UILang "settings.title" }}{{end}}
|
|
|
|
{{define "top_css"}}
|
|
<style>
|
|
.wg-set-wrap{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
@media(max-width:1024px){.wg-set-wrap{grid-template-columns:1fr}}
|
|
.wg-set-card{background:#1e1e1e;border:1px solid rgba(255,255,255,.06);border-radius:16px;overflow:visible}
|
|
.wg-set-head{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.06)}
|
|
.wg-set-head .t{font-size:10px;letter-spacing:.4px;text-transform:uppercase;color:#9e9e9e;font-weight:800}
|
|
.wg-set-body{padding:10px 12px}
|
|
.wg-set-row{padding:9px 0;border-bottom:1px solid rgba(255,255,255,.06)}
|
|
.wg-set-row:last-child{border-bottom:0}
|
|
.wg-set-row label{display:block;font-size:11px;color:#d7d7d7;margin:0 0 5px;font-weight:700}
|
|
.wg-set-hint{font-size:9px;color:#757575;margin:0 0 7px}
|
|
.wg-set-input,.wg-set-body .bootstrap-tagsinput{background:#252525!important;border:1px solid rgba(255,255,255,.08)!important;color:#eee!important;border-radius:10px!important}
|
|
.wg-set-input{height:34px;padding:7px 9px;width:100%;font-size:12px}
|
|
.wg-set-body .bootstrap-tagsinput{width:100%;min-height:34px;padding:5px 7px}
|
|
.wg-set-body .bootstrap-tagsinput input{background:transparent;color:#eaeaea}
|
|
.wg-set-foot{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:16px;padding-top:4px;}
|
|
.wg-btn-alt{background:#252525;border:1px solid rgba(255,255,255,.08);color:#eaeaea;border-radius:10px;padding:7px 10px;font-size:11px;font-weight:700}
|
|
.wg-help dl{margin:0}
|
|
.wg-help dt{font-size:11px;color:#e0e0e0;margin-top:10px}
|
|
.wg-help dd{font-size:11px;color:#a8a8a8;line-height:1.45;margin:4px 0 0}
|
|
.wg-help code{color:#80cbc4}
|
|
.wg-set-col{display:grid;grid-template-columns:1fr;gap:12px}
|
|
.wg-inline-end{display:flex;justify-content:flex-end}
|
|
.wg-switch{appearance:none;width:42px;height:24px;background:#3a3a3a;border-radius:999px;border:1px solid rgba(255,255,255,.1);position:relative;cursor:pointer;outline:none}
|
|
.wg-switch::after{content:'';position:absolute;left:3px;top:3px;width:16px;height:16px;background:#d0d0d0;border-radius:50%;transition:.18s}
|
|
.wg-switch:checked{background:rgba(239,83,80,.38);border-color:rgba(239,83,80,.52)}
|
|
.wg-switch:checked::after{left:22px;background:#EF5350}
|
|
</style>
|
|
{{end}}
|
|
|
|
{{define "username"}}
|
|
{{ .username }}
|
|
{{end}}
|
|
|
|
{{define "page_title"}}{{ tr .UILang "settings.page" }}{{end}}
|
|
|
|
{{define "page_content"}}
|
|
<section class="content wg-shell-page">
|
|
<div class="wg-set-wrap">
|
|
<form role="form" id="frm_global_settings" name="frm_global_settings" class="wg-set-card">
|
|
<div class="wg-set-head"><div class="t">{{ tr .UILang "settings.card_head_wireguard" }}</div></div>
|
|
<div class="wg-set-body">
|
|
<div class="wg-set-row form-group">
|
|
<label for="endpoint_address">Endpoint Address</label>
|
|
<p class="wg-set-hint">{{ tr .UILang "settings.endpoint_hint" }}</p>
|
|
<div class="input-group input-group">
|
|
<input type="text" class="form-control wg-set-input" id="endpoint_address" name="endpoint_address" placeholder="Endpoint Address" value="{{ .globalSettings.EndpointAddress }}">
|
|
<span class="input-group-append">
|
|
<button type="button" class="wg-btn-alt" data-toggle="modal" data-target="#modal_endpoint_address_suggestion"><i class="nav-icon fas fa-magic"></i> {{ tr .UILang "settings.btn_suggest" }}</button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wg-set-row form-group">
|
|
<label for="dns_servers" class="control-label">DNS Servers</label>
|
|
<p class="wg-set-hint">{{ tr .UILang "settings.dns_hint" }}</p>
|
|
<input type="text" data-role="tagsinput" class="form-control" id="dns_servers" value="">
|
|
</div>
|
|
|
|
<div class="wg-set-row form-group">
|
|
<label for="mtu">MTU</label>
|
|
<p class="wg-set-hint">{{ tr .UILang "settings.mtu_hint" }}</p>
|
|
<input type="text" class="form-control wg-set-input" id="mtu" name="mtu" placeholder="MTU" value="{{if .globalSettings.MTU}}{{ .globalSettings.MTU }}{{end}}">
|
|
</div>
|
|
|
|
<div class="wg-set-row form-group">
|
|
<label for="persistent_keepalive">Persistent Keepalive</label>
|
|
<p class="wg-set-hint">{{ tr .UILang "settings.keepalive_hint" }}</p>
|
|
<input type="text" class="form-control wg-set-input" id="persistent_keepalive" name="persistent_keepalive" placeholder="Persistent Keepalive" value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}">
|
|
</div>
|
|
|
|
<div class="wg-set-row form-group">
|
|
<label for="firewall_mark">Firewall Mark</label>
|
|
<input type="text" class="form-control wg-set-input" id="firewall_mark" name="firewall_mark" placeholder="Firewall Mark" value="{{ .globalSettings.FirewallMark }}">
|
|
</div>
|
|
|
|
<div class="wg-set-row form-group">
|
|
<label for="Table">Table</label>
|
|
<input type="text" class="form-control wg-set-input" id="table" name="table" placeholder="auto" value="{{ .globalSettings.Table }}">
|
|
</div>
|
|
|
|
<div class="wg-set-row form-group">
|
|
<label for="config_file_path">Wireguard Config File Path</label>
|
|
<p class="wg-set-hint">{{ tr .UILang "settings.config_path_example" }}</p>
|
|
<input type="text" class="form-control wg-set-input" id="config_file_path" name="config_file_path" placeholder="E.g. /etc/wireguard/wg0.conf" value="{{ .globalSettings.ConfigFilePath }}">
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="wg-set-col">
|
|
<div class="wg-set-card">
|
|
<div class="wg-set-head"><div class="t">{{ tr .UILang "settings.card_session" }}</div></div>
|
|
<div class="wg-set-body">
|
|
<div class="wg-set-row">
|
|
<label for="session_timeout_minutes">{{ tr .UILang "settings.session_timeout_label" }}</label>
|
|
<p class="wg-set-hint">{{ tr .UILang "settings.session_timeout_hint" }}</p>
|
|
<div class="wg-sess-timeout-row" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
<input type="number" min="5" max="1440" class="form-control wg-set-input" id="session_timeout_minutes" name="session_timeout_minutes" inputmode="numeric" style="max-width:120px"
|
|
value="{{if gt .globalSettings.SessionTimeoutMinutes 0}}{{.globalSettings.SessionTimeoutMinutes}}{{else}}30{{end}}"
|
|
aria-describedby="session_timeout_hint">
|
|
<span class="wg-sess-timeout-unit" style="font-size:12px;font-weight:700;color:#9e9e9e;text-transform:none;letter-spacing:0">{{ tr .UILang "settings.minutes_unit" }}</span>
|
|
</div>
|
|
<p id="session_timeout_hint" class="wg-set-hint" style="margin-top:6px;margin-bottom:0">{{ tr .UILang "settings.session_example" }}</p>
|
|
</div>
|
|
<div class="wg-set-row">
|
|
<label for="totp_enabled">{{ tr .UILang "settings.passkeys_label" }}</label>
|
|
<p class="wg-set-hint">{{ tr .UILang "settings.passkeys_hint" }}</p>
|
|
<div class="wg-inline-end"><input type="checkbox" id="totp_enabled" class="wg-switch" {{if .globalSettings.TOTPEnabled}}checked{{end}}></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wg-set-card">
|
|
<div class="wg-set-head"><div class="t">{{ tr .UILang "settings.card_appearance" }}</div></div>
|
|
<div class="wg-set-body">
|
|
<div class="wg-set-row">
|
|
<label for="ui_language">{{ tr .UILang "settings.lang_label" }}</label>
|
|
<select id="ui_language" class="form-control wg-set-input">
|
|
<option value="es" {{if eq .globalSettings.UILanguage "es"}}selected{{end}}>{{ tr .UILang "settings.lang_opt_es" }}</option>
|
|
<option value="en" {{if eq .globalSettings.UILanguage "en"}}selected{{end}}>{{ tr .UILang "settings.lang_opt_en" }}</option>
|
|
</select>
|
|
<p class="wg-set-hint" style="margin-top:7px;margin-bottom:0">{{ tr .UILang "settings.lang_reload_hint" }}</p>
|
|
</div>
|
|
<div class="wg-set-row">
|
|
<label for="ui_theme">{{ tr .UILang "settings.theme_label" }}</label>
|
|
<select id="ui_theme" class="form-control wg-set-input">
|
|
<option value="dark" {{if or (eq .globalSettings.UITheme "") (eq .globalSettings.UITheme "dark")}}selected{{end}}>{{ tr .UILang "settings.theme_dark" }}</option>
|
|
<option value="light" {{if eq .globalSettings.UITheme "light"}}selected{{end}}>{{ tr .UILang "settings.theme_light" }}</option>
|
|
<option value="auto" {{if eq .globalSettings.UITheme "auto"}}selected{{end}}>{{ tr .UILang "settings.theme_auto" }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="wg-set-row">
|
|
<label for="realtime_stats_enabled">{{ tr .UILang "settings.logs_toggle_label" }}</label>
|
|
<p class="wg-set-hint">{{ tr .UILang "settings.logs_hint" }}</p>
|
|
<div class="wg-inline-end"><input type="checkbox" id="realtime_stats_enabled" class="wg-switch" {{if .globalSettings.RealtimeStatsEnabled}}checked{{end}}></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<div class="wg-set-foot">
|
|
<button type="submit" form="frm_global_settings" class="wg-btn bp" id="btn_stage_settings"><i class="fas fa-save"></i> {{ tr .UILang "settings.save_footer" }}</button>
|
|
</div>
|
|
|
|
<div class="modal fade" id="modal_endpoint_address_suggestion">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">{{ tr .UILang "settings.endpoint_modal_title" }}</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">
|
|
<p>{{ tr .UILang "settings.endpoint_modal_intro" }}</p>
|
|
<select id="ip_suggestion" class="select2"
|
|
data-placeholder="{{ tr .UILang "settings.placeholder_select_ip" }}" style="width: 100%;">
|
|
</select>
|
|
</div>
|
|
<div class="modal-footer justify-content-between">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{ tr .UILang "settings.endpoint_modal_close" }}</button>
|
|
<button type="button" class="btn btn-success" id="btn_use_ip" disabled>{{ tr .UILang "settings.endpoint_modal_use" }}</button>
|
|
</div>
|
|
</div>
|
|
<!-- /.modal-content -->
|
|
</div>
|
|
<!-- /.modal-dialog -->
|
|
</div>
|
|
<!-- /.modal -->
|
|
{{end}}
|
|
|
|
{{define "bottom_js"}}
|
|
<script>
|
|
var wgSettingsInitialSnap = "";
|
|
var wgSettingsWGCardInitialSnap = "";
|
|
var wgSettingsUIPrefsInitialSnap = "";
|
|
/** Whether pending changes touch the WireGuard card (vs session/appearance only). */
|
|
var WG_PENDING_KIND_KEY = "wg_pending_global_apply_kind";
|
|
|
|
function snapWGCardStr(data) {
|
|
return JSON.stringify({
|
|
endpoint_address: String(data.endpoint_address || "").trim(),
|
|
dns_servers: Array.isArray(data.dns_servers) ? data.dns_servers : [],
|
|
mtu: String(data.mtu || "").trim(),
|
|
persistent_keepalive: String(data.persistent_keepalive || "").trim(),
|
|
firewall_mark: String(data.firewall_mark || "").trim(),
|
|
table: String(data.table || "").trim(),
|
|
config_file_path: String(data.config_file_path || "").trim(),
|
|
});
|
|
}
|
|
|
|
function snapUIPrefsStr(data) {
|
|
return JSON.stringify({
|
|
session_timeout_minutes: Number(data.session_timeout_minutes || 30),
|
|
totp_enabled: !!data.totp_enabled,
|
|
ui_language: String(data.ui_language || "es"),
|
|
ui_theme: String(data.ui_theme || "dark"),
|
|
realtime_stats_enabled: !!data.realtime_stats_enabled,
|
|
});
|
|
}
|
|
|
|
function normalizeGlobalSettingsData(data) {
|
|
var dns = Array.isArray(data.dns_servers) ? data.dns_servers : [];
|
|
dns = dns.map(function (x) { return String(x || "").trim(); }).filter(Boolean);
|
|
return {
|
|
endpoint_address: String(data.endpoint_address || "").trim(),
|
|
dns_servers: dns,
|
|
mtu: String(data.mtu || "").trim(),
|
|
persistent_keepalive: String(data.persistent_keepalive || "").trim(),
|
|
firewall_mark: String(data.firewall_mark || "").trim(),
|
|
table: String(data.table || "").trim(),
|
|
config_file_path: String(data.config_file_path || "").trim(),
|
|
session_timeout_minutes: Number(data.session_timeout_minutes || 30),
|
|
totp_enabled: !!data.totp_enabled,
|
|
ui_language: String(data.ui_language || "es"),
|
|
ui_theme: String(data.ui_theme || "dark"),
|
|
realtime_stats_enabled: !!data.realtime_stats_enabled
|
|
};
|
|
}
|
|
|
|
function getGlobalSettingsSnap(data) {
|
|
return JSON.stringify(normalizeGlobalSettingsData(data));
|
|
}
|
|
|
|
function hasPendingSettingsChanges() {
|
|
try {
|
|
var raw = localStorage.getItem('wg_pending_global_settings');
|
|
if (!raw) return false;
|
|
var pending = JSON.parse(raw);
|
|
return getGlobalSettingsSnap(pending) !== wgSettingsInitialSnap;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function collectGlobalSettingsData() {
|
|
const endpoint_address = $("#endpoint_address").val();
|
|
const dns_servers = $("#dns_servers").val().split(",");
|
|
const mtu = $("#mtu").val();
|
|
const persistent_keepalive = $("#persistent_keepalive").val();
|
|
const firewall_mark = $("#firewall_mark").val();
|
|
const table = $("#table").val();
|
|
const config_file_path = $("#config_file_path").val();
|
|
const session_timeout_minutes = Number($("#session_timeout_minutes").val() || 30);
|
|
const totp_enabled = $("#totp_enabled").is(":checked");
|
|
const ui_language = $("#ui_language").val();
|
|
const ui_theme = $("#ui_theme").val();
|
|
const realtime_stats_enabled = $("#realtime_stats_enabled").is(":checked");
|
|
return normalizeGlobalSettingsData({
|
|
"endpoint_address": endpoint_address,
|
|
"dns_servers": dns_servers,
|
|
"mtu": mtu,
|
|
"persistent_keepalive": persistent_keepalive,
|
|
"firewall_mark": firewall_mark,
|
|
"table": table,
|
|
"config_file_path": config_file_path,
|
|
"session_timeout_minutes": session_timeout_minutes,
|
|
"totp_enabled": totp_enabled,
|
|
"ui_language": ui_language,
|
|
"ui_theme": ui_theme,
|
|
"realtime_stats_enabled": realtime_stats_enabled
|
|
});
|
|
}
|
|
|
|
/** After POST /global-settings from topbar (any page); refreshes baseline dirty-state on this settings tab. */
|
|
window.wgRefreshGlobalSettingsBaseline = function () {
|
|
try {
|
|
var d = collectGlobalSettingsData();
|
|
wgSettingsInitialSnap = getGlobalSettingsSnap(d);
|
|
wgSettingsWGCardInitialSnap = snapWGCardStr(d);
|
|
wgSettingsUIPrefsInitialSnap = snapUIPrefsStr(d);
|
|
} catch (e0) {}
|
|
};
|
|
|
|
function updateEndpointSuggestionIP() {
|
|
var $sel = $("#ip_suggestion");
|
|
var btn = document.getElementById("btn_use_ip");
|
|
if (btn) btn.disabled = true;
|
|
$.getJSON("{{.basePath}}/api/machine-ips", null, function(data) {
|
|
$sel.empty();
|
|
$.each(data, function(index, item) {
|
|
$sel.append(
|
|
$("<option></option>")
|
|
.text(item.ip_address + ' - ' + item.name)
|
|
.val(item.ip_address)
|
|
);
|
|
});
|
|
try { $sel.trigger("change.select2"); } catch (eCh) { $sel.trigger("change"); }
|
|
if (btn) btn.disabled = $sel.find("option").length === 0;
|
|
}).fail(function(){ if (btn) btn.disabled = true; });
|
|
}
|
|
</script>
|
|
<script>
|
|
// DNS in JSON (ipv6 y comillas seguras en JS)
|
|
var wgInitialDNS = {{.dnsServersJS}};
|
|
|
|
$("#dns_servers").tagsInput({
|
|
'width': '100%',
|
|
'interactive': true,
|
|
'defaultText': wgT('settings.dns_tags_add'),
|
|
'removeWithBackspace': true,
|
|
'minChars': 0,
|
|
'minInputWidth': '100%',
|
|
'placeholderColor': '#666666'
|
|
});
|
|
|
|
try {
|
|
if (Array.isArray(wgInitialDNS)) {
|
|
wgInitialDNS.forEach(function (tag) {
|
|
if (tag !== null && tag !== undefined && String(tag).trim() !== "") {
|
|
$("#dns_servers").addTag(String(tag).trim());
|
|
}
|
|
});
|
|
}
|
|
} catch (eDns) {}
|
|
|
|
// Global setting form validation
|
|
$(document).ready(function () {
|
|
$.validator.setDefaults({
|
|
submitHandler: function () {
|
|
const data = collectGlobalSettingsData();
|
|
if (getGlobalSettingsSnap(data) === wgSettingsInitialSnap) {
|
|
toastr.info(wgT('settings.toast_no_changes'));
|
|
try { localStorage.removeItem('wg_pending_global_settings'); } catch(e){}
|
|
try { localStorage.removeItem(WG_PENDING_KIND_KEY); } catch(e){}
|
|
if (typeof updateApplyConfigVisibility === "function") updateApplyConfigVisibility();
|
|
return;
|
|
}
|
|
var dirtyWg = snapWGCardStr(data) !== wgSettingsWGCardInitialSnap;
|
|
var dirtyUi = snapUIPrefsStr(data) !== wgSettingsUIPrefsInitialSnap;
|
|
var kind = dirtyWg && dirtyUi ? "both" : dirtyWg ? "wg_card" : "ui_only";
|
|
try { localStorage.setItem('wg_pending_global_settings', JSON.stringify(data)); } catch(e){}
|
|
try { localStorage.setItem(WG_PENDING_KIND_KEY, kind); } catch(e){}
|
|
if (typeof updateApplyConfigVisibility === "function") {
|
|
updateApplyConfigVisibility();
|
|
} else {
|
|
$("#apply-config-button").show();
|
|
}
|
|
toastr.info(wgT('settings.toast_stage_local'));
|
|
}
|
|
});
|
|
$("#frm_global_settings").validate({
|
|
rules: {
|
|
mtu: {
|
|
required: false,
|
|
digits: true,
|
|
range: [1280, 9000]
|
|
},
|
|
persistent_keepalive: {
|
|
digits: true
|
|
},
|
|
config_file_path: {
|
|
required: true
|
|
},
|
|
firewall_mark: {
|
|
required: false
|
|
},
|
|
table: {
|
|
required: false
|
|
},
|
|
session_timeout_minutes: {
|
|
required: true,
|
|
digits: true,
|
|
range: [5, 1440]
|
|
}
|
|
},
|
|
messages: {
|
|
mtu: {
|
|
digits: wgT('settings.val_mtu_digits'),
|
|
range: wgT('settings.val_mtu_range')
|
|
},
|
|
persistent_keepalive: {
|
|
digits: wgT('settings.val_keepalive_digits')
|
|
},
|
|
config_file_path: {
|
|
required: wgT('settings.val_conf_required')
|
|
},
|
|
session_timeout_minutes: {
|
|
digits: wgT('settings.val_session_digits'),
|
|
range: wgT('settings.val_session_range')
|
|
}
|
|
},
|
|
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 () {
|
|
var initD = collectGlobalSettingsData();
|
|
wgSettingsInitialSnap = getGlobalSettingsSnap(initD);
|
|
wgSettingsWGCardInitialSnap = snapWGCardStr(initD);
|
|
wgSettingsUIPrefsInitialSnap = snapUIPrefsStr(initD);
|
|
|
|
try {
|
|
if (!hasPendingSettingsChanges()) {
|
|
localStorage.removeItem('wg_pending_global_settings');
|
|
localStorage.removeItem(WG_PENDING_KIND_KEY);
|
|
}
|
|
} catch (e) {}
|
|
|
|
if (typeof updateApplyConfigVisibility === "function") {
|
|
updateApplyConfigVisibility();
|
|
} else {
|
|
$("#apply-config-button").toggle(hasPendingSettingsChanges());
|
|
}
|
|
});
|
|
|
|
/** Modal Select2: dropdownParent fixes stacking and matches wg-shell styling */
|
|
$(document).ready(function () {
|
|
try {
|
|
if ($("#ip_suggestion").length && $("#modal_endpoint_address_suggestion").length) {
|
|
if ($("#ip_suggestion").data("select2")) {
|
|
$("#ip_suggestion").select2("destroy");
|
|
}
|
|
$("#ip_suggestion").select2({
|
|
width: "100%",
|
|
dropdownParent: $("#modal_endpoint_address_suggestion"),
|
|
placeholder: wgT('settings.placeholder_select_ip')
|
|
});
|
|
}
|
|
} catch (eSel) {}
|
|
});
|
|
|
|
// Endpoint IP suggestion modal event
|
|
$(document).ready(function () {
|
|
$("#modal_endpoint_address_suggestion").on('shown.bs.modal', function (e) {
|
|
updateEndpointSuggestionIP();
|
|
});
|
|
});
|
|
|
|
// Use selected IP address from suggestion form
|
|
$(document).ready(function () {
|
|
$("#btn_use_ip").click(function () {
|
|
var ip = $("#ip_suggestion").val();
|
|
if (Array.isArray(ip)) ip = ip[0];
|
|
$("#endpoint_address").val(ip || '');
|
|
$("#modal_endpoint_address_suggestion").modal('hide');
|
|
});
|
|
});
|
|
</script>
|
|
{{end}}
|