// 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 = `
Scan this QR code with your authenticator app
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 = '