v0.127.0: customer-facing escrow ceremony wizard (/backup/escrow) + Scenario-F stale-blob re-check — one-shot R reveal (no-store, typed-back), re-stage-first start order, agent version gate (MinAgent 0.88.0), escrowed-state hash re-check with card warning (never flips, never blocks); manual-confirm button removed (endpoint stays deprecated)
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
{{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 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>
|
||||
<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>
|
||||
<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'
|
||||
};
|
||||
|
||||
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>');
|
||||
html+='<li style="padding:.35rem 0;border-bottom:1px solid var(--line-soft)">'+mark+' '
|
||||
+esc(PF_LABELS[it.id]||it.id)
|
||||
+(it.detail?(' <span class="form-hint">— '+esc(it.detail)+'</span>'):'')+'</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;
|
||||
document.getElementById('code-block').textContent=claimedR; // textContent — never innerHTML
|
||||
document.getElementById('reveal-gate').style.display='none';
|
||||
document.getElementById('reveal-shown').style.display='block';
|
||||
// Typed-back: two random distinct 1-based word positions of the dash-separated code.
|
||||
var n=claimedR.split('-').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)];
|
||||
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; }
|
||||
}
|
||||
|
||||
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.
|
||||
claimedR='';
|
||||
document.getElementById('code-block').textContent='';
|
||||
window.location='/backups/remote';
|
||||
}
|
||||
|
||||
loadPreflight();
|
||||
</script>
|
||||
|
||||
{{end}}
|
||||
{{template "layout_end" .}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user