Files
felhom-controller/controller/internal/web/templates/settings_security.html
T
admin 8dbbc98ff2
gates / gates (push) Successful in 18s
v0.207.0 — R-249: the retrieval passphrase leaves the page body; R-252/R-253: two refusals learn to say what to do
R-249. settings_security.html rendered the passphrase into a display:none
span behind a Megjelenit button. That toggle stops a browser DRAWING the value
and nothing else — the plaintext was in the response body of every render, so a
curl of the page returned it. Found by exactly that: it landed in a session
transcript while driving the documented rebuild path.

The codebase already stated this rule for the recovery code and this page did not
follow it (escrow_handlers.go: 'reveal (claim XHR only — R is NEVER templated
server-side into HTML)'). The page now carries only HasRetrievalPassword; the
value comes from POST /settings/retrieval-password/reveal — CSRF-covered because
POST, no-store, and LOGGED as an act, which reading it off the markup never was.

The tests assert the RAW RESPONSE BODY. Every test that asked what the customer
sees passed while the bytes carried the secret; that is why this survived.

Census: the render-then-hide pattern appears twice more — app_info.html (a real
per-install app password in a hidden span) and deploy.html. Filed as R-254, NOT
fixed here.

R-252. A rebuilt box keeps its drives but loses their REGISTRATION. The restore
page now states that before the customer presses anything, says the backups and
drives are both still there, and links to Tarhely > Meghajtok. Page and resolver
ask ONE question — HasRestoreDestination() reads the same
GetSchedulableStoragePaths() the scratch resolver reads.

R-253. The list promised 'a visszaallitas elobb ujratelepiti' three lines above a
refusal that fired BECAUSE the app was not installed. The promise was the wrong
half: reconstitution writes to the app's own GetStackHDDPath, which exists only
once the CUSTOMER has chosen a drive at deploy time. Auto-reinstalling would mean
the product making that choice for them. Copy now says to install first and routes
to /stacks/<app>/deploy.

Both notices are conditional — a healthy box renders as before, pinned by a test
that fails if either becomes unconditional.
2026-08-07 18:04:26 +02:00

483 lines
21 KiB
HTML

