stackchain-dashboard/frontend/security-center.js
timmy 3896f17a58
All checks were successful
CI / lint (pull_request) Successful in 1m28s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped
feat: authorize and audit comment deletion (Closes #779)
2026-08-13 22:55:47 +00:00

191 lines
9.3 KiB
JavaScript

(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory;
else root.attachSecurityCenter = boundary => factory({ root, boundary });
})(typeof window !== 'undefined' ? window : this, function attachSecurityCenter({ root, boundary }) {
const devicesButton = root.document.getElementById('active-devices');
const devicesSheet = root.document.getElementById('active-devices-sheet');
const devicesList = root.document.getElementById('active-devices-list');
const devicesStatus = root.document.getElementById('active-devices-status');
const passkeysList = root.document.getElementById('enrolled-passkeys-list');
const passkeysStatus = root.document.getElementById('enrolled-passkeys-status');
const enrollPasskey = root.document.getElementById('enroll-passkey');
const activityList = root.document.getElementById('security-activity-list');
const activityStatus = root.document.getElementById('security-activity-status');
const loadMoreActivity = root.document.getElementById('load-more-security-activity');
let activityCursor = null;
const renderDevices = async () => {
devicesStatus.textContent = 'Loading active devices…';
devicesList.replaceChildren();
try {
const devices = await boundary.listActiveDevices();
devices.forEach(device => {
const row = root.document.createElement('article');
row.className = 'active-device';
const details = root.document.createElement('div');
const label = root.document.createElement('strong');
label.textContent = device.device_label;
const timing = root.document.createElement('span');
timing.className = 'small muted';
timing.textContent = `Signed in ${new Date(device.created_at * 1000).toLocaleString()} · expires ${new Date(device.expires_at * 1000).toLocaleString()}`;
details.append(label, timing);
if (device.current) {
const current = root.document.createElement('span');
current.className = 'active-device-current';
current.textContent = 'This device';
details.append(current);
}
const revoke = root.document.createElement('button');
revoke.type = 'button';
revoke.textContent = device.current ? 'Sign out' : 'Revoke';
revoke.addEventListener('click', async () => {
if (device.current) await boundary.signOut();
else if (await boundary.revokeActiveDevice(device)) await renderDevices();
});
row.append(details, revoke);
devicesList.append(row);
});
devicesStatus.textContent = devices.length ? `${devices.length} active device${devices.length === 1 ? '' : 's'}` : 'No active devices.';
} catch (_error) {
devicesStatus.textContent = 'Active devices could not be loaded. Try again.';
}
};
const renderPasskeys = async () => {
if (!passkeysList || !passkeysStatus) return;
passkeysStatus.textContent = 'Loading enrolled passkeys…';
passkeysList.replaceChildren();
try {
const enrolled = await boundary.listPasskeys();
enrolled.forEach(passkey => {
const row = root.document.createElement('article');
row.className = 'enrolled-passkey';
const details = root.document.createElement('div');
const label = root.document.createElement('strong');
label.textContent = passkey.device_label;
const timing = root.document.createElement('span');
timing.className = 'small muted';
const state = passkey.current ? 'This active device' : (passkey.active ? 'Active device' : 'No active session');
timing.textContent = `Enrolled ${new Date(passkey.created_at * 1000).toLocaleString()} · ${state}`;
details.append(label, timing);
const remove = root.document.createElement('button');
remove.type = 'button';
remove.textContent = 'Remove passkey';
remove.addEventListener('click', async () => {
remove.disabled = true;
passkeysStatus.textContent = `Removing passkey for ${passkey.device_label}`;
try {
const outcome = await boundary.revokePasskey(passkey);
if (outcome) {
passkeysStatus.textContent = outcome.current_session
? 'Passkey removed. This session remains active; keep your recovery token available.'
: 'Passkey removed.';
await renderPasskeys();
if (outcome.session_revoked) await renderDevices();
} else {
remove.disabled = false;
passkeysStatus.textContent = `${enrolled.length} enrolled passkey${enrolled.length === 1 ? '' : 's'}`;
}
} catch (_error) {
passkeysStatus.textContent = 'Passkey could not be removed. Refresh to verify before retrying.';
remove.disabled = false;
}
});
row.append(details, remove);
passkeysList.append(row);
});
passkeysStatus.textContent = enrolled.length
? `${enrolled.length} enrolled passkey${enrolled.length === 1 ? '' : 's'}`
: 'No passkeys enrolled.';
} catch (_error) {
passkeysStatus.textContent = 'Enrolled passkeys could not be loaded. Try again.';
}
};
const renderSecurityActivity = async (append = false) => {
if (!activityList || !activityStatus || !loadMoreActivity) return;
activityStatus.textContent = append ? 'Loading older activity…' : 'Loading security activity…';
loadMoreActivity.hidden = true;
if (!append) {
activityCursor = null;
activityList.replaceChildren();
}
try {
const page = await boundary.listSecurityEvents(append ? activityCursor : null);
if (!append) {
page.authentication_alerts.forEach(alert => {
const row = root.document.createElement('article');
row.className = 'security-event authentication-alert';
const formatted = boundary.formatAuthenticationAlert(alert);
const title = root.document.createElement('strong');
title.textContent = formatted.title;
const details = root.document.createElement('span');
details.className = 'small muted';
details.textContent = formatted.detail;
row.append(title, details);
activityList.append(row);
});
}
const labels = {
sign_in: 'Signed in', sign_out: 'Signed out', passkey_enrolled: 'Passkey enrolled',
passkey_counter_anomaly: 'Passkey counter anomaly',
device_revoked: 'Device access revoked', all_sessions_revoked: 'All device access revoked',
issue_closed: 'Issue closed', pull_merged: 'Pull request merged',
comment_deleted: 'Comment deleted',
pull_review_approved: 'Pull request approved',
pull_review_changes_requested: 'Changes requested', gitea_time_logged: 'Gitea time logged',
};
page.events.forEach(event => {
const row = root.document.createElement('article');
row.className = 'security-event';
const title = root.document.createElement('strong');
title.textContent = labels[event.kind] || 'Security event';
const details = root.document.createElement('span');
details.className = 'small muted';
const context = [event.device_label, event.method, event.target,
event.kind === 'passkey_counter_anomaly'
? 'Remove and re-enroll this passkey if the alert repeats'
: null,
event.status === 'pending' ? 'Outcome confirmation pending' : null]
.filter(value => typeof value === 'string' && value).join(' · ');
details.textContent = `${new Date(event.created_at * 1000).toLocaleString()}${context ? ' · ' + context : ''}`;
row.append(title, details);
activityList.append(row);
});
activityCursor = page.next_cursor;
activityStatus.textContent = activityList.children.length
? `${activityList.children.length} recent security event${activityList.children.length === 1 ? '' : 's'}`
: 'No security activity yet.';
loadMoreActivity.textContent = 'Load older activity';
loadMoreActivity.hidden = !activityCursor;
} catch (_error) {
activityStatus.textContent = 'Security activity could not be loaded.';
loadMoreActivity.textContent = 'Retry activity';
loadMoreActivity.hidden = false;
}
};
const open = () => {
if (!devicesSheet) return;
devicesSheet.hidden = false;
root.document.getElementById('close-active-devices')?.focus();
return Promise.all([renderDevices(), renderPasskeys(), renderSecurityActivity()]);
};
devicesButton?.addEventListener('click', open);
loadMoreActivity?.addEventListener('click', () => renderSecurityActivity(Boolean(activityCursor)));
enrollPasskey?.addEventListener('click', async () => {
enrollPasskey.disabled = true;
devicesStatus.textContent = 'Waiting for your device passkey…';
try {
await boundary.enrollPasskey();
devicesStatus.textContent = 'Passkey enrolled. You can use it at sign-in and authorization prompts.';
await renderPasskeys();
} catch (_error) {
devicesStatus.textContent = 'Passkey enrollment was not completed. Try again.';
} finally {
enrollPasskey.disabled = false;
}
});
return { open, renderDevices, renderPasskeys, renderSecurityActivity };
});