2025-12-14 01:54:40 -06:00
|
|
|
// Shared Authentication Utility for Admin Panel
|
|
|
|
|
// Include this file in all admin pages to handle authentication
|
|
|
|
|
|
|
|
|
|
// Global authentication state
|
|
|
|
|
window.adminAuth = {
|
|
|
|
|
user: null,
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-24 00:13:23 -06:00
|
|
|
// Load and apply theme on all admin pages
|
|
|
|
|
function loadAdminTheme() {
|
|
|
|
|
const savedTheme = localStorage.getItem("adminTheme") || "light";
|
|
|
|
|
applyAdminTheme(savedTheme);
|
|
|
|
|
|
|
|
|
|
// Watch for system theme changes if in auto mode
|
|
|
|
|
if (savedTheme === "auto") {
|
|
|
|
|
window
|
|
|
|
|
.matchMedia("(prefers-color-scheme: dark)")
|
|
|
|
|
.addEventListener("change", (e) => {
|
|
|
|
|
if (localStorage.getItem("adminTheme") === "auto") {
|
|
|
|
|
applyAdminTheme("auto");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyAdminTheme(theme) {
|
|
|
|
|
const body = document.body;
|
|
|
|
|
|
|
|
|
|
if (theme === "dark") {
|
|
|
|
|
body.classList.add("dark-mode");
|
|
|
|
|
body.classList.remove("light-mode");
|
|
|
|
|
} else if (theme === "light") {
|
|
|
|
|
body.classList.add("light-mode");
|
|
|
|
|
body.classList.remove("dark-mode");
|
|
|
|
|
} else if (theme === "auto") {
|
|
|
|
|
// Check system preference
|
|
|
|
|
const prefersDark = window.matchMedia(
|
|
|
|
|
"(prefers-color-scheme: dark)"
|
|
|
|
|
).matches;
|
|
|
|
|
if (prefersDark) {
|
|
|
|
|
body.classList.add("dark-mode");
|
|
|
|
|
body.classList.remove("light-mode");
|
|
|
|
|
} else {
|
|
|
|
|
body.classList.add("light-mode");
|
|
|
|
|
body.classList.remove("dark-mode");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize theme immediately (before page loads)
|
|
|
|
|
loadAdminTheme();
|
|
|
|
|
|
2025-12-19 20:44:46 -06:00
|
|
|
// Check authentication and redirect if needed - attach to window
|
|
|
|
|
window.checkAuth = async function () {
|
2025-12-14 01:54:40 -06:00
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/admin/session", {
|
|
|
|
|
credentials: "include",
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: "application/json",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2025-12-19 20:44:46 -06:00
|
|
|
window.redirectToLogin();
|
2025-12-14 01:54:40 -06:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (!data.authenticated) {
|
2025-12-19 20:44:46 -06:00
|
|
|
window.redirectToLogin();
|
2025-12-14 01:54:40 -06:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store user data
|
|
|
|
|
window.adminAuth.user = data.user;
|
|
|
|
|
window.adminAuth.isAuthenticated = true;
|
2025-12-19 20:44:46 -06:00
|
|
|
|
|
|
|
|
// Initialize mobile menu after auth check
|
|
|
|
|
window.initMobileMenu();
|
|
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
2025-12-19 20:44:46 -06:00
|
|
|
// Only log in development
|
|
|
|
|
if (window.location.hostname === "localhost") {
|
|
|
|
|
console.error("Authentication check failed:", error);
|
|
|
|
|
}
|
|
|
|
|
window.redirectToLogin();
|
2025-12-14 01:54:40 -06:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-19 20:44:46 -06:00
|
|
|
};
|
2025-12-14 01:54:40 -06:00
|
|
|
|
|
|
|
|
// Redirect to login page
|
2025-12-19 20:44:46 -06:00
|
|
|
window.redirectToLogin = function () {
|
2025-12-14 01:54:40 -06:00
|
|
|
if (window.location.pathname !== "/admin/login.html") {
|
|
|
|
|
window.location.href = "/admin/login.html";
|
|
|
|
|
}
|
2025-12-19 20:44:46 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Initialize mobile menu toggle
|
|
|
|
|
window.initMobileMenu = function () {
|
|
|
|
|
// Check if mobile menu button exists
|
|
|
|
|
let menuToggle = document.getElementById("mobileMenuToggle");
|
|
|
|
|
|
|
|
|
|
if (!menuToggle && window.innerWidth <= 768) {
|
|
|
|
|
// Create mobile menu button
|
|
|
|
|
menuToggle = document.createElement("button");
|
|
|
|
|
menuToggle.id = "mobileMenuToggle";
|
|
|
|
|
menuToggle.className = "mobile-menu-toggle";
|
|
|
|
|
menuToggle.setAttribute("aria-label", "Toggle navigation menu");
|
|
|
|
|
menuToggle.setAttribute("aria-expanded", "false");
|
|
|
|
|
menuToggle.innerHTML = '<i class="bi bi-list"></i>';
|
|
|
|
|
document.body.appendChild(menuToggle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (menuToggle) {
|
|
|
|
|
menuToggle.addEventListener("click", function () {
|
|
|
|
|
const sidebar = document.querySelector(".sidebar");
|
|
|
|
|
if (sidebar) {
|
|
|
|
|
const isActive = sidebar.classList.toggle("active");
|
|
|
|
|
this.setAttribute("aria-expanded", isActive ? "true" : "false");
|
|
|
|
|
this.innerHTML = isActive
|
|
|
|
|
? '<i class="bi bi-x"></i>'
|
|
|
|
|
: '<i class="bi bi-list"></i>';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close sidebar when clicking outside on mobile
|
|
|
|
|
document.addEventListener("click", function (event) {
|
|
|
|
|
const sidebar = document.querySelector(".sidebar");
|
|
|
|
|
const menuToggle = document.getElementById("mobileMenuToggle");
|
|
|
|
|
|
|
|
|
|
if (sidebar && menuToggle && window.innerWidth <= 768) {
|
|
|
|
|
if (
|
|
|
|
|
!sidebar.contains(event.target) &&
|
|
|
|
|
event.target !== menuToggle &&
|
|
|
|
|
!menuToggle.contains(event.target)
|
|
|
|
|
) {
|
|
|
|
|
if (sidebar.classList.contains("active")) {
|
|
|
|
|
sidebar.classList.remove("active");
|
|
|
|
|
menuToggle.setAttribute("aria-expanded", "false");
|
|
|
|
|
menuToggle.innerHTML = '<i class="bi bi-list"></i>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close menu on link click (mobile)
|
|
|
|
|
const sidebarLinks = document.querySelectorAll(".sidebar-menu a");
|
|
|
|
|
sidebarLinks.forEach((link) => {
|
|
|
|
|
link.addEventListener("click", function () {
|
|
|
|
|
if (window.innerWidth <= 768) {
|
|
|
|
|
const sidebar = document.querySelector(".sidebar");
|
|
|
|
|
if (sidebar && sidebar.classList.contains("active")) {
|
|
|
|
|
sidebar.classList.remove("active");
|
|
|
|
|
if (menuToggle) {
|
|
|
|
|
menuToggle.setAttribute("aria-expanded", "false");
|
|
|
|
|
menuToggle.innerHTML = '<i class="bi bi-list"></i>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle window resize
|
|
|
|
|
let resizeTimer;
|
|
|
|
|
window.addEventListener("resize", function () {
|
|
|
|
|
clearTimeout(resizeTimer);
|
|
|
|
|
resizeTimer = setTimeout(function () {
|
|
|
|
|
if (window.innerWidth > 768) {
|
|
|
|
|
const sidebar = document.querySelector(".sidebar");
|
|
|
|
|
if (sidebar) {
|
|
|
|
|
sidebar.classList.remove("active");
|
|
|
|
|
}
|
|
|
|
|
if (menuToggle) {
|
|
|
|
|
menuToggle.setAttribute("aria-expanded", "false");
|
|
|
|
|
menuToggle.innerHTML = '<i class="bi bi-list"></i>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 250);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Custom logout confirmation modal
|
|
|
|
|
window.showLogoutConfirm = function (onConfirm) {
|
|
|
|
|
// Create modal backdrop
|
|
|
|
|
const backdrop = document.createElement("div");
|
|
|
|
|
backdrop.style.cssText = `
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
z-index: 10000;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
animation: fadeIn 0.2s ease;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Create modal
|
|
|
|
|
const modal = document.createElement("div");
|
|
|
|
|
modal.style.cssText = `
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 30px;
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
width: 90%;
|
|
|
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
|
|
|
animation: slideIn 0.3s ease;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
modal.innerHTML = `
|
|
|
|
|
<style>
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
from { opacity: 0; }
|
|
|
|
|
to { opacity: 1; }
|
|
|
|
|
}
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
from { transform: translateY(-20px); opacity: 0; }
|
|
|
|
|
to { transform: translateY(0); opacity: 1; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<div style="text-align: center;">
|
|
|
|
|
<div style="font-size: 48px; margin-bottom: 15px;">
|
|
|
|
|
<i class="bi bi-box-arrow-right" style="color: #dc3545;"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 style="margin: 0 0 10px 0; color: #2c3e50; font-weight: 600;">Confirm Logout</h3>
|
|
|
|
|
<p style="color: #6c757d; margin: 0 0 25px 0;">Are you sure you want to logout?</p>
|
|
|
|
|
<div style="display: flex; gap: 10px; justify-content: center;">
|
|
|
|
|
<button id="cancelLogout" style="
|
|
|
|
|
padding: 10px 24px;
|
|
|
|
|
border: 2px solid #6c757d;
|
|
|
|
|
background: white;
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
">Cancel</button>
|
|
|
|
|
<button id="confirmLogout" style="
|
|
|
|
|
padding: 10px 24px;
|
|
|
|
|
border: none;
|
|
|
|
|
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
|
|
|
|
|
">Logout</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
backdrop.appendChild(modal);
|
|
|
|
|
document.body.appendChild(backdrop);
|
|
|
|
|
|
|
|
|
|
// Add hover effects
|
|
|
|
|
const cancelBtn = modal.querySelector("#cancelLogout");
|
|
|
|
|
const confirmBtn = modal.querySelector("#confirmLogout");
|
|
|
|
|
|
|
|
|
|
cancelBtn.addEventListener("mouseenter", function () {
|
|
|
|
|
this.style.background = "#6c757d";
|
|
|
|
|
this.style.color = "white";
|
|
|
|
|
});
|
|
|
|
|
cancelBtn.addEventListener("mouseleave", function () {
|
|
|
|
|
this.style.background = "white";
|
|
|
|
|
this.style.color = "#6c757d";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
confirmBtn.addEventListener("mouseenter", function () {
|
|
|
|
|
this.style.transform = "translateY(-2px)";
|
|
|
|
|
this.style.boxShadow = "0 4px 12px rgba(220, 53, 69, 0.4)";
|
|
|
|
|
});
|
|
|
|
|
confirmBtn.addEventListener("mouseleave", function () {
|
|
|
|
|
this.style.transform = "translateY(0)";
|
|
|
|
|
this.style.boxShadow = "0 2px 8px rgba(220, 53, 69, 0.3)";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle buttons
|
|
|
|
|
const closeModal = () => {
|
|
|
|
|
backdrop.style.animation = "fadeIn 0.2s ease reverse";
|
|
|
|
|
setTimeout(() => backdrop.remove(), 200);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cancelBtn.addEventListener("click", closeModal);
|
|
|
|
|
backdrop.addEventListener("click", function (e) {
|
|
|
|
|
if (e.target === backdrop) closeModal();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
confirmBtn.addEventListener("click", function () {
|
|
|
|
|
closeModal();
|
|
|
|
|
onConfirm();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ESC key to close
|
|
|
|
|
const escHandler = (e) => {
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
closeModal();
|
|
|
|
|
document.removeEventListener("keydown", escHandler);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener("keydown", escHandler);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Logout function - explicitly attach to window for onclick handlers
|
|
|
|
|
window.logout = async function (skipConfirm = false) {
|
|
|
|
|
if (!skipConfirm) {
|
|
|
|
|
window.showLogoutConfirm(async () => {
|
|
|
|
|
await performLogout();
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await performLogout();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Global function for inline onclick="logout()" handlers
|
|
|
|
|
// This must be at global scope so inline onclick can find it
|
|
|
|
|
function logout(skipConfirm = false) {
|
|
|
|
|
window.logout(skipConfirm);
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 20:44:46 -06:00
|
|
|
// Actual logout logic
|
|
|
|
|
async function performLogout() {
|
2025-12-14 01:54:40 -06:00
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/admin/logout", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
credentials: "include",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
window.adminAuth.user = null;
|
|
|
|
|
window.adminAuth.isAuthenticated = false;
|
|
|
|
|
window.location.href = "/admin/login.html";
|
2025-12-19 20:44:46 -06:00
|
|
|
} else {
|
|
|
|
|
console.error("Logout failed with status:", response.status);
|
|
|
|
|
// Still redirect to login even if logout fails
|
|
|
|
|
window.location.href = "/admin/login.html";
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-12-19 20:44:46 -06:00
|
|
|
console.error("Logout error:", error);
|
|
|
|
|
// Still redirect to login even if logout fails
|
2025-12-14 01:54:40 -06:00
|
|
|
window.location.href = "/admin/login.html";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show success notification
|
2025-12-19 20:44:46 -06:00
|
|
|
window.showSuccess = function (message) {
|
2025-12-14 01:54:40 -06:00
|
|
|
const alert = document.createElement("div");
|
|
|
|
|
alert.className =
|
|
|
|
|
"alert alert-success alert-dismissible fade show position-fixed";
|
|
|
|
|
alert.style.cssText =
|
|
|
|
|
"top: 20px; right: 20px; z-index: 9999; min-width: 300px;";
|
|
|
|
|
alert.innerHTML = `
|
|
|
|
|
${message}
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
|
|
|
`;
|
|
|
|
|
document.body.appendChild(alert);
|
|
|
|
|
setTimeout(() => alert.remove(), 5000);
|
2025-12-19 20:44:46 -06:00
|
|
|
};
|
2025-12-14 01:54:40 -06:00
|
|
|
|
|
|
|
|
// Show error notification
|
2025-12-19 20:44:46 -06:00
|
|
|
window.showError = function (message) {
|
2025-12-14 01:54:40 -06:00
|
|
|
const alert = document.createElement("div");
|
|
|
|
|
alert.className =
|
|
|
|
|
"alert alert-danger alert-dismissible fade show position-fixed";
|
|
|
|
|
alert.style.cssText =
|
|
|
|
|
"top: 20px; right: 20px; z-index: 9999; min-width: 300px;";
|
|
|
|
|
alert.innerHTML = `
|
|
|
|
|
${message}
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
|
|
|
`;
|
|
|
|
|
document.body.appendChild(alert);
|
|
|
|
|
setTimeout(() => alert.remove(), 5000);
|
2025-12-19 20:44:46 -06:00
|
|
|
};
|
2025-12-14 01:54:40 -06:00
|
|
|
|
|
|
|
|
// Auto-check authentication when this script loads
|
|
|
|
|
// Only run if we're not on the login page
|
|
|
|
|
if (window.location.pathname !== "/admin/login.html") {
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
2025-12-19 20:44:46 -06:00
|
|
|
window.checkAuth();
|
|
|
|
|
|
|
|
|
|
// Attach logout event listeners to all logout buttons
|
|
|
|
|
const logoutButtons = document.querySelectorAll(
|
|
|
|
|
'.btn-logout, [data-logout], [onclick*="logout"]'
|
|
|
|
|
);
|
|
|
|
|
logoutButtons.forEach((button) => {
|
|
|
|
|
// Remove inline onclick if it exists
|
|
|
|
|
button.removeAttribute("onclick");
|
|
|
|
|
|
|
|
|
|
// Add proper event listener
|
|
|
|
|
button.addEventListener("click", function (e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
window.logout();
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-12-14 01:54:40 -06:00
|
|
|
});
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
|
|
|
|
|
// Fix Bootstrap modal aria-hidden focus warning for all modals - Universal Solution
|
|
|
|
|
(function () {
|
|
|
|
|
// Use event delegation on document level to catch all modal hide events
|
|
|
|
|
document.addEventListener(
|
|
|
|
|
"hide.bs.modal",
|
|
|
|
|
function (event) {
|
|
|
|
|
// Get the modal that's closing
|
|
|
|
|
const modalElement = event.target;
|
|
|
|
|
|
|
|
|
|
// Blur any focused element inside the modal before it closes
|
|
|
|
|
const focusedElement = document.activeElement;
|
|
|
|
|
if (focusedElement && modalElement.contains(focusedElement)) {
|
|
|
|
|
focusedElement.blur();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
true
|
|
|
|
|
); // Use capture phase to run before Bootstrap's handlers
|
|
|
|
|
})();
|