feat: geo-restriction via Cloudflare WAF custom rules
Add country-based access control managed through the Settings page.
Global allow-list with per-app overrides, searchable country selector,
automatic sync to Cloudflare WAF on settings change / deploy / remove,
plus periodic 6-hour verification.
New package: internal/cloudflare/ (client, zone, waf, countries, geosync)
New API: /api/geo/* (6 endpoints) + /api/stacks/{name}/geo/override
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -478,6 +478,22 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
data["HasOptionalConfig"] = found.Meta.HasOptionalConfig()
|
||||
data["EffectiveSubdomain"] = effectiveSubdomain
|
||||
|
||||
// Geo-restriction per-app data
|
||||
geo := s.settings.GetGeoRestriction()
|
||||
if geo != nil && geo.Enabled && s.cfg.Infrastructure.CFAPIToken != "" {
|
||||
data["GeoGlobalEnabled"] = true
|
||||
data["GeoGlobalCountries"] = geo.AllowedCountries
|
||||
if ov, ok := geo.AppOverrides[found.Name]; ok {
|
||||
data["GeoAppOverride"] = true
|
||||
data["GeoAppOverrideCountries"] = ov.AllowedCountries
|
||||
} else {
|
||||
data["GeoAppOverrideCountries"] = []string{}
|
||||
}
|
||||
} else {
|
||||
data["GeoGlobalCountries"] = []string{}
|
||||
data["GeoAppOverrideCountries"] = []string{}
|
||||
}
|
||||
|
||||
s.executeTemplate(w, r, "app_info", data)
|
||||
}
|
||||
|
||||
@@ -1084,6 +1100,33 @@ func (s *Server) settingsData() map[string]interface{} {
|
||||
data["SupportEmail"] = "support@felhom.eu"
|
||||
data["SupportURL"] = "https://felhom.eu/kapcsolat"
|
||||
|
||||
// Geo-restriction data
|
||||
data["CFConfigured"] = s.cfg.Infrastructure.CFAPIToken != ""
|
||||
geo := s.settings.GetGeoRestriction()
|
||||
if geo != nil {
|
||||
data["GeoEnabled"] = geo.Enabled
|
||||
data["GeoAllowedCountries"] = geo.AllowedCountries
|
||||
data["GeoAppOverrides"] = geo.AppOverrides
|
||||
data["GeoLastSync"] = geo.LastSync
|
||||
data["GeoLastError"] = geo.LastSyncError
|
||||
} else {
|
||||
data["GeoEnabled"] = false
|
||||
data["GeoAllowedCountries"] = []string{"HU"}
|
||||
data["GeoAppOverrides"] = map[string]interface{}{}
|
||||
}
|
||||
// Deployed apps for per-app override selector
|
||||
var deployedApps []map[string]string
|
||||
for _, stack := range s.stackMgr.GetStacks() {
|
||||
if !stack.Deployed || s.cfg.IsProtectedStack(stack.Name) {
|
||||
continue
|
||||
}
|
||||
deployedApps = append(deployedApps, map[string]string{
|
||||
"Name": stack.Name,
|
||||
"Display": stack.Meta.DisplayName,
|
||||
})
|
||||
}
|
||||
data["DeployedApps"] = deployedApps
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -179,5 +179,141 @@ async function saveOptionalConfig(stackName) {
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{if .GeoGlobalEnabled}}
|
||||
<div class="app-optional-config">
|
||||
<h3>Földrajzi korlátozás</h3>
|
||||
<p class="config-group-desc">
|
||||
Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi.
|
||||
</p>
|
||||
|
||||
<label class="toggle" style="margin-bottom:1rem">
|
||||
<input type="checkbox" id="app-geo-override-toggle"
|
||||
{{if .GeoAppOverride}}checked{{end}}
|
||||
onchange="toggleAppGeoOverride(this.checked)">
|
||||
<span class="toggle-label">Egyéni országkorlátozás</span>
|
||||
</label>
|
||||
|
||||
<div id="app-geo-override-details" {{if not .GeoAppOverride}}style="display:none"{{end}}>
|
||||
<div class="form-group">
|
||||
<label>Engedélyezett országok ehhez az alkalmazáshoz</label>
|
||||
<div class="geo-country-selector">
|
||||
<input type="text" id="app-geo-search" class="form-control config-input"
|
||||
placeholder="Ország keresése..." autocomplete="off"
|
||||
oninput="filterAppGeoCountries(this.value)"
|
||||
onfocus="showAppGeoList()"
|
||||
onblur="setTimeout(function(){hideAppGeoList()},200)">
|
||||
<div class="geo-country-list" id="app-geo-country-list"></div>
|
||||
</div>
|
||||
<div class="geo-selected-tags" id="app-geo-tags"></div>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-primary" onclick="saveAppGeoOverride()">Mentés</button>
|
||||
<span id="app-geo-status" class="config-save-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var allCountries = [];
|
||||
var appGeoCountries = {{json .GeoAppOverrideCountries}};
|
||||
var stackName = '{{.Stack.Name}}';
|
||||
|
||||
function loadCountries(cb) {
|
||||
if (allCountries.length > 0) { cb(); return; }
|
||||
fetch('/api/geo/countries', {headers: csrfHeaders()})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){ if(d.ok) allCountries = d.data; cb(); })
|
||||
.catch(function(){ cb(); });
|
||||
}
|
||||
|
||||
window.toggleAppGeoOverride = function(enabled) {
|
||||
document.getElementById('app-geo-override-details').style.display = enabled ? '' : 'none';
|
||||
if (enabled) {
|
||||
if (!appGeoCountries || appGeoCountries.length === 0) {
|
||||
appGeoCountries = {{json .GeoGlobalCountries}};
|
||||
}
|
||||
loadCountries(renderAppGeoTags);
|
||||
} else {
|
||||
// Remove override
|
||||
fetch('/api/stacks/' + stackName + '/geo/override', {method:'DELETE', headers:csrfHeaders()});
|
||||
}
|
||||
};
|
||||
|
||||
window.showAppGeoList = function() { loadCountries(function(){ filterAppGeoCountries(''); }); };
|
||||
window.hideAppGeoList = function() { document.getElementById('app-geo-country-list').style.display='none'; };
|
||||
|
||||
window.filterAppGeoCountries = function(q) {
|
||||
var list = document.getElementById('app-geo-country-list');
|
||||
q = q.toLowerCase();
|
||||
var html = ''; var count = 0;
|
||||
for (var i = 0; i < allCountries.length && count < 15; i++) {
|
||||
var c = allCountries[i];
|
||||
if (appGeoCountries.indexOf(c.code) >= 0) continue;
|
||||
if (q && c.name.toLowerCase().indexOf(q) < 0 && c.code.toLowerCase().indexOf(q) < 0) continue;
|
||||
html += '<div class="geo-country-option" onmousedown="addAppGeoCountry(\'' + c.code + '\')">'
|
||||
+ esc(c.name) + ' <small>(' + c.code + ')</small></div>';
|
||||
count++;
|
||||
}
|
||||
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
|
||||
list.style.display = (count > 0 || q) ? '' : 'none';
|
||||
};
|
||||
|
||||
window.addAppGeoCountry = function(code) {
|
||||
if (appGeoCountries.indexOf(code) >= 0) return;
|
||||
appGeoCountries.push(code);
|
||||
renderAppGeoTags();
|
||||
document.getElementById('app-geo-search').value = '';
|
||||
hideAppGeoList();
|
||||
};
|
||||
|
||||
window.removeAppGeoCountry = function(code) {
|
||||
appGeoCountries = appGeoCountries.filter(function(c){return c !== code});
|
||||
renderAppGeoTags();
|
||||
};
|
||||
|
||||
function renderAppGeoTags() {
|
||||
var el = document.getElementById('app-geo-tags');
|
||||
var html = '';
|
||||
for (var i = 0; i < appGeoCountries.length; i++) {
|
||||
var code = appGeoCountries[i];
|
||||
var name = code;
|
||||
for (var j = 0; j < allCountries.length; j++) {
|
||||
if (allCountries[j].code === code) { name = allCountries[j].name; break; }
|
||||
}
|
||||
html += '<span class="geo-tag">' + esc(name) + ' (' + code + ') '
|
||||
+ '<span class="geo-tag-remove" onclick="removeAppGeoCountry(\'' + code + '\')">×</span></span>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
window.saveAppGeoOverride = function() {
|
||||
var status = document.getElementById('app-geo-status');
|
||||
fetch('/api/stacks/' + stackName + '/geo/override', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({allowed_countries: appGeoCountries})
|
||||
})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
status.textContent = d.ok ? (d.message || 'Mentve') : (d.error || 'Hiba');
|
||||
status.className = 'config-save-status ' + (d.ok ? 'config-save-ok' : 'config-save-err');
|
||||
setTimeout(function(){ status.textContent=''; }, 5000);
|
||||
})
|
||||
.catch(function(){
|
||||
status.textContent = 'Hálózati hiba';
|
||||
status.className = 'config-save-status config-save-err';
|
||||
});
|
||||
};
|
||||
|
||||
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
if (document.getElementById('app-geo-override-toggle') && document.getElementById('app-geo-override-toggle').checked) {
|
||||
loadCountries(renderAppGeoTags);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{template "layout_end" .}}
|
||||
{{end}}
|
||||
|
||||
@@ -376,6 +376,345 @@ function pollUntilBack() {
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Section: Geo-Restriction -->
|
||||
<div class="settings-card">
|
||||
<h3>Földrajzi korlátozás</h3>
|
||||
<p class="settings-card-desc">
|
||||
Ország alapján korlátozható a webes alkalmazások elérése a Cloudflare WAF segítségével.
|
||||
<br><span class="form-hint">A helyi hálózati hozzáférés mindig engedélyezett (nem halad át a Cloudflare-en).</span>
|
||||
</p>
|
||||
|
||||
{{if not .CFConfigured}}
|
||||
<div class="alert alert-info">
|
||||
A Cloudflare API token nincs konfigurálva. Kérd az üzemeltetőt a beállításhoz.<br>
|
||||
<small>A tokennek <strong>Zone WAF:Edit</strong> jogosultsággal kell rendelkeznie.</small>
|
||||
</div>
|
||||
{{else}}
|
||||
<div id="geo-status-msg"></div>
|
||||
|
||||
<label class="toggle" style="margin-bottom:1rem">
|
||||
<input type="checkbox" id="geo-enabled" {{if .GeoEnabled}}checked{{end}}
|
||||
onchange="toggleGeo(this.checked)">
|
||||
<span class="toggle-label">Geo-korlátozás aktív</span>
|
||||
</label>
|
||||
|
||||
<div id="geo-details" {{if not .GeoEnabled}}style="display:none"{{end}}>
|
||||
<!-- Global allowed countries -->
|
||||
<div class="form-group">
|
||||
<label>Engedélyezett országok (globális)</label>
|
||||
<div class="geo-country-selector" id="geo-countries">
|
||||
<input type="text" id="geo-search" class="form-control"
|
||||
placeholder="Ország keresése..."
|
||||
autocomplete="off"
|
||||
oninput="filterCountries(this.value)"
|
||||
onfocus="showCountryList()"
|
||||
onblur="setTimeout(function(){hideCountryList()},200)">
|
||||
<div class="geo-country-list" id="geo-country-list"></div>
|
||||
</div>
|
||||
<div class="geo-selected-tags" id="geo-selected-tags"></div>
|
||||
<span class="form-hint">Csak a kiválasztott országokból érhető el a rendszer.</span>
|
||||
</div>
|
||||
|
||||
<!-- Per-app overrides -->
|
||||
<div class="form-group" style="margin-top:1.5rem">
|
||||
<label>Alkalmazás-specifikus felülírások</label>
|
||||
<div id="geo-app-overrides"></div>
|
||||
{{if .DeployedApps}}
|
||||
<div style="margin-top:.5rem;display:flex;align-items:center;gap:.5rem">
|
||||
<select id="geo-add-app-select" class="form-control" style="max-width:250px">
|
||||
<option value="">— Alkalmazás kiválasztása —</option>
|
||||
{{range .DeployedApps}}
|
||||
<option value="{{.Name}}">{{.Display}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline" onclick="addAppOverride()">+ Hozzáadás</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Sync status & save -->
|
||||
<div class="form-group" style="margin-top:1.5rem">
|
||||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||
<button class="btn btn-primary" id="btn-geo-save" onclick="saveGeoSettings()">
|
||||
Mentés és szinkronizálás
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="triggerGeoSync()">Kézi szinkronizálás</button>
|
||||
<span id="geo-sync-status" class="form-hint">
|
||||
{{if .GeoLastSync}}Utolsó szinkronizálás: {{.GeoLastSync}}{{end}}
|
||||
{{if .GeoLastError}} <span class="state-text-red">{{.GeoLastError}}</span>{{end}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// Geo-restriction UI state
|
||||
var allCountries = [];
|
||||
var selectedCountries = {{json .GeoAllowedCountries}};
|
||||
var appOverrides = {{json .GeoAppOverrides}};
|
||||
|
||||
// Load countries list on first use
|
||||
function ensureCountries(cb) {
|
||||
if (allCountries.length > 0) { cb(); return; }
|
||||
fetch('/api/geo/countries', {headers: csrfHeaders()})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if (d.ok) allCountries = d.data;
|
||||
cb();
|
||||
})
|
||||
.catch(function(){ cb(); });
|
||||
}
|
||||
|
||||
window.toggleGeo = function(enabled) {
|
||||
document.getElementById('geo-details').style.display = enabled ? '' : 'none';
|
||||
if (enabled) ensureCountries(renderTags);
|
||||
};
|
||||
|
||||
window.showCountryList = function() {
|
||||
ensureCountries(function(){ filterCountries(document.getElementById('geo-search').value); });
|
||||
};
|
||||
|
||||
window.hideCountryList = function() {
|
||||
document.getElementById('geo-country-list').style.display = 'none';
|
||||
};
|
||||
|
||||
window.filterCountries = function(query) {
|
||||
var list = document.getElementById('geo-country-list');
|
||||
var q = query.toLowerCase();
|
||||
var html = '';
|
||||
var count = 0;
|
||||
for (var i = 0; i < allCountries.length && count < 15; i++) {
|
||||
var c = allCountries[i];
|
||||
if (selectedCountries.indexOf(c.code) >= 0) continue;
|
||||
if (q && c.name.toLowerCase().indexOf(q) < 0 && c.code.toLowerCase().indexOf(q) < 0) continue;
|
||||
html += '<div class="geo-country-option" onmousedown="addCountry(\'' + c.code + '\',\'' + escHtml(c.name) + '\')">'
|
||||
+ escHtml(c.name) + ' <small>(' + c.code + ')</small></div>';
|
||||
count++;
|
||||
}
|
||||
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
|
||||
list.style.display = count > 0 || q ? '' : 'none';
|
||||
};
|
||||
|
||||
window.addCountry = function(code, name) {
|
||||
if (selectedCountries.indexOf(code) >= 0) return;
|
||||
selectedCountries.push(code);
|
||||
renderTags();
|
||||
document.getElementById('geo-search').value = '';
|
||||
hideCountryList();
|
||||
};
|
||||
|
||||
window.removeCountry = function(code) {
|
||||
if (code === 'HU') {
|
||||
if (!confirm('Figyelem: Magyarország eltávolítása azt jelenti, hogy magyar IP-ről sem lesz elérhető a rendszer távolról. Biztosan folytatja?')) return;
|
||||
}
|
||||
selectedCountries = selectedCountries.filter(function(c){return c !== code});
|
||||
renderTags();
|
||||
};
|
||||
|
||||
function renderTags() {
|
||||
var el = document.getElementById('geo-selected-tags');
|
||||
var html = '';
|
||||
for (var i = 0; i < selectedCountries.length; i++) {
|
||||
var code = selectedCountries[i];
|
||||
var name = countryName(code);
|
||||
var isHU = code === 'HU' ? ' geo-tag-hu' : '';
|
||||
html += '<span class="geo-tag' + isHU + '">'
|
||||
+ escHtml(name) + ' (' + code + ') '
|
||||
+ '<span class="geo-tag-remove" onclick="removeCountry(\'' + code + '\')">×</span>'
|
||||
+ '</span>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
renderAppOverrides();
|
||||
}
|
||||
|
||||
function countryName(code) {
|
||||
for (var i = 0; i < allCountries.length; i++) {
|
||||
if (allCountries[i].code === code) return allCountries[i].name;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// --- Per-app overrides ---
|
||||
window.addAppOverride = function() {
|
||||
var sel = document.getElementById('geo-add-app-select');
|
||||
var appName = sel.value;
|
||||
if (!appName) return;
|
||||
if (!appOverrides) appOverrides = {};
|
||||
if (appOverrides[appName]) { sel.value = ''; return; }
|
||||
// Default: same countries as global
|
||||
appOverrides[appName] = {allowed_countries: selectedCountries.slice()};
|
||||
sel.value = '';
|
||||
renderAppOverrides();
|
||||
};
|
||||
|
||||
window.removeAppOverride = function(appName) {
|
||||
delete appOverrides[appName];
|
||||
renderAppOverrides();
|
||||
};
|
||||
|
||||
window.toggleAppCountry = function(appName, code, el) {
|
||||
var ov = appOverrides[appName];
|
||||
if (!ov) return;
|
||||
var idx = ov.allowed_countries.indexOf(code);
|
||||
if (idx >= 0) {
|
||||
if (code === 'HU' && !confirm('Magyarország eltávolítása nem ajánlott. Folytatja?')) {
|
||||
el.checked = true;
|
||||
return;
|
||||
}
|
||||
ov.allowed_countries.splice(idx, 1);
|
||||
} else {
|
||||
ov.allowed_countries.push(code);
|
||||
}
|
||||
};
|
||||
|
||||
function renderAppOverrides() {
|
||||
var el = document.getElementById('geo-app-overrides');
|
||||
if (!appOverrides || Object.keys(appOverrides).length === 0) {
|
||||
el.innerHTML = '<p class="form-hint">Nincs alkalmazás-specifikus beállítás. Minden alkalmazás a globális beállítást követi.</p>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
for (var appName in appOverrides) {
|
||||
var ov = appOverrides[appName];
|
||||
var displayName = appName;
|
||||
// Try to find display name from select
|
||||
var opts = document.getElementById('geo-add-app-select');
|
||||
if (opts) {
|
||||
for (var j = 0; j < opts.options.length; j++) {
|
||||
if (opts.options[j].value === appName) { displayName = opts.options[j].text; break; }
|
||||
}
|
||||
}
|
||||
html += '<div class="geo-app-override-row">';
|
||||
html += '<strong>' + escHtml(displayName) + '</strong>';
|
||||
html += '<div class="geo-selected-tags" style="flex:1;margin:0 .5rem">';
|
||||
for (var i = 0; i < ov.allowed_countries.length; i++) {
|
||||
var code = ov.allowed_countries[i];
|
||||
html += '<span class="geo-tag geo-tag-sm">' + code + '</span>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '<button class="btn btn-sm btn-outline" onclick="editAppOverride(\'' + appName + '\')">Szerkesztés</button>';
|
||||
html += '<button class="btn btn-sm btn-danger-outline" onclick="removeAppOverride(\'' + appName + '\')">Törlés</button>';
|
||||
html += '</div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
window.editAppOverride = function(appName) {
|
||||
var ov = appOverrides[appName];
|
||||
if (!ov) return;
|
||||
ensureCountries(function(){
|
||||
var checked = {};
|
||||
for (var i = 0; i < ov.allowed_countries.length; i++) checked[ov.allowed_countries[i]] = true;
|
||||
var html = '<div class="geo-edit-overlay" id="geo-edit-' + appName + '">';
|
||||
html += '<h4>Engedélyezett országok: ' + escHtml(appName) + '</h4>';
|
||||
html += '<div class="geo-edit-grid">';
|
||||
for (var i = 0; i < allCountries.length; i++) {
|
||||
var c = allCountries[i];
|
||||
html += '<label class="geo-edit-item"><input type="checkbox" value="' + c.code + '"'
|
||||
+ (checked[c.code] ? ' checked' : '') + ' onchange="toggleAppCountry(\'' + appName + '\',\'' + c.code + '\',this)">'
|
||||
+ ' ' + escHtml(c.name) + ' (' + c.code + ')</label>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '<button class="btn btn-sm btn-primary" style="margin-top:.5rem" onclick="closeAppEdit(\'' + appName + '\')">Kész</button>';
|
||||
html += '</div>';
|
||||
document.getElementById('geo-app-overrides').innerHTML += html;
|
||||
});
|
||||
};
|
||||
|
||||
window.closeAppEdit = function(appName) {
|
||||
var el = document.getElementById('geo-edit-' + appName);
|
||||
if (el) el.remove();
|
||||
renderAppOverrides();
|
||||
};
|
||||
|
||||
// --- Save & Sync ---
|
||||
window.saveGeoSettings = function() {
|
||||
var btn = document.getElementById('btn-geo-save');
|
||||
var status = document.getElementById('geo-status-msg');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Mentés...';
|
||||
|
||||
var payload = {
|
||||
enabled: document.getElementById('geo-enabled').checked,
|
||||
allowed_countries: selectedCountries
|
||||
};
|
||||
|
||||
fetch('/api/geo/settings', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if (d.ok) {
|
||||
status.innerHTML = '<div class="alert alert-info">' + (d.message || 'Mentve') + '</div>';
|
||||
// Save per-app overrides
|
||||
if (appOverrides && Object.keys(appOverrides).length > 0) {
|
||||
saveAllAppOverrides();
|
||||
}
|
||||
} else {
|
||||
status.innerHTML = '<div class="alert alert-error">' + (d.error || 'Hiba') + '</div>';
|
||||
}
|
||||
})
|
||||
.catch(function(err){
|
||||
status.innerHTML = '<div class="alert alert-error">Hálózati hiba</div>';
|
||||
})
|
||||
.finally(function(){
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Mentés és szinkronizálás';
|
||||
setTimeout(function(){ status.innerHTML = ''; }, 8000);
|
||||
});
|
||||
};
|
||||
|
||||
function saveAllAppOverrides() {
|
||||
for (var appName in appOverrides) {
|
||||
(function(name, ov){
|
||||
fetch('/api/stacks/' + name + '/geo/override', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({allowed_countries: ov.allowed_countries})
|
||||
});
|
||||
})(appName, appOverrides[appName]);
|
||||
}
|
||||
}
|
||||
|
||||
window.triggerGeoSync = function() {
|
||||
fetch('/api/geo/sync', {method:'POST', headers: csrfHeaders()})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
var status = document.getElementById('geo-sync-status');
|
||||
status.textContent = d.ok ? 'Szinkronizálás elindítva...' : (d.error || 'Hiba');
|
||||
setTimeout(function(){
|
||||
fetch('/api/geo/status', {headers: csrfHeaders()})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if (d.ok && d.data) {
|
||||
var sync = d.data.last_sync || '';
|
||||
var err = d.data.last_sync_error || '';
|
||||
status.innerHTML = sync ? ('Utolsó: ' + sync.substring(0,19).replace('T',' ')) : '';
|
||||
if (err) status.innerHTML += ' <span class="state-text-red">' + escHtml(err) + '</span>';
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
function escHtml(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
if (document.getElementById('geo-enabled') && document.getElementById('geo-enabled').checked) {
|
||||
ensureCountries(renderTags);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Section B: Password Change -->
|
||||
<div class="settings-card">
|
||||
<h3>Jelszó módosítás</h3>
|
||||
|
||||
@@ -3009,3 +3009,50 @@ a.stat-card:hover {
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
@keyframes debug-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* --- Geo-restriction UI --- */
|
||||
.geo-country-selector { position: relative; }
|
||||
.geo-selected-tags { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.5rem; }
|
||||
.geo-tag {
|
||||
display: inline-flex; align-items: center; gap: 0.3rem;
|
||||
background: rgba(0,136,204,0.15); color: var(--accent-light);
|
||||
padding: 0.25rem 0.55rem; border-radius: 4px; font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.geo-tag-hu { background: rgba(35,134,54,0.2); color: var(--green); }
|
||||
.geo-tag-sm { font-size: 0.75rem; padding: 0.15rem 0.4rem; }
|
||||
.geo-tag-remove { cursor: pointer; opacity: 0.7; font-size: 1.1em; line-height: 1; }
|
||||
.geo-tag-remove:hover { opacity: 1; }
|
||||
.geo-country-list {
|
||||
position: absolute; z-index: 10; background: var(--bg-card);
|
||||
border: 1px solid var(--border-color); border-radius: 6px;
|
||||
max-height: 220px; overflow-y: auto; width: 100%; margin-top: 2px;
|
||||
display: none;
|
||||
}
|
||||
.geo-country-option {
|
||||
padding: 0.4rem 0.75rem; cursor: pointer; font-size: 0.9rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.geo-country-option:hover { background: rgba(0,136,204,0.1); }
|
||||
.geo-country-option small { color: var(--text-muted); }
|
||||
.geo-app-override-row {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.geo-app-override-row strong { min-width: 120px; color: var(--text-primary); }
|
||||
.geo-edit-overlay {
|
||||
background: var(--bg-secondary); border: 1px solid var(--border-color);
|
||||
border-radius: 8px; padding: 1rem; margin-top: 0.5rem;
|
||||
}
|
||||
.geo-edit-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 0.2rem; max-height: 250px; overflow-y: auto; margin-top: 0.5rem;
|
||||
}
|
||||
.geo-edit-item { font-size: 0.85rem; cursor: pointer; padding: 0.15rem 0; color: var(--text-primary); }
|
||||
.geo-edit-item input { margin-right: 0.3rem; }
|
||||
.btn-danger-outline {
|
||||
background: transparent; color: var(--red); border: 1px solid var(--red);
|
||||
border-radius: 6px; padding: 0.2rem 0.5rem; cursor: pointer; font-size: 0.8rem;
|
||||
}
|
||||
.btn-danger-outline:hover { background: var(--red-bg); }
|
||||
|
||||
Reference in New Issue
Block a user