2dd05670ae
report.Waiter holds a hanging GET against hub /api/v1/wait?gen=N (same hub URL+key as the pusher). On a generation change it fires the v0.139.0 report.Trigger — nothing else; the report ACK delivers everything through the unchanged machinery. No overall client timeout (held GET); per-request ctx bounds a dead connection. First-observation records-not-fires; same-gen timeout fires nothing; errors (incl. 404 from a pre-v0.58.0 hub) back off 5s->5min while the 15-min cycle reconciles. Wired beside the trigger under the same hubPusher!=nil && Hub.Enabled gate. Red-proof: disable the baseline branch -> first observation fires (TestWaiter_FirstObservationRecordsNoFire), run-fail-reverted. Copy soften: backups_remote/escrow "néhány másodperc, legfeljebb 15 perc". Pairs with hub v0.58.0. Grounding: felhom.eu/documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md
299 lines
16 KiB
HTML
299 lines
16 KiB
HTML
{{define "backups_escrow"}}
|
|
{{template "layout_start" .}}
|
|
|
|
<!-- Controller-driven escrow ceremony wizard (v0.127.0). R (the recovery code) arrives ONLY via
|
|
the claim XHR and lives ONLY in this page's JS scope — it is never templated server-side,
|
|
never sent anywhere else, and the page + claim response are Cache-Control: no-store. -->
|
|
|
|
<div class="page-header">
|
|
<div style="display:flex;align-items:center;gap:.5rem">
|
|
<a href="/backups/remote" class="btn btn-sm btn-outline">← Vissza</a>
|
|
<h2>Helyreállítási kód</h2>
|
|
</div>
|
|
<span class="domain-badge">{{.Domain}}</span>
|
|
</div>
|
|
|
|
{{if not .AgentSupported}}
|
|
<div class="alert alert-info">A funkcióhoz az ügynök frissítése szükséges — a frissítés automatikusan megérkezik. Próbálja újra később.</div>
|
|
{{else}}
|
|
|
|
<!-- 1. Preflight -->
|
|
<div class="settings-card" id="pf-card">
|
|
<h3>1. Előfeltételek ellenőrzése</h3>
|
|
<div id="pf-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
|
|
<div id="pf-list"><p class="form-hint">Ellenőrzés folyamatban…</p></div>
|
|
</div>
|
|
|
|
<!-- 2. Warning + 3. Re-auth (revealed when preflight is green) -->
|
|
<div class="settings-card" id="warn-card" style="display:none">
|
|
<h3>2. Fontos tudnivalók</h3>
|
|
<div class="alert alert-warning" style="margin-bottom:.75rem">
|
|
A helyreállítási kód a mentései utolsó kulcsa. Pontosan egyszer jelenik meg — a rendszer
|
|
sehol nem tárolja, és a Felhom sem ismeri. Ha a szerver megsemmisül, a távoli mentések CSAK
|
|
ezzel a kóddal állíthatók vissza. Írja fel papírra vagy mentse jelszókezelőbe — de NE ezen a
|
|
szerveren tárolja.
|
|
</div>
|
|
{{if .Receremony}}
|
|
<div class="alert alert-info" style="margin-bottom:.75rem">
|
|
Az új kód létrehozásával a korábbi kód a jövőbeli mentésekre érvényét veszti; a már meglévő
|
|
mentési előzményekhez érvényes marad.
|
|
</div>
|
|
{{end}}
|
|
<h3 style="margin-top:1.25rem">3. Megerősítés</h3>
|
|
<form id="start-form" onsubmit="return startCeremony(event)">
|
|
<div class="form-group">
|
|
<label for="reauth-password">A folytatáshoz adja meg a bejelentkezési jelszavát</label>
|
|
<input type="password" id="reauth-password" class="form-control" autocomplete="current-password" required style="max-width:280px">
|
|
</div>
|
|
<div id="start-error" class="alert alert-error" style="display:none;margin-bottom:.75rem"></div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary" id="start-btn">Kód létrehozása</button>
|
|
<a href="/backups/remote" class="btn btn-outline">Mégsem</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 4. Progress -->
|
|
<div class="settings-card" id="progress-card" style="display:none">
|
|
<h3>4. Létrehozás</h3>
|
|
<p class="form-hint" id="progress-line">A kód létrehozása folyamatban… (általában néhány másodperc)</p>
|
|
<div id="progress-error" class="alert alert-error" style="display:none"></div>
|
|
</div>
|
|
|
|
<!-- 5. Reveal + 6. Typed-back + 7. Finish -->
|
|
<div class="settings-card" id="reveal-card" style="display:none">
|
|
<h3>5. A helyreállítási kód</h3>
|
|
<div id="reveal-gate">
|
|
<p class="form-hint">A kód pontosan egyszer jeleníthető meg. Készítsen elő papírt és tollat, mielőtt megnyomja a gombot.</p>
|
|
<button class="btn btn-primary" id="reveal-btn" onclick="claimCode()">Kód megjelenítése</button>
|
|
<div id="reveal-error" class="alert alert-error" style="display:none;margin-top:.75rem"></div>
|
|
</div>
|
|
<div id="reveal-shown" style="display:none">
|
|
<div class="alert alert-warning" style="margin-bottom:.5rem">Ez mostantól az élő helyreállítási kód — a korábbi kód érvényét vesztette. Mentse el most: a kód többé nem jeleníthető meg.</div>
|
|
<pre class="mono" id="code-block" style="white-space:pre-wrap;word-break:break-all;background:var(--bg-0);border:1px solid var(--line);border-radius:var(--radius);padding:1rem;font-size:1.05rem"></pre>
|
|
<button type="button" class="btn btn-ghost btn-sm" id="code-blur-toggle" onclick="toggleCodeBlur()">Elrejtés</button>
|
|
<h3 style="margin-top:1.25rem">6. Ellenőrzés</h3>
|
|
<div class="form-group">
|
|
<label id="verify-label" for="verify-w1">Megerősítés: írja be a kód szavait</label>
|
|
<div style="display:flex;gap:.5rem">
|
|
<input type="text" id="verify-w1" class="form-control" autocomplete="off" style="max-width:180px">
|
|
<input type="text" id="verify-w2" class="form-control" autocomplete="off" style="max-width:180px">
|
|
</div>
|
|
<div id="verify-error" class="alert alert-error" style="display:none;margin-top:.5rem">A megadott szavak nem egyeznek — ellenőrizze a felírt kódot.</div>
|
|
</div>
|
|
<button class="btn btn-sm" id="verify-btn" onclick="verifyWords()">Szavak ellenőrzése</button>
|
|
<div id="finish-block" style="display:none;margin-top:1rem">
|
|
<h3>7. Befejezés</h3>
|
|
{{/* v0.138.0: the "what happens next" note — the wizard finishes to /backups/remote,
|
|
where the interim "awaiting hub confirmation" card shows until the flip. Setting
|
|
expectations here avoids surprise at that card. */}}
|
|
<div class="alert alert-info" style="margin-bottom:.75rem">
|
|
<strong>Mi történik ezután?</strong> A helyreállítási kód elkészült, és a rendszer elküldi a központba megerősítésre — ez általában néhány másodperc, legfeljebb 15 perc. Addig a Távoli mentés oldalon a „megerősítésre vár" üzenet látható; a megerősítés után indítható az első távoli mentés.
|
|
</div>
|
|
<label class="toggle" style="margin-bottom:.75rem">
|
|
<input type="checkbox" id="finish-check" onchange="document.getElementById('finish-btn').disabled=!this.checked">
|
|
<span class="toggle-label">Felírtam a kódot, és biztonságos helyen — nem ezen a szerveren — tárolom.</span>
|
|
</label>
|
|
<div><button class="btn btn-primary" id="finish-btn" disabled onclick="finishWizard()">Befejezés</button></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Void / unclaimed state -->
|
|
<div class="settings-card" id="void-card" style="display:none">
|
|
<div class="alert alert-warning">A kód létrejött, de nem lett megjelenítve — biztonsági okból újra nem kérhető le.
|
|
Indítsa újra a folyamatot: az új kód a régit érvényteleníti.</div>
|
|
<a href="/backup/escrow" class="btn btn-sm btn-primary">Újraindítás</a>
|
|
</div>
|
|
|
|
<script>
|
|
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
|
|
|
|
// Hungarian labels for the agent preflight row ids.
|
|
var PF_LABELS = {
|
|
pbs_storage_id: 'Mentési tároló (PBS) beállítva',
|
|
dr_tier: 'Helyreállítási (DR) szint aktív',
|
|
age_binary: 'Titkosító eszköz telepítve',
|
|
hub_upload: 'Központi feltöltés beállítva',
|
|
staged_secret: 'Távoli mentés jelszava előkészítve',
|
|
sudo_grant: 'Rendszerjogosultság engedélyezve'
|
|
};
|
|
|
|
// Hungarian DETAIL mapping (v0.127.1): the agent's `detail` strings are operator-English by
|
|
// design — presentation is the controller's job. OK rows keep only VALUE-bearing details
|
|
// (storage id / binary path); boolean-OK rows render no detail at all. Not-OK rows get the
|
|
// Hungarian explanation; unknown/future ids fall back to the raw detail (never blank a failure).
|
|
var PF_FAIL_DETAILS = {
|
|
pbs_storage_id: 'nincs beállítva mentési tároló',
|
|
age_binary: 'a titkosító eszköz (age) nem található',
|
|
dr_tier: 'a DR-szint nincs alkalmazva ezen a gépen',
|
|
hub_upload: 'a központi feltöltés nincs beállítva',
|
|
sudo_grant: 'a rendszerjogosultság hiányzik — az ügynök frissítése szükséges',
|
|
staged_secret: 'nincs előkészített jelszó — a varázsló indításkor automatikusan előkészíti, ha a távoli mentés be van állítva'
|
|
};
|
|
function pfDetail(it){
|
|
if(it.ok){
|
|
if(it.id==='pbs_storage_id' || it.id==='age_binary'){ return it.detail||''; } // the VALUE
|
|
if(it.id==='staged_secret'){ return 'előkészítve'; }
|
|
return ''; // dr_tier / hub_upload / sudo_grant: the check mark + label is complete
|
|
}
|
|
return PF_FAIL_DETAILS[it.id] || (it.detail||'');
|
|
}
|
|
|
|
async function loadPreflight(){
|
|
try{
|
|
var r = await fetch('/api/escrow/preflight'); var j = await r.json();
|
|
if(!j.ok && !j.data){ throw new Error(j.error||'Hiba'); }
|
|
var d = j.data||{};
|
|
var html='<ul style="list-style:none;padding:0;margin:0">';
|
|
(d.items||[]).forEach(function(it){
|
|
// staged_secret is informational: the controller re-stages before every run when offsite is
|
|
// configured, so a red row there would be noise — render it neutral when not ok.
|
|
var informational = it.id==='staged_secret';
|
|
var mark = it.ok ? '<span style="color:var(--blue-bright)">✓</span>'
|
|
: (informational ? '<span style="color:var(--text-3)">•</span>'
|
|
: '<span style="color:var(--crit)">✗</span>');
|
|
var det = pfDetail(it);
|
|
var line = mark+' '+esc(PF_LABELS[it.id]||it.id);
|
|
if(det){ line += ' <span class="form-hint">— '+esc(det)+'</span>'; }
|
|
// On failure keep the agent's raw detail as a muted diagnostic (English acceptable there) —
|
|
// unless the fallback above already IS the raw detail (unknown id).
|
|
if(!it.ok && it.detail && det!==it.detail){ line += ' <span class="form-hint" style="color:var(--text-3)">('+esc(it.detail)+')</span>'; }
|
|
html+='<li style="padding:.35rem 0;border-bottom:1px solid var(--line-soft)">'+line+'</li>';
|
|
});
|
|
if(!d.agent_supported){
|
|
html+='<li style="padding:.35rem 0"><span style="color:var(--crit)">✗</span> A funkcióhoz az ügynök frissítése szükséges — a frissítés automatikusan megérkezik.</li>';
|
|
}
|
|
html+='</ul>';
|
|
document.getElementById('pf-list').innerHTML=html;
|
|
if(d.ok){
|
|
document.getElementById('warn-card').style.display='block';
|
|
} else {
|
|
document.getElementById('pf-list').insertAdjacentHTML('beforeend',
|
|
'<p class="form-hint" style="margin-top:.75rem">A folyamat a piros feltételek teljesüléséig nem indítható.</p>');
|
|
}
|
|
}catch(e){
|
|
var el=document.getElementById('pf-error'); el.style.display='block';
|
|
el.textContent='Az előfeltételek ellenőrzése nem sikerült: '+e.message;
|
|
}
|
|
}
|
|
|
|
async function startCeremony(ev){
|
|
ev.preventDefault();
|
|
var btn=document.getElementById('start-btn'); btn.disabled=true;
|
|
var errEl=document.getElementById('start-error'); errEl.style.display='none';
|
|
try{
|
|
var body=new URLSearchParams({password:document.getElementById('reauth-password').value});
|
|
var r=await fetch('/api/escrow/start',{method:'POST',
|
|
headers:Object.assign({'Content-Type':'application/x-www-form-urlencoded'},csrfHeaders()),body:body});
|
|
var j=await r.json();
|
|
if(!j.ok){ errEl.textContent=j.error||'A folyamat indítása nem sikerült.'; errEl.style.display='block'; btn.disabled=false; return false; }
|
|
document.getElementById('reauth-password').value='';
|
|
document.getElementById('warn-card').style.display='none';
|
|
document.getElementById('progress-card').style.display='block';
|
|
pollStatus();
|
|
}catch(e){ errEl.textContent='Hiba: '+e.message; errEl.style.display='block'; btn.disabled=false; }
|
|
return false;
|
|
}
|
|
|
|
var pollTimer=null;
|
|
function pollStatus(){
|
|
pollTimer=setInterval(async function(){
|
|
try{
|
|
var r=await fetch('/api/escrow/status'); var j=await r.json();
|
|
var d=(j&&j.data)||{};
|
|
if(d.phase==='done'){
|
|
clearInterval(pollTimer);
|
|
document.getElementById('progress-card').style.display='none';
|
|
if(d.claimable){ document.getElementById('reveal-card').style.display='block'; }
|
|
else { document.getElementById('void-card').style.display='block'; }
|
|
} else if(d.phase==='failed'){
|
|
clearInterval(pollTimer);
|
|
var el=document.getElementById('progress-error'); el.style.display='block';
|
|
el.textContent='A létrehozás nem sikerült.'+(d.detail?(' Részletek: '+d.detail):'');
|
|
} else if(d.phase==='unclaimed_void' || d.phase==='none'){
|
|
clearInterval(pollTimer);
|
|
document.getElementById('progress-card').style.display='none';
|
|
document.getElementById('void-card').style.display='block';
|
|
}
|
|
}catch(e){ /* transient poll error — keep polling */ }
|
|
}, 2000);
|
|
}
|
|
|
|
// The claimed code lives ONLY in this closure variable + the #code-block textContent, until
|
|
// finishWizard() clears both. It never leaves the page's JS scope.
|
|
var claimedR='';
|
|
var verifyIdx=[0,0];
|
|
|
|
async function claimCode(){
|
|
var btn=document.getElementById('reveal-btn'); btn.disabled=true;
|
|
var errEl=document.getElementById('reveal-error'); errEl.style.display='none';
|
|
try{
|
|
var r=await fetch('/api/escrow/claim',{method:'POST',headers:csrfHeaders()});
|
|
var j=await r.json();
|
|
if(r.status===410){ document.getElementById('reveal-card').style.display='none'; document.getElementById('void-card').style.display='block'; return; }
|
|
if(!j.ok || !j.data || !j.data.recovery_code){ errEl.textContent=j.error||'A kód lekérése nem sikerült.'; errEl.style.display='block'; btn.disabled=false; return; }
|
|
claimedR=j.data.recovery_code;
|
|
// Typed-back positions FIRST (v0.127.1: the rendering below highlights them): two random
|
|
// distinct 1-based word positions of the dash-separated code.
|
|
var words=claimedR.split('-');
|
|
var n=words.length;
|
|
var a=1+Math.floor(Math.random()*n), b=1+Math.floor(Math.random()*n);
|
|
while(b===a){ b=1+Math.floor(Math.random()*n); }
|
|
verifyIdx=[Math.min(a,b),Math.max(a,b)];
|
|
// Span-per-word DOM build — createElement + textContent + createTextNode ONLY. R never flows
|
|
// through innerHTML/insertAdjacentHTML: with no HTML parse context there is no injection
|
|
// surface, the same guarantee the old single textContent assignment gave. The two words the
|
|
// customer must type back get the warn highlight so they can be found on paper too.
|
|
var block=document.getElementById('code-block');
|
|
block.textContent='';
|
|
words.forEach(function(w,i){
|
|
if(i>0){ block.appendChild(document.createTextNode('-')); }
|
|
var sp=document.createElement('span');
|
|
sp.textContent=w;
|
|
if(i+1===verifyIdx[0] || i+1===verifyIdx[1]){ sp.style.color='var(--warn)'; sp.style.fontWeight='600'; }
|
|
block.appendChild(sp);
|
|
});
|
|
document.getElementById('reveal-gate').style.display='none';
|
|
document.getElementById('reveal-shown').style.display='block';
|
|
document.getElementById('verify-label').textContent='Megerősítés: írja be a kód '+verifyIdx[0]+'. és '+verifyIdx[1]+'. szavát';
|
|
}catch(e){ errEl.textContent='Hiba: '+e.message; errEl.style.display='block'; btn.disabled=false; }
|
|
}
|
|
|
|
// Manual hide toggle (v0.127.2 — Viktor's live finding reversed the v0.127.1 auto-blur: blurring
|
|
// the code the moment a verification input gets focus made typing the words HARDER, since most
|
|
// customers type from the screen. The code now stays visible; "Elrejtés" is offered from the
|
|
// reveal onward for when hiding is wanted, e.g. a screen share). R stays in the JS closure;
|
|
// the blur is pure presentation.
|
|
var codeBlurred=false;
|
|
function setCodeBlur(on){
|
|
codeBlurred=on;
|
|
document.getElementById('code-block').style.filter=on?'blur(6px)':'';
|
|
document.getElementById('code-blur-toggle').textContent=on?'Megjelenítés':'Elrejtés';
|
|
}
|
|
function toggleCodeBlur(){ setCodeBlur(!codeBlurred); }
|
|
|
|
function verifyWords(){
|
|
var words=claimedR.split('-');
|
|
var w1=(document.getElementById('verify-w1').value||'').trim().toLowerCase();
|
|
var w2=(document.getElementById('verify-w2').value||'').trim().toLowerCase();
|
|
var ok = w1===words[verifyIdx[0]-1] && w2===words[verifyIdx[1]-1];
|
|
document.getElementById('verify-error').style.display=ok?'none':'block';
|
|
if(ok){ document.getElementById('finish-block').style.display='block'; }
|
|
}
|
|
|
|
function finishWizard(){
|
|
// Drop every copy of the code this page holds, then leave. (Assigning textContent replaces ALL
|
|
// child nodes with one empty text node — the per-word spans are removed with it, verified.)
|
|
claimedR='';
|
|
document.getElementById('code-block').textContent='';
|
|
window.location='/backups/remote';
|
|
}
|
|
|
|
loadPreflight();
|
|
</script>
|
|
|
|
{{end}}
|
|
{{template "layout_end" .}}
|
|
{{end}}
|