// main.js // --- 2FA Implementation --- let twoFactorVerified = false; // Check 2FA status on page load async function check2FAStatus() { try { const res = await fetch("/2fa/status"); if (res.ok) { const data = await res.json(); twoFactorVerified = data.verified; if (!twoFactorVerified) { show2FAModal(); } } else { show2FAModal(); } } catch (err) { show2FAModal(); } } // Show 2FA modal function show2FAModal() { const modal = document.getElementById("2fa-modal"); const setup = document.getElementById("2fa-setup"); const verify = document.getElementById("2fa-verify"); const loading = document.getElementById("2fa-loading"); modal.classList.remove("hidden"); loading.classList.remove("hidden"); setup.classList.add("hidden"); verify.classList.add("hidden"); // Generate 2FA secret generate2FASecret(); } // Generate 2FA secret async function generate2FASecret() { try { const res = await fetch("/2fa/generate"); if (res.ok) { const data = await res.json(); const setup = document.getElementById("2fa-setup"); const verify = document.getElementById("2fa-verify"); const loading = document.getElementById("2fa-loading"); loading.classList.add("hidden"); setup.classList.remove("hidden"); // Display secret key document.getElementById("secret-key").textContent = data.secret; // Generate QR code image const qrCode = document.getElementById("qr-code"); qrCode.innerHTML = `
QR Code for 2FA

Scan this QR code with your authenticator app

