feat(portal): add service drawers and employee portal preview

This commit is contained in:
DeCentN2Madness committed 2026-10-30 00:03:21 -07:00
1 parent ff80ee516f
commit e97f0ea9b4
39 files changed
+1446 -130

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import { decryptPortalStream, validatePortalPayload } from '../utils/portal-crypto.mjs';
/** Build a passive DOM, never inject executable decrypted HTML. Remote media,
* embedded forms and styles require owner review and are not loaded on unlock. */
export function safePortalContent(html: string): DocumentFragment {
const template = document.createElement('template');
template.innerHTML = html;
const allowed = new Set(['DIV','SECTION','ARTICLE','HEADER','FOOTER','H1','H2','H3','H4','H5','H6','P','SPAN','STRONG','EM','B','I','U','SMALL','BR','HR','UL','OL','LI','DL','DT','DD','TABLE','THEAD','TBODY','TR','TH','TD','CAPTION','A']);
const output = document.createDocumentFragment();
const copy = (source: Node, target: Node) => {
if (source.nodeType === Node.TEXT_NODE) { target.appendChild(document.createTextNode(source.textContent ?? '')); return; }
if (!(source instanceof HTMLElement)) return;
if (!allowed.has(source.tagName)) return;
const element = document.createElement(source.tagName.toLowerCase());
if (source.tagName === 'A') {
const href = source.getAttribute('href') ?? '';
try {
const url = new URL(href, location.origin);
if (['https:', 'mailto:', 'tel:'].includes(url.protocol) || (url.origin === location.origin && url.protocol === location.protocol)) {
element.setAttribute('href', url.href);
element.setAttribute('rel', 'noopener noreferrer');
element.setAttribute('referrerpolicy', 'no-referrer');
if (url.origin !== location.origin && url.protocol === 'https:') element.setAttribute('target', '_blank');
}
} catch { /* Invalid destination remains visible text without navigation. */ }
}
for (const child of source.childNodes) copy(child, element);
target.appendChild(element);
};
for (const child of template.content.childNodes) copy(child, output);
return output;
}
function initialize() {
const root = document.querySelector<HTMLElement>('[data-employee-portal]');
if (!root || root.dataset.bound) return;
root.dataset.bound = 'true';
const input = root.querySelector<HTMLInputElement>('[data-portal-code]')!;
const button = root.querySelector<HTMLButtonElement>('button')!;
const feedback = root.querySelector<HTMLElement>('[data-portal-feedback]')!;
const prompt = root.querySelector<HTMLElement>('[data-portal-prompt]')!;
const content = root.querySelector<HTMLElement>('[data-portal-content]')!;
let data: ReturnType<typeof validatePortalPayload>;
try {
if (!crypto.subtle) throw new Error();
data = validatePortalPayload(JSON.parse(root.querySelector('[data-portal-payload]')?.textContent ?? 'null'));
input.disabled = false; button.disabled = false;
feedback.textContent = '';
} catch { feedback.textContent = 'Portal access is unavailable in this preview. Please contact AIA.'; return; }
const unlock = async () => {
if (button.disabled) return;
button.disabled = true;
input.setAttribute('aria-invalid', 'false');
feedback.textContent = '';
let pass = input.value;
try {
for (const stream of data.streams) {
const plain = await decryptPortalStream(stream, pass);
if (plain !== null) {
content.replaceChildren(safePortalContent(plain));
prompt.hidden = true; content.hidden = false;
input.value = ''; content.focus(); return;
}
}
input.setAttribute('aria-invalid', 'true'); feedback.textContent = 'Invalid Code'; input.focus();
} catch { feedback.textContent = 'Portal access is unavailable. Please try again or contact AIA.'; }
finally { pass = ''; button.disabled = false; }
};
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; });
}
initialize();
document.addEventListener('astro:page-load', initialize);