feat(hub): v0.69.0 — customer DELETE is the guided full-teardown cascade (R-25b)
POST /configs/{id}/delete now runs hosts -> RESET -> purge behind three
acknowledgements, a typed customer-id, a stale-preview check and the
ONLINE-host refusal (every gate before any write, so a refusal has zero
side effects). The shallow handleConfigDelete is gone.
Two invariants are asserted, not just commented: ruling 3 is preserved by
construction (leg 2 never sees a host row) and retained escrow custody is
purged exactly once, in leg 3 (leg 2 runs with purgeEscrow=false).
handleCustomerReset's committed half was extracted as commitCustomerReset;
the standalone RESET path is byte-identical to v0.68.1 and its suite is
untouched. Five red-proofs run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J55BQE1gE2V4ffud5jweGS
This commit is contained in:
@@ -816,7 +816,7 @@
|
||||
Customer Info header — endpoints and confirm() handlers unchanged. -->
|
||||
<section class="card">
|
||||
<h2>Danger zone</h2>
|
||||
<p class="text-muted">Blocking hides the customer from the Dashboard (reports are still accepted); deleting removes the managed configuration permanently — and permanently removes the retained recovery-key custody (escrow blobs) for this customer's hosts. This is the one true purge point; host deletion only demotes custody, never destroys it.</p>
|
||||
<p class="text-muted">Blocking hides the customer from the Dashboard (reports are still accepted). <strong>Delete customer</strong> is the full offboarding teardown (v0.69.0): it deletes the host(s), then RESETs the customer (offsite repository destroyed, PBS credentials revoked, tunnel and zone removed), then purges the customer record and all escrow ciphertext — including the <strong>retained recovery-key custody</strong> for this customer's hosts. This is the one true purge point; host deletion only demotes custody, never destroys it. Three acknowledgements and the typed customer-id are required. For identity-preserving re-onboarding use <em>Ügyfél-visszaállítás (RESET)</em> above instead.</p>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">
|
||||
{{if .IsBlocked}}
|
||||
<form method="POST" action="/customers/{{.CustomerID}}/unblock" style="display:inline">
|
||||
@@ -829,12 +829,109 @@
|
||||
<button type="submit" class="btn btn-outline btn-sm" data-confirm="Block this customer? They will be hidden from the Dashboard.">Block</button>
|
||||
</form>
|
||||
{{end}}
|
||||
<form method="POST" action="/configs/{{.CustomerID}}/delete" style="display:inline">
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="customerDeleteOpen('{{.CustomerID}}')">Delete customer…</button>
|
||||
</div>
|
||||
|
||||
<!-- Guided full-teardown cascade (v0.69.0, R-25b). The inventory panel is fetched from
|
||||
GET /configs/{id}/delete; the three acknowledgements + typed customer-id are ALSO
|
||||
enforced server-side (this is convenience, never the gate). -->
|
||||
<div id="cust-del-box-{{.CustomerID}}" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid var(--crit); border-radius: var(--radius); max-width: 52em;">
|
||||
<p id="cust-del-inv-{{.CustomerID}}" style="margin: 0 0 0.75rem; font-size: 0.9em;">…</p>
|
||||
<div id="cust-del-journal-{{.CustomerID}}" style="display: none; margin: 0 0 0.75rem; padding: 0.5rem; border: 1px solid var(--warn); font-size: 0.85em;"></div>
|
||||
<label style="display: block; margin: 0 0 0.5rem; font-size: 0.85em;">
|
||||
<input type="checkbox" id="cust-del-ack1-{{.CustomerID}}">
|
||||
<strong>1.</strong> <span id="cust-del-ack1-text-{{.CustomerID}}">The host(s) will be deleted</span> — recovery-key custody is <strong>demoted</strong> to retained custody, not destroyed.
|
||||
</label>
|
||||
<label style="display: block; margin: 0 0 0.5rem; font-size: 0.85em;">
|
||||
<input type="checkbox" id="cust-del-ack2-{{.CustomerID}}">
|
||||
<strong>2.</strong> The customer will be <strong>RESET</strong> — the offsite repository is <strong>DESTROYED</strong>, PBS credentials are revoked, tunnel and zone are removed.
|
||||
</label>
|
||||
<label style="display: block; margin: 0 0 0.75rem; font-size: 0.85em; color: var(--crit);">
|
||||
<input type="checkbox" id="cust-del-ack3-{{.CustomerID}}">
|
||||
<strong>3.</strong> The customer record and <strong>ALL escrow ciphertext</strong> are <strong>PURGED</strong> — unrecoverable.
|
||||
</label>
|
||||
<p style="margin: 0 0 0.4rem; font-size: 0.85em; color: var(--text-2);">Type the customer-id to confirm:</p>
|
||||
<form method="POST" action="/configs/{{.CustomerID}}/delete" id="cust-del-form-{{.CustomerID}}" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
|
||||
{{.CSRFField}}
|
||||
<button type="submit" class="btn btn-danger btn-sm" data-confirm="Delete configuration for {{.CustomerID}}? This cannot be undone.">Delete</button>
|
||||
<input type="hidden" name="confirm_id" id="cust-del-cid-{{.CustomerID}}" value="">
|
||||
<input type="hidden" name="ack_hosts" id="cust-del-h1-{{.CustomerID}}" value="">
|
||||
<input type="hidden" name="ack_reset" id="cust-del-h2-{{.CustomerID}}" value="">
|
||||
<input type="hidden" name="ack_purge" id="cust-del-h3-{{.CustomerID}}" value="">
|
||||
<input type="hidden" name="expect_hosts" id="cust-del-exp-{{.CustomerID}}" value="">
|
||||
<input type="text" id="cust-del-input-{{.CustomerID}}" placeholder="customer-id…" style="padding: 0.3em 0.5em; width: 16em;">
|
||||
<button type="button" class="btn btn-danger btn-sm" id="cust-del-go-{{.CustomerID}}" onclick="customerDeleteSubmit('{{.CustomerID}}')">Confirm & delete everything</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('cust-del-box-{{.CustomerID}}').style.display='none';">Cancel</button>
|
||||
</form>
|
||||
<p id="cust-del-err-{{.CustomerID}}" style="margin: 0.4em 0 0; font-size: 0.8em; color: var(--crit);"></p>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
function customerDeleteOpen(cid) {
|
||||
var box = document.getElementById('cust-del-box-' + cid);
|
||||
var inv = document.getElementById('cust-del-inv-' + cid);
|
||||
var go = document.getElementById('cust-del-go-' + cid);
|
||||
var jr = document.getElementById('cust-del-journal-' + cid);
|
||||
box.style.display = 'block';
|
||||
inv.textContent = 'Loading inventory…';
|
||||
jr.style.display = 'none';
|
||||
document.getElementById('cust-del-input-' + cid).value = '';
|
||||
document.getElementById('cust-del-err-' + cid).textContent = '';
|
||||
document.getElementById('cust-del-exp-' + cid).value = '';
|
||||
['ack1', 'ack2', 'ack3'].forEach(function(a) { document.getElementById('cust-del-' + a + '-' + cid).checked = false; });
|
||||
go.disabled = false;
|
||||
fetch('/configs/' + encodeURIComponent(cid) + '/delete')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
document.getElementById('cust-del-exp-' + cid).value = String(d.host_count);
|
||||
var hostNames = (d.hosts || []).map(function(h){ return h.host_id + ' (' + h.status + ')'; });
|
||||
document.getElementById('cust-del-ack1-text-' + cid).textContent =
|
||||
d.host_count + ' host(s) will be deleted' + (hostNames.length ? ': ' + hostNames.join(', ') : '');
|
||||
var dies = [];
|
||||
if (d.host_count) dies.push(d.host_count + ' host row(s)');
|
||||
if (d.offsite_enabled) dies.push('offsite repository' + (d.offsite_identifier ? ' (' + d.offsite_identifier + ')' : ''));
|
||||
if (d.pbs_tenancy_configured) dies.push('PBS namespace + backups + token');
|
||||
if (d.dr_recipe_present) dies.push('DR recipe');
|
||||
if (d.one_time_secret) dies.push('one-time password');
|
||||
if (d.claim_present) dies.push('claim state');
|
||||
dies.push('customer record');
|
||||
var custody = d.superseded_blobs > 0
|
||||
? d.superseded_blobs + ' retained escrow blob(s) + every current host escrow'
|
||||
: 'every current host escrow';
|
||||
inv.innerHTML = '<strong>Will be destroyed:</strong> ' + dies.join(', ') +
|
||||
'. <strong>Custody:</strong> ' + custody + ' (purged in the final leg). ' +
|
||||
'<strong>Survives:</strong> the audit event stream and the deletion provenance.';
|
||||
if (d.online_host_present) {
|
||||
inv.innerHTML += '<br><strong style="color: var(--crit)">Refused:</strong> a host is ONLINE. ' +
|
||||
'Decommission the box first — the cascade never deletes a live host.';
|
||||
go.disabled = true;
|
||||
}
|
||||
if (d.pending_journal) {
|
||||
var legs = d.pending_journal.legs || {};
|
||||
var parts = Object.keys(legs).map(function(k){ return k + '=' + legs[k]; });
|
||||
jr.innerHTML = '<strong>An earlier cascade stopped mid-way</strong> (journal #' + d.pending_journal.id +
|
||||
', started ' + d.pending_journal.started_at + '). Legs: ' + (parts.length ? parts.join(', ') : 'none recorded') +
|
||||
'. Confirming again RESUMES it — completed legs are no-ops.';
|
||||
jr.style.display = 'block';
|
||||
document.getElementById('cust-del-go-' + cid).textContent = 'Confirm & resume teardown';
|
||||
}
|
||||
})
|
||||
.catch(function(){ inv.textContent = 'Inventory unavailable — the server enforces every gate regardless.'; });
|
||||
}
|
||||
function customerDeleteSubmit(cid) {
|
||||
var err = document.getElementById('cust-del-err-' + cid);
|
||||
var a1 = document.getElementById('cust-del-ack1-' + cid).checked;
|
||||
var a2 = document.getElementById('cust-del-ack2-' + cid).checked;
|
||||
var a3 = document.getElementById('cust-del-ack3-' + cid).checked;
|
||||
if (!a1 || !a2 || !a3) { err.textContent = 'All three acknowledgements are required.'; return; }
|
||||
var typed = document.getElementById('cust-del-input-' + cid).value.trim();
|
||||
if (typed !== cid) { err.textContent = 'The typed customer-id does not match.'; return; }
|
||||
document.getElementById('cust-del-cid-' + cid).value = typed;
|
||||
document.getElementById('cust-del-h1-' + cid).value = '1';
|
||||
document.getElementById('cust-del-h2-' + cid).value = '1';
|
||||
document.getElementById('cust-del-h3-' + cid).value = '1';
|
||||
document.getElementById('cust-del-form-' + cid).submit();
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user