`; } else { throw new Error("Failed to generate 2FA secret"); } } catch (err) { console.error("2FA generation error:", err); document.getElementById("2fa-loading").innerHTML = '

Error generating 2FA. Please refresh the page.

'; } } // Setup 2FA event listeners document.addEventListener("DOMContentLoaded", function () { const generateBtn = document.getElementById("generate-2fa"); const verifyBtn = document.getElementById("verify-2fa"); const backBtn = document.getElementById("back-to-setup"); const tokenInput = document.getElementById("2fa-token"); if (generateBtn) { generateBtn.addEventListener("click", generate2FASecret); } if (verifyBtn) { verifyBtn.addEventListener("click", verify2FA); } if (backBtn) { backBtn.addEventListener("click", function () { document.getElementById("2fa-setup").classList.remove("hidden"); document.getElementById("2fa-verify").classList.add("hidden"); }); } if (tokenInput) { tokenInput.addEventListener("input", function () { if (this.value.length === 6) { verify2FA(); } }); } const proceedBtn = document.getElementById("proceed-to-verify"); if (proceedBtn) { proceedBtn.addEventListener("click", function () { document.getElementById("2fa-setup").classList.add("hidden"); document.getElementById("2fa-verify").classList.remove("hidden"); document.getElementById("2fa-token").focus(); }); } const downloadQrBtn = document.getElementById("download-qr"); if (downloadQrBtn) { downloadQrBtn.addEventListener("click", function () { const qrImg = document.querySelector("#qr-code img"); if (qrImg && qrImg.src) { const link = document.createElement("a"); link.download = "2fa-qr-code.png"; link.href = qrImg.src; link.click(); } }); } const copySecretBtn = document.getElementById("copy-secret"); if (copySecretBtn) { copySecretBtn.addEventListener("click", function () { const secretKey = document.getElementById("secret-key").textContent; if (secretKey) { navigator.clipboard .writeText(secretKey) .then(() => { // Show temporary success message const originalText = copySecretBtn.textContent; copySecretBtn.textContent = "Copied!"; copySecretBtn.classList.remove("bg-gray-500", "hover:bg-gray-600"); copySecretBtn.classList.add("bg-green-500"); setTimeout(() => { copySecretBtn.textContent = originalText; copySecretBtn.classList.remove("bg-green-500"); copySecretBtn.classList.add("bg-gray-500", "hover:bg-gray-600"); }, 2000); }) .catch(() => { alert("Failed to copy to clipboard. Secret: " + secretKey); }); } }); } }); // Verify 2FA token async function verify2FA() { const token = document.getElementById("2fa-token").value.trim(); if (token.length !== 6) { alert("Please enter a 6-digit code"); return; } try { const res = await fetch("/2fa/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), }); if (res.ok) { const data = await res.json(); if (data.success) { twoFactorVerified = true; document.getElementById("2fa-modal").classList.add("hidden"); // Enable all widgets enableDashboard(); } else { alert("Invalid token. Please try again."); } } else { const errorData = await res.json(); alert(errorData.error || "Verification failed"); } } catch (err) { alert("Verification failed. Please try again."); } } // Enable dashboard after 2FA verification function enableDashboard() { // Dashboard is already visible, just ensure all functionality is enabled console.log("Dashboard enabled after 2FA verification"); } // Check 2FA status on page load check2FAStatus(); // Function to update weather widget async function updateWeather() { try { const res = await fetch("/api/weather"); if (!res.ok) throw new Error("Failed to fetch weather"); const data = await res.json(); const weatherDiv = document.getElementById("weather-widget"); if (!weatherDiv) return; // Set temperature weatherDiv.querySelector(".weather-temp").textContent = `${data.temp_c} °C`; // Set icon const icon = weatherDiv.querySelector(".weather-icon"); if (data.condition === "sunny") { icon.src = "https://cdn-icons-png.flaticon.com/512/869/869869.png"; icon.alt = "Sunny"; } else if (data.condition === "rain") { icon.src = "https://cdn-icons-png.flaticon.com/512/414/414974.png"; icon.alt = "Rainy"; } else if (data.condition === "snow") { icon.src = "https://cdn-icons-png.flaticon.com/512/642/642102.png"; icon.alt = "Snowy"; } } catch (err) { // Show error in widget const weatherDiv = document.getElementById("weather-widget"); if (weatherDiv) { weatherDiv.querySelector(".weather-temp").textContent = "N/A"; weatherDiv.querySelector(".weather-icon").src = ""; weatherDiv.querySelector(".weather-icon").alt = "Error"; } } } // Initial load updateWeather(); // Update every 5 minutes setInterval(updateWeather, 5 * 60 * 1000); // --- Time Widget Logic --- // Time zone offsets in hours relative to UTC const timeZones = { london: 0, // UTC+0 (BST not handled for simplicity) est: -4, // UTC-4 (EDT, adjust for DST if needed) nigeria: 1, // UTC+1 pakistan: 5, // UTC+5 }; function pad(n) { return n < 10 ? "0" + n : n; } async function updateClocks() { try { const res = await fetch("/api/utc"); if (!res.ok) throw new Error("Failed to fetch UTC time"); const { utc } = await res.json(); const utcDate = new Date(utc); // Update each clock for (const [zone, offset] of Object.entries(timeZones)) { const local = new Date(utcDate.getTime() + offset * 60 * 60 * 1000); const h = pad(local.getUTCHours()); const m = pad(local.getUTCMinutes()); const s = pad(local.getUTCSeconds()); const el = document.getElementById(`${zone}-clock`); if (el) el.textContent = `${h}:${m}:${s}`; } } catch (err) { // Show error in all clocks for (const zone of Object.keys(timeZones)) { const el = document.getElementById(`${zone}-clock`); if (el) el.textContent = "N/A"; } } } // Initial load and update every second updateClocks(); setInterval(updateClocks, 1000); // --- Airport Autocomplete --- const airportInput = document.querySelector('input[placeholder*="airport"]'); let dropdownDiv; let selectedAirport; // Declare globally to store selection if (airportInput) { dropdownDiv = document.createElement("div"); dropdownDiv.className = "absolute bg-white border border-gray-300 rounded shadow z-10 w-full max-h-48 overflow-y-auto top-[calc(100%+10px)]"; dropdownDiv.style.display = "none"; dropdownDiv.setAttribute("role", "listbox"); // ARIA role airportInput.parentNode.appendChild(dropdownDiv); // Event Delegation for dropdown clicks (fixes listener duplication) dropdownDiv.addEventListener("click", (e) => { const item = e.target.closest("[data-index]"); if (!item) return; const index = item.dataset.index; selectedAirport = currentAirports[index]; // Use latest fetched data airportInput.value = `${selectedAirport.name} (${selectedAirport.code})`; // Set once onAirportSelected(selectedAirport); dropdownDiv.style.display = "none"; }); // Debounce input (300ms delay) let debounceTimer; let currentAirports = []; // Track latest results airportInput.addEventListener("input", async function () { clearTimeout(debounceTimer); debounceTimer = setTimeout(async () => { const val = airportInput.value.trim(); if (val.length < 3) { dropdownDiv.style.display = "none"; return; } try { const res = await fetch(`/airports?search=${encodeURIComponent(val)}`); if (!res.ok) throw new Error("Failed to fetch"); const airports = await res.json(); currentAirports = airports; // Store for click handler if (!airports.length) { dropdownDiv.innerHTML = '
No results
'; dropdownDiv.style.display = "block"; return; } // Escape HTML to prevent XSS dropdownDiv.innerHTML = airports .map((a, i) => { const name = a.name.replace( /[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]) ); return `
${name}
`; }) .join(""); dropdownDiv.style.display = "block"; } catch (err) { dropdownDiv.innerHTML = '
Error loading airports
'; dropdownDiv.style.display = "block"; } }, 300); }); // Hide dropdown only when focus leaves input/dropdown document.addEventListener("click", (e) => { if (!airportInput.contains(e.target) && !dropdownDiv.contains(e.target)) { dropdownDiv.style.display = "none"; } }); } // --- Map Widget Logic --- // let selectedAirport = null; function showMap(lat, lon) { console.log(lat, lon); const mapDiv = document.getElementById("map-widget"); if (!mapDiv) return; mapDiv.innerHTML = ""; mapDiv.style.height = "200px"; mapDiv.style.width = "300px"; // OpenLayers map if (window.ol && window.ol.Map) { new ol.Map({ target: mapDiv, layers: [new ol.layer.Tile({ source: new ol.source.OSM() })], view: new ol.View({ center: ol.proj.fromLonLat([-79.6248, 43.6532]), zoom: 13, }), }); } else { mapDiv.innerHTML = '
Map library not loaded
'; } } // --- Distance from Arctic Circle Widget --- function haversine(lat1, lon1, lat2, lon2) { const toRad = (deg) => (deg * Math.PI) / 180; const R = 6371; // Earth radius in km const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } function updateDistanceWidget(lat, lon) { const distDiv = document.getElementById("distance-widget"); if (!distDiv) return; // Arctic Circle: latitude 66.56333, longitude 0 const arcticLat = 66.56333; const arcticLon = 0; const dist = haversine(arcticLat, arcticLon, lat, lon); distDiv.textContent = dist.toFixed(1) + " KM"; } // Update distance when airport is selected function onAirportSelected(airport) { if ( airport && airport.latitude_deg && airport.longitude_deg && !isNaN(Number(airport.latitude_deg)) && !isNaN(Number(airport.longitude_deg)) ) { showMap(Number(airport.latitude_deg), Number(airport.longitude_deg)); updateDistanceWidget( Number(airport.latitude_deg), Number(airport.longitude_deg) ); } } // --- Analytic Logging --- function logWidgetClick(widgetName) { fetch("/analytic", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ widget_name: widgetName, browser_type: navigator.userAgent, }), }).then(async (res) => { if (res.status === 429) { const data = await res.json(); if (data.redirect) window.location.href = data.redirect; } }); } // --- Click Counter Widget --- async function updateClickCounter() { try { const res = await fetch("/analytic/count"); if (!res.ok) throw new Error("Failed to fetch count"); const { count } = await res.json(); const el = document.getElementById("click-counter-widget"); if (el) el.textContent = count; } catch (err) { const el = document.getElementById("click-counter-widget"); if (el) el.textContent = "N/A"; } } updateClickCounter(); setInterval(updateClickCounter, 60 * 1000); // --- Export XML Widget --- const exportBtn = document.querySelector( "button.bg-sky-400.text-white.font-semibold.px-8.py-2.rounded" ); if (exportBtn && exportBtn.textContent.trim().toLowerCase() === "export") { exportBtn.addEventListener("click", function () { window.location.href = "/analytic/export"; }); } // --- Import XML Widget --- const importXmlInput = document.getElementById("import-xml-input"); const importXmlBtn = document.getElementById("import-xml-btn"); const importStatus = document.getElementById("import-status"); if (importXmlBtn && importXmlInput && importStatus) { importXmlBtn.addEventListener("click", async function () { if (!importXmlInput.files || !importXmlInput.files[0]) { importStatus.textContent = "Please select an XML file"; importStatus.className = "text-xs mt-2 text-red-500"; return; } const file = importXmlInput.files[0]; // Validate file type if (!file.type.includes("xml")) { importStatus.textContent = "Please select a valid XML file"; importStatus.className = "text-xs mt-2 text-red-500"; return; } const formData = new FormData(); formData.append("xmlfile", file); try { importStatus.textContent = "Importing..."; importStatus.className = "text-xs mt-2 text-blue-500"; const res = await fetch("/analytic/import", { method: "POST", body: formData, }); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.error || "Import failed"); } const data = await res.json(); importStatus.textContent = data.message; importStatus.className = "text-xs mt-2 text-green-500"; // Clear the file input importXmlInput.value = ""; // Update the click counter to reflect new data setTimeout(updateClickCounter, 1000); } catch (err) { importStatus.textContent = `Error: ${err.message}`; importStatus.className = "text-xs mt-2 text-red-500"; } }); } // --- Reddit News Widget --- async function updateRedditNews() { try { const res = await fetch("/reddit"); if (!res.ok) throw new Error("Failed to fetch reddit"); const posts = await res.json(); const newsDiv = document.getElementById("news-widget"); if (!newsDiv) return; newsDiv.innerHTML = posts .map( (post) => `
${post.title}
by ${post.author}
` ) .join(""); } catch (err) { const newsDiv = document.getElementById("news-widget"); if (newsDiv) newsDiv.innerHTML = '
Failed to load news
'; } } updateRedditNews(); // --- Coin Calculator Widget --- const coinInput = document.querySelector('input[placeholder*="coin"]'); const coinBtn = document.querySelector( "button.bg-sky-400.text-white.font-semibold.px-8.py-2.rounded.mt-2" ); const coinResult = document.getElementById("coin-result-list"); if (coinInput && coinBtn && coinResult) { coinBtn.addEventListener("click", async function () { const val = coinInput.value.trim(); if (!val || isNaN(val) || Number(val) < 0) { coinResult.innerHTML = '
  • Enter a valid amount
  • '; return; } try { const res = await fetch("/coin-calc", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: val }), }); if (!res.ok) throw new Error("Failed to calculate"); const data = await res.json(); coinResult.innerHTML = data .map((c) => `
  • ${c.count} x $${c.denomination}
  • `) .join(""); } catch (err) { coinResult.innerHTML = '
  • Error calculating coins
  • '; } }); } // --- Upload Widget --- const uploadInput = document.querySelector( 'input[type="file"][id="upload-input"]' ); const uploadBtn = document.getElementById("upload-btn"); const uploadPreview = document.getElementById("upload-preview"); const uploadError = document.getElementById("upload-error"); async function fetchLatestUpload() { try { const res = await fetch("/upload/latest"); const data = await res.json(); if (data.url && uploadPreview) { uploadPreview.src = data.url; uploadPreview.style.display = ""; } else if (uploadPreview) { uploadPreview.style.display = "none"; } } catch {} } if (uploadBtn && uploadInput) { uploadBtn.addEventListener("click", async function () { if (!uploadInput.files || !uploadInput.files[0]) { if (uploadError) uploadError.textContent = "Please select an image."; return; } const formData = new FormData(); formData.append("image", uploadInput.files[0]); try { const res = await fetch("/upload", { method: "POST", body: formData }); const data = await res.json(); if (data.url) { if (uploadPreview) { uploadPreview.src = data.url; uploadPreview.style.display = ""; } if (uploadError) uploadError.textContent = ""; } else { if (uploadError) uploadError.textContent = data.error || "Upload failed."; } } catch (err) { if (uploadError) uploadError.textContent = "Upload failed."; } }); } fetchLatestUpload(); // Attach click listeners to widgets function attachAnalyticListeners() { const widgets = [ { id: "weather-widget", name: "weather" }, { id: "nigeria-clock", name: "nigeria-clock" }, { id: "london-clock", name: "london-clock" }, { id: "est-clock", name: "est-clock" }, { id: "pakistan-clock", name: "pakistan-clock" }, { id: "map-widget", name: "map" }, { id: "distance-widget", name: "distance" }, { id: "airport-autocomplete", name: "airport-autocomplete" }, { id: "click-counter-widget", name: "click-counter" }, // Add more as needed ]; widgets.forEach((w) => { const el = document.getElementById(w.id); if (el) { el.addEventListener("click", () => logWidgetClick(w.name)); } }); } // Call after DOMContentLoaded if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", attachAnalyticListeners); } else { attachAnalyticListeners(); }