{{define "settings_security"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>Beállítások — Biztonság és hozzáférés</h2>
</div>
<!-- Section B: Password Change -->
<div class="settings-card">
<h3>Jelszó módosítás</h3>
{{if .AuthEnabled}}
{{if .PasswordError}}<div class="alert alert-error">{{.PasswordError}}</div>{{end}}
<form method="POST" action="/settings/password">
{{.CSRFField}}
<div class="form-group">
<label for="current_password">Jelenlegi jelszó</label>
<input type="password" id="current_password" name="current_password" required
placeholder="Adja meg a jelenlegi jelszavát" class="form-control">
</div>
<div class="form-group">
<label for="new_password">Új jelszó</label>
<input type="password" id="new_password" name="new_password" required minlength="8"
placeholder="Legalább 8 karakter" class="form-control">
</div>
<div class="form-group">
<label for="confirm_password">Új jelszó megerősítése</label>
<input type="password" id="confirm_password" name="confirm_password" required minlength="8"
placeholder="Jelszó mégegyszer" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Jelszó módosítása</button>
</form>
{{else}}
<div class="alert alert-info">
A jelszavas védelem nincs beállítva. Kérd az üzemeltetőt a beállításhoz.
</div>
{{end}}
</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-crit">{{.GeoLastError}}</span>{{end}}
</span>
</div>
</div>
</div>
{{end}}
</div>
<div id="dialog-root"></div>
<script>
// Light overlay dialog (D1) — replaces the native blocking browser dialogs (same texts).
function escText(s){var d=document.createElement('div');d.textContent=String(s==null?'':s);return d.innerHTML;}
function closeDialog(){ var r=document.getElementById('dialog-root'); if(r) r.innerHTML=''; }
function openDialog(opts){
var root=document.getElementById('dialog-root');
if(!root) return;
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
+'</div></div>';
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
}
(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>';
// Reveal with 'block', NOT '' — the .geo-country-list CSS default is display:none,
// and clearing the inline style ('') would fall back to that and keep the (populated)
// list hidden. This was the country-autocomplete "no list" bug.
list.style.display = count > 0 || q ? 'block' : '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') {
openDialog({title:'Figyelem', confirmLabel:'Eltávolítás',
message:'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?',
onConfirm:function(){ doRemoveCountry(code); }});
return;
}
doRemoveCountry(code);
};
function doRemoveCountry(code) {
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 + '\')">&times;</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') {
openDialog({title:'Figyelem', confirmLabel:'Eltávolítás',
message:'Magyarország eltávolítása nem ajánlott. Folytatja?',
onConfirm:function(){ ov.allowed_countries.splice(ov.allowed_countries.indexOf(code), 1); },
onCancel:function(){ 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-crit">' + 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: Recovery Info -->
{{if .HasRetrievalPassword}}
<div class="settings-card">
<h3>Vészhelyzeti információk</h3>
<p class="settings-card-desc">
Ezeket az adatokat mentse el biztos helyre. Újratelepítés esetén szükség lesz rájuk a rendszer visszaállításához.
</p>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Ügyfél azonosító</span>
<span class="settings-value mono">{{.CustomerID}}</span>
</div>
<div class="settings-row">
<span class="settings-label">Hub URL</span>
<span class="settings-value mono">{{.HubURL}}</span>
</div>
<div class="settings-row">
<span class="settings-label">Visszaállítási jelszó</span>
<!-- R-249: the value is NOT in this page. The old markup rendered it into a
display:none span, which hid it from the eye and from nothing else — a fetch of the
page returned the plaintext. „Megjelenít" now asks the server for it. -->
<span class="settings-value">
<span id="retrieval-pw-slot" class="mono">••••••••••••••••</span>
<button type="button" id="retrieval-pw-btn" class="btn btn-xs btn-outline" onclick="revealRetrievalPw()">Megjelenít</button>
<span id="retrieval-pw-err" class="form-hint" style="display:none;color:var(--red)"></span>
</span>
</div>
<div class="settings-row">
<span class="settings-label">Támogatás</span>
<span class="settings-value">
<a href="mailto:{{.SupportEmail}}" style="color: var(--blue);">{{.SupportEmail}}</a>
&nbsp;|&nbsp;
<a href="{{.SupportURL}}" target="_blank" style="color: var(--blue);">felhom.eu/kapcsolat</a>
</span>
</div>
</div>
</div>
<script>
// R-249: fetch the passphrase on demand. The page itself never contains it, so this is the only
// way it reaches a browser — and it takes a live session plus the CSRF token bound to it.
function revealRetrievalPw() {
var slot = document.getElementById('retrieval-pw-slot');
var btn = document.getElementById('retrieval-pw-btn');
var err = document.getElementById('retrieval-pw-err');
err.style.display = 'none';
if (btn.dataset.shown === '1') { // „Elrejt" — drop the value out of the DOM again
slot.textContent = '••••••••••••••••';
btn.textContent = 'Megjelenít';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/settings/retrieval-password/reveal', {
method: 'POST',
headers: {'X-CSRF-Token': '{{.CSRFToken}}'},
credentials: 'same-origin'
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) {
err.textContent = j.error || 'A visszaállítási jelszó lekérése nem sikerült.';
err.style.display = 'inline';
return;
}
slot.textContent = j.data.password;
btn.textContent = 'Elrejt';
btn.dataset.shown = '1';
}).catch(function () {
btn.disabled = false;
err.textContent = 'A visszaállítási jelszó lekérése nem sikerült.';
err.style.display = 'inline';
});
}
</script>
{{end}}
{{template "layout_end" .}}
{{end}}