Fix 8 bugs: XSS, race condition, null safety, form reset, pricing
1. XSS: error messages use textContent by default, innerHTML only for controlled HTML (CORES link) via allowHtml flag 2. XSS: name search errors built with DOM API, not innerHTML 3. Race condition: concurrent FRN lookups cancel prior request via AbortController tracking 4. Null safety: DOM element guards with error logging 5. Null safety: check.detail uses || "" fallback, \n → <br> 6. Quote form: auto-resets after 3 seconds on successful submit 7. Pricing: discount uses Math.round(total*15)/100 for cent precision 8. Future-proofing: parseFloat for prices instead of parseInt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4853f67f5e
commit
6171c64b90
1 changed files with 42 additions and 10 deletions
|
|
@ -202,9 +202,17 @@ import Base from "../../layouts/Base.astro";
|
|||
const errorMessage = document.getElementById("error-message");
|
||||
const resultsEl = document.getElementById("results");
|
||||
|
||||
// Guard against missing DOM elements
|
||||
if (!nameInput || !frnInput || !loadingEl || !errorBox || !errorMessage || !resultsEl) {
|
||||
console.error("[fcc-check] Required DOM elements missing");
|
||||
}
|
||||
|
||||
// Race condition guard — cancel prior fetch when new one starts
|
||||
let currentController = null;
|
||||
|
||||
// --- Name search ---
|
||||
nameSearchBtn.addEventListener("click", runNameSearch);
|
||||
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
|
||||
nameSearchBtn?.addEventListener("click", runNameSearch);
|
||||
nameInput?.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
|
||||
|
||||
async function runNameSearch() {
|
||||
const name = nameInput.value.trim();
|
||||
|
|
@ -258,7 +266,11 @@ import Base from "../../layouts/Base.astro";
|
|||
});
|
||||
});
|
||||
} catch (err) {
|
||||
nameResults.innerHTML = `<p class="text-sm text-red-600">Search error: ${err.message}</p>`;
|
||||
const errP = document.createElement("p");
|
||||
errP.className = "text-sm text-red-600";
|
||||
errP.textContent = "Search error: " + (err.message || "Unknown error");
|
||||
nameResults.innerHTML = "";
|
||||
nameResults.appendChild(errP);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -321,6 +333,15 @@ import Base from "../../layouts/Base.astro";
|
|||
statusEl.textContent = "✓ Request submitted! We'll email you within 1 business day.";
|
||||
statusEl.className = "mt-2 text-xs text-green-700";
|
||||
btn.textContent = "Sent ✓";
|
||||
// Reset form after 3 seconds
|
||||
setTimeout(() => {
|
||||
document.getElementById("quote-form")?.classList.add("hidden");
|
||||
document.getElementById("quote-name").value = "";
|
||||
document.getElementById("quote-email").value = "";
|
||||
btn.textContent = "Request Assessment";
|
||||
btn.disabled = false;
|
||||
statusEl.classList.add("hidden");
|
||||
}, 3000);
|
||||
} else {
|
||||
throw new Error("Failed");
|
||||
}
|
||||
|
|
@ -360,13 +381,20 @@ import Base from "../../layouts/Base.astro";
|
|||
errorBox.classList.add("hidden");
|
||||
loadingEl.classList.remove("hidden");
|
||||
|
||||
// Cancel any prior in-flight request
|
||||
if (currentController) currentController.abort();
|
||||
const controller = new AbortController();
|
||||
currentController = controller;
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
const res = await fetch(`${API}/api/v1/fcc/lookup?frn=${frn}`, { signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
|
||||
// Ignore if a newer request superseded this one
|
||||
if (currentController !== controller) return;
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (res.status === 400) {
|
||||
|
|
@ -376,7 +404,7 @@ import Base from "../../layouts/Base.astro";
|
|||
}
|
||||
const data = await res.json();
|
||||
if (!data.entity_name && !data.cores?.entity_name && !data.filer_499) {
|
||||
showError('FRN ' + frn + ' was not found in any FCC database. Verify your number at <a href="https://apps.fcc.gov/coresWeb/publicHome.do" target="_blank" style="color:#1e40af;text-decoration:underline;">FCC CORES</a>.');
|
||||
showError('FRN ' + frn + ' was not found in any FCC database. Verify your number at <a href="https://apps.fcc.gov/coresWeb/publicHome.do" target="_blank" style="color:#1e40af;text-decoration:underline;">FCC CORES</a>.', true);
|
||||
return;
|
||||
}
|
||||
renderResults(data);
|
||||
|
|
@ -391,8 +419,12 @@ import Base from "../../layouts/Base.astro";
|
|||
}
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorMessage.innerHTML = msg;
|
||||
function showError(msg, allowHtml) {
|
||||
if (allowHtml) {
|
||||
errorMessage.innerHTML = msg;
|
||||
} else {
|
||||
errorMessage.textContent = msg;
|
||||
}
|
||||
errorBox.classList.remove("hidden");
|
||||
}
|
||||
|
||||
|
|
@ -684,7 +716,7 @@ import Base from "../../layouts/Base.astro";
|
|||
function attachF499Handlers() {
|
||||
card.querySelector(".f499-yes")?.addEventListener("click", () => {
|
||||
showF499Result(
|
||||
`${check.detail}\n${eName} had telecom revenue — filing is required for each missed year.`,
|
||||
`${check.detail || ""}<br>${eName} had telecom revenue — filing is required for each missed year.`,
|
||||
"red"
|
||||
);
|
||||
});
|
||||
|
|
@ -808,7 +840,7 @@ import Base from "../../layouts/Base.astro";
|
|||
|
||||
boxes.forEach((cb) => {
|
||||
if (cb.checked) {
|
||||
const p = parseInt(cb.dataset.price, 10);
|
||||
const p = parseFloat(cb.dataset.price) || 0;
|
||||
selectedIds.push(cb.dataset.id);
|
||||
if (p > 0) {
|
||||
// Exclude dc_agent from discount calc
|
||||
|
|
@ -825,7 +857,7 @@ import Base from "../../layouts/Base.astro";
|
|||
|
||||
const fmt = (v) => v.toFixed(2);
|
||||
if (pricedCount >= 2) {
|
||||
const discount = Math.round(total * 0.15);
|
||||
const discount = Math.round(total * 15) / 100; // Precise to cents
|
||||
const final_ = total - discount;
|
||||
discountHtml = `<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Subtotal</span>
|
||||
|
|
|
|||
Loading…
Reference in a new issue