feat: enroll panel proxyHeadersSeen chips on login/app

This commit is contained in:
George Lambert 2026-10-13 21:18:29 -04:00
commit d026f7eeb4
2 changed files with 66 additions and 9 deletions

View file

@ -249,10 +249,12 @@ function drawSecurity() {
<div class="card sec-card" id="sec-device-cert">
<h3>Device certificate (mTLS) <span class="pill off" id="sec-mtls-state">…</span></h3>
<p class="muted" style="margin:0 0 0.5rem;font-size:13px" id="sec-mtls-note">Loading enroll status…</p>
<p class="muted" style="margin:0 0 0.35rem;font-size:11px;font-family:ui-monospace,monospace" id="sec-mtls-updated" hidden></p>
<div id="sec-mtls-proxy" style="display:none;flex-wrap:wrap;gap:0.3rem;margin:0 0 0.35rem"></div>
<p class="muted" style="margin:0 0 0.35rem;font-size:12px" id="sec-mtls-mint" hidden></p>
<p class="muted" style="margin:0;font-size:12px" id="sec-mtls-renew" hidden></p>
<ol id="sec-mtls-next" class="muted" style="margin:0.55rem 0 0;padding-left:1.15rem;font-size:12px;line-height:1.45" hidden></ol>
<p class="muted" style="margin:0.65rem 0 0;font-size:12px">Staff cookie only — never a customer xVerae- key. Cert mint/renew is reverse-proxy PKI (see ENROLL.md).</p>
<p class="muted" style="margin:0.65rem 0 0;font-size:12px">Staff cookie only — never a customer xVerae- key. Cert mint/renew is reverse-proxy PKI (see ENROLL.md). Shows <span class="mono">updatedAt</span> + <span class="mono">proxyHeadersSeen</span> chips.</p>
</div>`;
const setupBtn = $('sec-totp-setup');
if (setupBtn) setupBtn.onclick = () => runTotpSetup('sec-totp-setup-box');
@ -277,12 +279,45 @@ function drawSecurity() {
const mint = $('sec-mtls-mint');
const renew = $('sec-mtls-renew');
const next = $('sec-mtls-next');
const updated = $('sec-mtls-updated');
const proxyBox = $('sec-mtls-proxy');
if (st) {
st.textContent = s.stateLabel || s.state || 'Device cert';
st.className = 'pill' + (s.state === 'enrolled' || s.state === 'enforce_on' ? '' : ' off');
if (s.state === 'pending_proxy') st.className = 'pill warn';
}
if (note) note.textContent = s.note || '';
if (updated) {
if (typeof s.updatedAt === 'string' && s.updatedAt) {
updated.hidden = false;
updated.textContent = 'updatedAt ' + s.updatedAt;
} else {
updated.hidden = true;
updated.textContent = '';
}
}
if (proxyBox) {
const seen = s.proxyHeadersSeen && typeof s.proxyHeadersSeen === 'object' ? s.proxyHeadersSeen : null;
if (seen) {
const keys = ['verify', 'verifyOk', 'dn', 'pem', 'any'];
const chips = keys
.filter((k) => Object.prototype.hasOwnProperty.call(seen, k))
.map((k) => {
const on = !!seen[k];
return `<span class="pill ${on ? '' : 'off'}" style="font-size:11px">${k} ${on ? 'yes' : 'no'}</span>`;
});
if (chips.length) {
proxyBox.style.display = 'flex';
proxyBox.innerHTML = chips.join('');
} else {
proxyBox.style.display = 'none';
proxyBox.innerHTML = '';
}
} else {
proxyBox.style.display = 'none';
proxyBox.innerHTML = '';
}
}
if (mint && s.mint) {
mint.hidden = false;
mint.textContent = 'Mint: ' + s.mint;

View file

@ -136,23 +136,45 @@
if (!s || !s.note) return;
const el = document.getElementById('enroll-status');
el.hidden = false;
const esc = (t) => String(t).replace(/&/g, '&amp;').replace(/</g, '&lt;');
/* Prefer API state copy (not_enrolled|pending_proxy|enrolled|enforce_on) + nextSteps. */
const label = s.stateLabel ? s.stateLabel + ' · ' : '';
const steps = Array.isArray(s.nextSteps)
? s.nextSteps.filter((x) => typeof x === 'string' && x.trim())
: [];
if (steps.length) {
el.innerHTML =
label +
(s.note || '') +
'<ol style="margin:0.45rem 0 0;padding-left:1.1rem">' +
steps
.map((t) => `<li>${String(t).replace(/&/g, '&amp;').replace(/</g, '&lt;')}</li>`)
.join('') +
'</ol>';
} else {
el.textContent = label + (s.note || '');
let html = label + (s.note || '');
if (typeof s.updatedAt === 'string' && s.updatedAt) {
html +=
'<div style="margin:0.35rem 0 0;font-family:ui-monospace,monospace;font-size:0.72rem">updatedAt ' +
esc(s.updatedAt) +
'</div>';
}
const seen = s.proxyHeadersSeen && typeof s.proxyHeadersSeen === 'object' ? s.proxyHeadersSeen : null;
if (seen) {
const keys = ['verify', 'verifyOk', 'dn', 'pem', 'any'];
const chips = keys
.filter((k) => Object.prototype.hasOwnProperty.call(seen, k))
.map((k) => {
const on = !!seen[k];
return (
'<span style="display:inline-block;margin:0.15rem 0.2rem 0 0;padding:0.05rem 0.4rem;border-radius:999px;font-size:0.68rem;font-weight:700;border:1px solid var(--line,#ddd);' +
(on ? 'background:#ecfdf5;color:#047857' : 'background:#f4f1f8;color:#6b6478') +
'">' +
esc(k) +
' ' +
(on ? 'yes' : 'no') +
'</span>'
);
});
if (chips.length) html += '<div style="margin:0.35rem 0 0">' + chips.join('') + '</div>';
}
if (steps.length) {
html +=
'<ol style="margin:0.45rem 0 0;padding-left:1.1rem">' +
steps.map((t) => '<li>' + esc(t) + '</li>').join('') +
'</ol>';
}
el.innerHTML = html;
if (s.state) el.dataset.state = s.state;
})
.catch(() => {});