feat(portal): reuse unlock code for protected downloads

This commit is contained in:
DeCentN2Madness authored and AIAMarketing committed 2026-11-05 19:21:26 -07:00
1 parent 475dd3379b
commit 596246f20a
9 files changed
+268 -13

No files matched your search

+52 -2
View File
@@ -1,4 +1,5 @@
import { decryptPortalStream, validatePortalPayload } from '../utils/portal-crypto.mjs';
import { fetchProtectedPortalResource, portalDownloadErrorMessage, portalRoleForStreamId, protectedPortalResource } from '../utils/portal-download.mjs';
import { portalLinkPresentation } from '../utils/portal-presentation';
/** Build a passive DOM, never inject executable decrypted HTML. Remote media,
@@ -24,6 +25,8 @@ export function safePortalContent(html: string): DocumentFragment {
if (url.origin !== location.origin && url.protocol === 'https:') element.setAttribute('target', '_blank');
const presentation = portalLinkPresentation([...source.classList], source.style.backgroundColor, url.protocol, source.textContent ?? '');
if (presentation) element.className = presentation;
const resource = protectedPortalResource(url.href, location.origin);
if (resource) element.dataset.portalResourceRole = resource.role;
}
} catch { /* Invalid destination remains visible text without navigation. */ }
}
@@ -99,6 +102,9 @@ function initialize() {
const feedback = root.querySelector<HTMLElement>('[data-portal-feedback]')!;
const prompt = root.querySelector<HTMLElement>('[data-portal-prompt]')!;
const content = root.querySelector<HTMLElement>('[data-portal-content]')!;
const downloadFeedback = root.querySelector<HTMLElement>('[data-portal-download-feedback]')!;
let credential: { role: 'staff' | 'management'; code: string } | null = null;
let downloading = false;
let data: ReturnType<typeof validatePortalPayload>;
try {
if (!crypto.subtle) throw new Error();
@@ -106,6 +112,45 @@ function initialize() {
input.disabled = false; button.disabled = false;
feedback.textContent = '';
} catch { feedback.textContent = 'Portal access is unavailable in this preview. Please contact AIA.'; return; }
const clearSession = () => {
credential = null;
input.value = '';
content.replaceChildren();
content.hidden = true;
prompt.hidden = false;
downloadFeedback.hidden = true;
downloadFeedback.textContent = '';
};
const saveDownload = (blob: Blob, filename: string) => {
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl; link.download = filename; link.hidden = true;
document.body.append(link); link.click(); link.remove();
setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
};
content.addEventListener('click', (event) => {
const target = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[data-portal-resource-role]') : null;
if (!target) return;
event.preventDefault();
if (downloading) return;
const resource = protectedPortalResource(target.href, location.origin);
if (!resource || !credential) {
downloadFeedback.textContent = portalDownloadErrorMessage(undefined);
downloadFeedback.hidden = false;
return;
}
downloading = true;
target.setAttribute('aria-disabled', 'true');
downloadFeedback.textContent = 'Downloading document…';
downloadFeedback.hidden = false;
void fetchProtectedPortalResource(resource, credential)
.then(({ blob, filename }) => {
saveDownload(blob, filename);
downloadFeedback.textContent = `Downloaded ${filename}.`;
})
.catch((error) => { downloadFeedback.textContent = portalDownloadErrorMessage(error); })
.finally(() => { downloading = false; target.removeAttribute('aria-disabled'); });
});
const unlock = async () => {
if (button.disabled) return;
button.disabled = true;
@@ -116,8 +161,12 @@ function initialize() {
for (const stream of data.streams) {
const plain = await decryptPortalStream(stream, pass);
if (plain !== null) {
const role = portalRoleForStreamId(stream.id);
if (!role) throw new Error('Unknown portal stream');
credential = { role, code: pass };
content.replaceChildren(safePortalContent(plain));
prompt.hidden = true; content.hidden = false;
downloadFeedback.hidden = true; downloadFeedback.textContent = '';
input.value = ''; content.focus(); return;
}
}
@@ -127,8 +176,9 @@ function initialize() {
};
button.addEventListener('click', unlock);
input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); void unlock(); } });
// BFCache must not restore an already unlocked view.
addEventListener('pagehide', () => { input.value = ''; content.replaceChildren(); content.hidden = true; prompt.hidden = false; });
// Credentials exist only in this page closure and are discarded on exit/swap.
addEventListener('pagehide', clearSession, { once: true });
document.addEventListener('astro:before-swap', clearSession, { once: true });
}
initialize();
document.addEventListener('astro:page-load', initialize);