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 errorMessage = document.getElementById("error-message");
|
||||||
const resultsEl = document.getElementById("results");
|
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 ---
|
// --- Name search ---
|
||||||
nameSearchBtn.addEventListener("click", runNameSearch);
|
nameSearchBtn?.addEventListener("click", runNameSearch);
|
||||||
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
|
nameInput?.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
|
||||||
|
|
||||||
async function runNameSearch() {
|
async function runNameSearch() {
|
||||||
const name = nameInput.value.trim();
|
const name = nameInput.value.trim();
|
||||||
|
|
@ -258,7 +266,11 @@ import Base from "../../layouts/Base.astro";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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.textContent = "✓ Request submitted! We'll email you within 1 business day.";
|
||||||
statusEl.className = "mt-2 text-xs text-green-700";
|
statusEl.className = "mt-2 text-xs text-green-700";
|
||||||
btn.textContent = "Sent ✓";
|
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 {
|
} else {
|
||||||
throw new Error("Failed");
|
throw new Error("Failed");
|
||||||
}
|
}
|
||||||
|
|
@ -360,13 +381,20 @@ import Base from "../../layouts/Base.astro";
|
||||||
errorBox.classList.add("hidden");
|
errorBox.classList.add("hidden");
|
||||||
loadingEl.classList.remove("hidden");
|
loadingEl.classList.remove("hidden");
|
||||||
|
|
||||||
|
// Cancel any prior in-flight request
|
||||||
|
if (currentController) currentController.abort();
|
||||||
|
const controller = new AbortController();
|
||||||
|
currentController = controller;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
|
||||||
const timeout = setTimeout(() => controller.abort(), 30000);
|
const timeout = setTimeout(() => controller.abort(), 30000);
|
||||||
|
|
||||||
const res = await fetch(`${API}/api/v1/fcc/lookup?frn=${frn}`, { signal: controller.signal });
|
const res = await fetch(`${API}/api/v1/fcc/lookup?frn=${frn}`, { signal: controller.signal });
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
// Ignore if a newer request superseded this one
|
||||||
|
if (currentController !== controller) return;
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const body = await res.json().catch(() => ({}));
|
const body = await res.json().catch(() => ({}));
|
||||||
if (res.status === 400) {
|
if (res.status === 400) {
|
||||||
|
|
@ -376,7 +404,7 @@ import Base from "../../layouts/Base.astro";
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.entity_name && !data.cores?.entity_name && !data.filer_499) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
renderResults(data);
|
renderResults(data);
|
||||||
|
|
@ -391,8 +419,12 @@ import Base from "../../layouts/Base.astro";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg, allowHtml) {
|
||||||
errorMessage.innerHTML = msg;
|
if (allowHtml) {
|
||||||
|
errorMessage.innerHTML = msg;
|
||||||
|
} else {
|
||||||
|
errorMessage.textContent = msg;
|
||||||
|
}
|
||||||
errorBox.classList.remove("hidden");
|
errorBox.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -684,7 +716,7 @@ import Base from "../../layouts/Base.astro";
|
||||||
function attachF499Handlers() {
|
function attachF499Handlers() {
|
||||||
card.querySelector(".f499-yes")?.addEventListener("click", () => {
|
card.querySelector(".f499-yes")?.addEventListener("click", () => {
|
||||||
showF499Result(
|
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"
|
"red"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -808,7 +840,7 @@ import Base from "../../layouts/Base.astro";
|
||||||
|
|
||||||
boxes.forEach((cb) => {
|
boxes.forEach((cb) => {
|
||||||
if (cb.checked) {
|
if (cb.checked) {
|
||||||
const p = parseInt(cb.dataset.price, 10);
|
const p = parseFloat(cb.dataset.price) || 0;
|
||||||
selectedIds.push(cb.dataset.id);
|
selectedIds.push(cb.dataset.id);
|
||||||
if (p > 0) {
|
if (p > 0) {
|
||||||
// Exclude dc_agent from discount calc
|
// Exclude dc_agent from discount calc
|
||||||
|
|
@ -825,7 +857,7 @@ import Base from "../../layouts/Base.astro";
|
||||||
|
|
||||||
const fmt = (v) => v.toFixed(2);
|
const fmt = (v) => v.toFixed(2);
|
||||||
if (pricedCount >= 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;
|
const final_ = total - discount;
|
||||||
discountHtml = `<div class="flex justify-between text-sm">
|
discountHtml = `<div class="flex justify-between text-sm">
|
||||||
<span class="text-gray-600">Subtotal</span>
|
<span class="text-gray-600">Subtotal</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue