// mkcert Web UI - Frontend JavaScript // Fixed version with proper template literal handling // Configuration const API_BASE = window.location.origin + '/api'; // Authentication state let authEnabled = false; let currentUser = null; let csrfToken = null; // DOM Elements let certificatesList, generateForm, domainsInput, formatSelect; let installCaBtn, showCaBtn, hideModal, caModal; let themeToggle; let statusIndicators = {}; let uploadDropzone, fileInput, uploadProgress, uploadResults, fileList; // Theme management // Theme management let currentTheme = localStorage.getItem('theme'); // Don't set default here, let server decide // Initialize app when DOM is loaded document.addEventListener('DOMContentLoaded', async function() { await checkAuthentication(); await fetchCSRFToken(); initializeElements(); await initializeTheme(); loadSystemStatus(); loadCertificates(); setupEventListeners(); }); // Fetch CSRF token async function fetchCSRFToken() { try { const response = await fetch('/api/csrf-token'); const data = await response.json(); if (data.success && data.csrfToken) { csrfToken = data.csrfToken; console.log('CSRF token fetched successfully'); } } catch (error) { console.error('Failed to fetch CSRF token:', error); } } // Check authentication status async function checkAuthentication() { try { const response = await fetch('/api/auth/status'); const data = await response.json(); authEnabled = data.authEnabled; currentUser = data.username; if (authEnabled && !data.authenticated) { window.location.href = '/login'; return; } // Show auth controls if authentication is enabled and user is logged in if (authEnabled && data.authenticated) { const authControls = document.getElementById('auth-controls'); const usernameDisplay = document.getElementById('username-display'); if (authControls) { authControls.style.display = 'block'; usernameDisplay.textContent = `Welcome, ${currentUser}`; } } } catch (error) { console.log('Auth check failed:', error); // If auth check fails and we're not on login page, assume no auth required } } // Handle logout function logout() { if (!authEnabled) return; fetch('/api/auth/logout', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { window.location.href = data.redirectTo || '/login'; } }) .catch(error => { console.error('Logout failed:', error); // Force redirect to login anyway window.location.href = '/login'; }); } // Initialize DOM elements function initializeElements() { certificatesList = document.getElementById('certificates-list'); generateForm = document.getElementById('generate-form'); domainsInput = document.getElementById('domains'); formatSelect = document.getElementById('format'); installCaBtn = document.getElementById('install-ca-btn'); showCaBtn = document.getElementById('show-ca-btn'); hideModal = document.getElementById('hide-modal'); caModal = document.getElementById('ca-modal'); themeToggle = document.getElementById('theme-toggle'); // Upload elements uploadDropzone = document.getElementById('upload-dropzone'); fileInput = document.getElementById('file-input'); uploadProgress = document.getElementById('upload-progress'); uploadResults = document.getElementById('upload-results'); fileList = document.getElementById('file-list'); // Debug: Check if upload elements are found console.log('Upload elements found:', { uploadDropzone: !!uploadDropzone, fileInput: !!fileInput, uploadProgress: !!uploadProgress, uploadResults: !!uploadResults, fileList: !!fileList }); // Status indicators statusIndicators.mkcert = document.getElementById('mkcert-status'); statusIndicators.ca = document.getElementById('ca-status'); statusIndicators.openssl = document.getElementById('openssl-status'); } // API request helper async function apiRequest(endpoint, options = {}) { try { const headers = { 'Content-Type': 'application/json', ...options.headers }; // Add CSRF token for state-changing operations if (csrfToken && (options.method === 'POST' || options.method === 'PUT' || options.method === 'DELETE')) { headers['X-CSRF-Token'] = csrfToken; } const response = await fetch(API_BASE + endpoint, { headers, credentials: 'include', // Important: include cookies for session ...options }); if (!response.ok) { const error = await response.json(); // Handle CSRF token errors by refreshing token if (response.status === 403 && error.code === 'CSRF_INVALID') { console.log('CSRF token invalid, refreshing...'); await fetchCSRFToken(); // Retry the request with new token if (csrfToken) { headers['X-CSRF-Token'] = csrfToken; const retryResponse = await fetch(API_BASE + endpoint, { headers, credentials: 'include', ...options }); if (!retryResponse.ok) { const retryError = await retryResponse.json(); throw retryError; } return await retryResponse.json(); } } // Handle authentication errors if (response.status === 401 && error.redirectTo) { window.location.href = error.redirectTo; return; } // Throw the full error object so UI can access all fields throw error; } return await response.json(); } catch (error) { console.error('API request failed:', error); throw error; } } // Setup event listeners function setupEventListeners() { if (generateForm) { generateForm.addEventListener('submit', handleGenerate); } if (installCaBtn) { installCaBtn.addEventListener('click', handleInstallCA); } if (showCaBtn) { showCaBtn.addEventListener('click', showRootCA); } if (hideModal) { hideModal.addEventListener('click', hideModalDialog); } if (themeToggle) { themeToggle.addEventListener('click', toggleTheme); } // Upload event listeners setupUploadEventListeners(); // Notification & monitoring event listeners setupNotificationEventListeners(); // Add logout button event listener const logoutBtn = document.getElementById('logout-btn'); if (logoutBtn) { logoutBtn.addEventListener('click', logout); } } // Load system status async function loadSystemStatus() { try { console.log('Loading system status...'); const status = await apiRequest('/status'); console.log('System status received:', { caExists: status.caExists, autoGenerated: status.autoGenerated }); // Show notification if CA was auto-generated if (status.autoGenerated) { showAlert( 'Root CA Auto-Generated!
' + 'A new Root Certificate Authority was automatically created and installed.
' + 'Location: ' + (status.caRoot || 'Default location'), 'success' ); } // Create status indicators HTML const statusHtml = '
' + '
' + '' + 'mkcert ' + (status.mkcertInstalled ? 'installed' : 'not installed') + '' + '
' + '
' + '' + 'Root CA ' + (status.caExists ? 'exists' : 'missing') + '' + (status.autoGenerated ? ' Auto-generated' : '') + '
' + '
' + '' + 'OpenSSL ' + (status.opensslAvailable ? 'available' : 'not available') + '' + '
' + '
'; // Update the status-info div const statusInfo = document.getElementById('status-info'); if (statusInfo) { statusInfo.innerHTML = statusHtml; } // Re-initialize status indicators after creating them statusIndicators.mkcert = document.getElementById('mkcert-status'); statusIndicators.ca = document.getElementById('ca-status'); statusIndicators.openssl = document.getElementById('openssl-status'); // Load Root CA information if CA exists if (status.caExists) { console.log('CA exists, loading Root CA info...'); try { await loadRootCAInfo(); console.log('Root CA info loaded successfully'); } catch (error) { console.error('Error loading Root CA info:', error); } } else { console.log('CA does not exist, showing manual generation option...'); // Show manual generation option if auto-generation failed showManualCAGeneration(); } } catch (error) { console.error('Failed to load system status:', error); const statusInfo = document.getElementById('status-info'); if (statusInfo) { statusInfo.innerHTML = '
Failed to load system status: ' + error.message + '
'; } } } // Load and display Root CA information async function loadRootCAInfo() { console.log('loadRootCAInfo: Starting...'); try { console.log('loadRootCAInfo: Making API request...'); const response = await apiRequest('/rootca/info'); console.log('loadRootCAInfo: API response:', response); const caInfo = response; // Data is at root level, not nested in caInfo let expiryInfo, expiryClass = ''; if (caInfo.daysUntilExpiry < 0) { expiryInfo = 'Expired ' + Math.abs(caInfo.daysUntilExpiry) + ' days ago'; expiryClass = 'expiry-expired'; } else if (caInfo.daysUntilExpiry <= 30) { expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; expiryClass = 'expiry-warning'; } else if (caInfo.daysUntilExpiry <= 90) { expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; expiryClass = 'expiry-caution'; } else { expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; expiryClass = 'expiry-good'; } if (caInfo.expiry) { expiryInfo += ' (' + caInfo.expiry + ')'; } const rootCAHtml = '
' + '
' + '
' + '' + '
' + (caInfo.subject || 'N/A') + '
' + '
' + '
' + '' + '
' + (caInfo.issuer || 'N/A') + '
' + '
' + '
' + '' + '
' + expiryInfo + '
' + '
' + '
' + '' + '
' + (caInfo.fingerprint || caInfo.serial || 'N/A') + '
' + '
' + '
' + '' + '
' + (caInfo.path || 'N/A') + '
' + '
' + '
' + '
' + '

CA Management

' + '
' + '' + '' + '
' + '
' + '

Installation Instructions

' + '
    ' + '
  • macOS: Double-click downloaded file, add to Keychain, set trust to "Always Trust"
  • ' + '
  • Linux: sudo cp rootCA.pem /usr/local/share/ca-certificates/mkcert-rootCA.crt && sudo update-ca-certificates
  • ' + '
  • Windows: Double-click file, install to "Trusted Root Certification Authorities"
  • ' + '
' + '
' + '
' + '
'; // Show the Root CA section console.log('loadRootCAInfo: Showing Root CA section...'); const rootCASection = document.getElementById('rootca-section'); if (rootCASection) { rootCASection.style.display = 'block'; console.log('loadRootCAInfo: Root CA section displayed'); } else { console.log('loadRootCAInfo: Root CA section element not found'); } // Update the rootca-info div console.log('loadRootCAInfo: Updating rootca-info div...'); const rootCAInfo = document.getElementById('rootca-info'); if (rootCAInfo) { rootCAInfo.innerHTML = rootCAHtml; console.log('loadRootCAInfo: HTML content updated'); // Add highlight effect to show the section was updated rootCAInfo.classList.add('ca-updated'); setTimeout(() => { rootCAInfo.classList.remove('ca-updated'); }, 3000); } else { console.log('loadRootCAInfo: rootca-info element not found'); } // Re-attach event listener for install CA button const newInstallBtn = document.getElementById('install-ca-btn'); if (newInstallBtn) { newInstallBtn.addEventListener('click', handleInstallCA); } } catch (error) { console.error('Failed to load Root CA info:', error); const rootCAInfo = document.getElementById('rootca-info'); if (rootCAInfo) { rootCAInfo.innerHTML = '
Failed to load Root CA information: ' + error.message + '
'; } } } // Update status indicator function updateStatusIndicator(type, status, message) { const indicator = statusIndicators[type]; if (!indicator) return; indicator.className = 'status-indicator ' + (status ? 'status-success' : 'status-error'); indicator.textContent = message; } // Show Root CA modal async function showRootCA() { try { const caInfo = await apiRequest('/rootca/info'); let expiryInfo; if (caInfo.daysUntilExpiry < 0) { expiryInfo = 'Expired ' + Math.abs(caInfo.daysUntilExpiry) + ' days ago'; } else if (caInfo.daysUntilExpiry <= 30) { expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; } else if (caInfo.daysUntilExpiry <= 90) { expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; } else { expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; } if (caInfo.expiry) { expiryInfo += ' (' + caInfo.expiry + ')'; } document.getElementById('ca-subject').textContent = caInfo.subject || 'N/A'; document.getElementById('ca-issuer').textContent = caInfo.issuer || 'N/A'; document.getElementById('ca-expiry').textContent = expiryInfo; document.getElementById('ca-fingerprint').textContent = caInfo.fingerprint || 'N/A'; document.getElementById('ca-path').textContent = caInfo.caRoot || 'N/A'; caModal.style.display = 'block'; } catch (error) { showAlert('Failed to load CA information: ' + error.message, 'error'); } } // Hide modal dialog function hideModalDialog() { caModal.style.display = 'none'; } // Handle certificate generation async function handleGenerate(event) { event.preventDefault(); const domains = domainsInput.value.trim().split('\n').filter(d => d.trim()); const format = formatSelect.value; if (domains.length === 0) { showAlert('Please enter at least one domain', 'error'); return; } try { const result = await apiRequest('/execute', { method: 'POST', body: JSON.stringify({ command: 'generate', input: domains.join(' '), format: format }) }); const formatName = format.toUpperCase(); showAlert('Certificate generated successfully for: ' + domains.join(', '), 'success'); loadCertificates(); generateForm.reset(); } catch (error) { showAlert('Failed to generate certificate: ' + error.message, 'error'); } } // Load and display certificates async function loadCertificates() { try { const response = await apiRequest('/certificates'); displayCertificates(response.certificates || []); } catch (error) { showAlert('Failed to load certificates: ' + error.message, 'error'); certificatesList.innerHTML = '

Failed to load certificates

'; } } // Display certificates list function displayCertificates(certificates) { if (!certificates || certificates.length === 0) { certificatesList.innerHTML = '

No certificates found

'; return; } const html = certificates.map(cert => { // Truncate domains list if too long let domainsDisplay = cert.domains ? cert.domains.join(', ') : 'Unknown'; if (domainsDisplay.length > 100) { const truncated = domainsDisplay.substring(0, 97) + '...'; domainsDisplay = `${truncated}`; } // Use the modification date from the cert file, or current date if not available const createdDate = cert.cert ? new Date(cert.cert.modified).toLocaleDateString() : new Date().toLocaleDateString(); const createdTime = cert.cert ? new Date(cert.cert.modified).toLocaleTimeString() : new Date().toLocaleTimeString(); // Get file size from cert file const certFileSize = cert.cert ? cert.cert.size : 0; const formatBadge = cert.format ? '' + cert.format.toUpperCase() + '' : ''; let expiryInfo, expiryClass = ''; if (cert.expiry) { const expiryDate = new Date(cert.expiry); const now = new Date(); const daysUntilExpiry = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24)); const expiryDateStr = expiryDate.toLocaleDateString(); if (daysUntilExpiry < 0) { expiryInfo = 'Expired ' + Math.abs(daysUntilExpiry) + ' days ago'; expiryClass = 'expiry-expired'; cert.isExpired = true; } else if (daysUntilExpiry <= 30) { expiryInfo = 'Expires in ' + daysUntilExpiry + ' days'; expiryClass = 'expiry-warning'; } else if (daysUntilExpiry <= 90) { expiryInfo = 'Expires in ' + daysUntilExpiry + ' days'; expiryClass = 'expiry-caution'; } else { expiryInfo = 'Expires in ' + daysUntilExpiry + ' days'; expiryClass = 'expiry-good'; } expiryInfo += ' (' + expiryDateStr + ')'; } else { expiryInfo = 'Unknown'; } // Format folder display - use actual certificate folder information let folderDisplay, folderParam, isRootCert; if (cert.isInterfaceSSL) { // Interface SSL certificates (in root certificates folder) folderDisplay = 'Interface SSL'; folderParam = 'interface-ssl'; isRootCert = true; // Interface SSL certs are read-only } else if (cert.folder) { // Date-based certificates (user-generated) folderDisplay = cert.folder; folderParam = cert.folder; isRootCert = false; // User-generated certificates are editable } else if (cert.name === 'mkcert-rootCA' || (cert.cert && cert.cert.filename === 'mkcert-rootCA.pem')) { // Actual root CA certificates folderDisplay = 'Root CA'; folderParam = 'root-ca'; isRootCert = true; // Root CA is read-only } else { // Legacy certificates (uploaded or other) folderDisplay = 'Legacy'; folderParam = 'legacy'; isRootCert = cert.canEdit === false; // Use canEdit flag from backend } const isArchived = cert.isArchived || false; const isRootCA = cert.name === 'mkcert-rootCA' || (cert.cert && cert.cert.filename === 'mkcert-rootCA.pem'); // Extract filenames for compatibility const certFile = cert.cert ? cert.cert.filename : null; const keyFile = cert.key ? cert.key.filename : null; return '
' + '
' + '
' + ' ' + cert.name + (isRootCA ? ' (Root Certificate Authority)' : '') + '
' + '
' + formatBadge + (cert.isExpired ? 'EXPIRED' : '') + (isRootCA ? 'ROOT CA' : '') + (isRootCert && !isRootCA ? 'READ-ONLY' : '') + (isArchived ? 'ARCHIVED' : '') + '
' + '
' + '
' + '
Domains:
' + domainsDisplay + '
' + '
Location:
' + folderDisplay + '
' + '
Created:
' + createdDate + ' ' + createdTime + '
' + '
Expiry:
' + expiryInfo + '
' + '
Certificate File:
' + (certFile || 'Unknown') + '
' + '
Private Key File:
' + (keyFile || 'Missing') + '
' + '
File Size:
' + formatFileSize(certFileSize) + '
' + '
Status:
' + (isArchived ? 'Archived' : 'Active') + '
' + '
' + (isRootCA ? '
' + '

Root Certificate Authority

' + '

This is your mkcert Root CA certificate. Install this certificate in your system\'s trust store to enable local HTTPS development with automatically trusted certificates.

' + '

Installation: Download and install this certificate to trust all mkcert-generated certificates on this system.

' + '
' : '') + '
' + '' + (keyFile ? '' : '') + '' + (keyFile && !isRootCert ? '' : '') + (!isRootCert && !isArchived ? '' : isArchived ? '' + '' : '' + ' Protected') + '
'; }).join(''); certificatesList.innerHTML = html; } // Certificate management functions async function deleteCertificate(folder, certName) { // Check if this is a root certificate if (folder === 'root') { showAlert('Root certificates are read-only and cannot be deleted', 'error'); return; } try { let endpoint; if (folder === 'root') { endpoint = '/certificates/' + certName; } else { endpoint = '/certificates/' + folder + '/' + certName; } await apiRequest(endpoint, { method: 'DELETE' }); showAlert('Certificate "' + certName + '" deleted permanently', 'success'); loadCertificates(); } catch (error) { if (error.message.includes('read-only')) { showAlert('Root certificates are read-only and cannot be deleted', 'error'); } else { showAlert('Failed to delete certificate: ' + error.message, 'error'); } } } async function archiveCertificate(folder, certName) { // Check if this is a read-only certificate that cannot be archived if (folder === 'interface-ssl') { showAlert('Interface SSL certificates cannot be archived', 'error'); return; } if (folder === 'root-ca') { showAlert('Root CA certificates cannot be archived', 'error'); return; } if (folder === 'legacy') { showAlert('Legacy certificates may be read-only and cannot be archived', 'error'); return; } if (!confirm('Are you sure you want to archive the certificate "' + certName + '"?')) { return; } try { // Encode folder for URL path const folderParam = encodeURIComponent(folder); const endpoint = '/certificates/' + folderParam + '/' + encodeURIComponent(certName) + '/archive'; await apiRequest(endpoint, { method: 'POST' }); showAlert('Certificate "' + certName + '" archived successfully', 'success'); loadCertificates(); } catch (error) { let mainMsg = error.message || error.error || 'Unknown error'; let msg = 'Failed to archive certificate: ' + mainMsg; if (error.checkedCertPaths || error.checkedKeyPaths) { msg += '
Checked Cert Paths:
' + (error.checkedCertPaths ? error.checkedCertPaths.join('
') : 'None'); msg += '
Checked Key Paths:
' + (error.checkedKeyPaths ? error.checkedKeyPaths.join('
') : 'None'); } // If no message, show full error object for debugging if (!error.message && !error.error) { msg += '
' + JSON.stringify(error, null, 2) + '
'; } showAlert(msg, 'error'); } } async function restoreCertificate(folder, certName) { if (!confirm('Are you sure you want to restore the certificate "' + certName + '"?')) { return; } try { // Encode folder for URL path const folderParam = encodeURIComponent(folder); const endpoint = '/certificates/' + folderParam + '/' + encodeURIComponent(certName) + '/restore'; await apiRequest(endpoint, { method: 'POST' }); showAlert('Certificate "' + certName + '" restored successfully', 'success'); loadCertificates(); } catch (error) { let mainMsg = error.message || error.error || 'Unknown error'; let msg = 'Failed to restore certificate: ' + mainMsg; if (error.checkedCertPaths || error.checkedKeyPaths) { msg += '
Checked Cert Paths:
' + (error.checkedCertPaths ? error.checkedCertPaths.join('
') : 'None'); msg += '
Checked Key Paths:
' + (error.checkedKeyPaths ? error.checkedKeyPaths.join('
') : 'None'); } // If no message, show full error object for debugging if (!error.message && !error.error) { msg += '
' + JSON.stringify(error, null, 2) + '
'; } showAlert(msg, 'error'); } } // Show manual CA generation option function showManualCAGeneration() { const rootCASection = document.getElementById('rootca-section'); if (rootCASection) { rootCASection.style.display = 'block'; const rootCAInfo = document.getElementById('rootca-info'); if (rootCAInfo) { rootCAInfo.innerHTML = '
' + '
' + '' + '

Root CA Not Found

' + '

A Root Certificate Authority (CA) is required to generate SSL certificates. You can generate one now.

' + '
' + '
' + '' + '
' + '

What this does:

' + '
    ' + '
  • Creates a new Root Certificate Authority
  • ' + '
  • Installs it in your system trust store
  • ' + '
  • Enables certificate generation for local development
  • ' + '
' + '
' + '
' + '
'; // Attach event listener for generate CA button const generateBtn = document.getElementById('generate-ca-btn'); if (generateBtn) { generateBtn.addEventListener('click', handleGenerateCA); } } } } // Handle manual CA generation async function handleGenerateCA() { const generateBtn = document.getElementById('generate-ca-btn'); const rootCAInfo = document.getElementById('rootca-info'); if (generateBtn) { generateBtn.innerHTML = ' Generating Root CA...'; generateBtn.disabled = true; } // Show progress indicator in the Root CA section if (rootCAInfo) { rootCAInfo.innerHTML = '
' + '
' + '' + '

Generating Root Certificate Authority...

' + '

Please wait while we create your new Root CA. This may take a few moments.

' + '
' + '
Creating CA certificate
' + '
Installing in system trust store
' + '
Configuring for certificate generation
' + '
' + '
' + '
'; } try { const result = await apiRequest('/generate-ca', { method: 'POST' }); if (result.success) { // Show immediate success notification showAlert(result.message, 'success'); // Update progress indicator to show completion if (rootCAInfo) { rootCAInfo.innerHTML = '
' + '
' + '' + '

Root CA Generated Successfully!

' + '

Loading certificate details...

' + '
' + '
'; } // Force reload the system status to check if CA was created console.log('CA generation completed, refreshing system status...'); await loadSystemStatus(); // Double-check by making another status call after a brief delay setTimeout(async () => { console.log('Secondary status refresh after CA generation...'); await loadSystemStatus(); // Show detailed success message let successMessage = 'Root CA Generated Successfully!
' + 'Your new Root Certificate Authority is now ready to generate SSL certificates.
' + 'CA Root Path: ' + (result.caRoot || 'Default location'); if (result.caCopiedToPublic) { successMessage += '
✓ CA Certificate copied to public download area
' + 'Available for download in the certificates list'; } if (result.caInfo && result.caInfo.expiry) { successMessage += '
Valid Until: ' + new Date(result.caInfo.expiry).toLocaleDateString(); } showAlert(successMessage, 'success'); // Scroll to the Root CA section to show the new information const rootCASection = document.getElementById('rootca-section'); if (rootCASection) { rootCASection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // Also refresh the certificates list to show the public CA copy await loadCertificates(); }, 2000); // Increased delay to 2 seconds to ensure backend processing is complete } else { // Show error in the Root CA section if (rootCAInfo) { rootCAInfo.innerHTML = '
' + '
' + '' + '

Root CA Generation Failed

' + '

Error: ' + result.error + '

' + '' + '
' + '
'; // Attach retry event listener const retryBtn = document.getElementById('retry-generate-ca-btn'); if (retryBtn) { retryBtn.addEventListener('click', handleGenerateCA); } } showAlert('Failed to generate CA: ' + result.error, 'error'); } } catch (error) { // Show error in the Root CA section if (rootCAInfo) { rootCAInfo.innerHTML = '
' + '
' + '' + '

Root CA Generation Failed

' + '

Error: ' + error.message + '

' + '' + '
' + '
'; // Attach retry event listener const retryBtn = document.getElementById('retry-generate-ca-btn'); if (retryBtn) { retryBtn.addEventListener('click', handleGenerateCA); } } showAlert('Failed to generate CA: ' + error.message, 'error'); } finally { if (generateBtn) { generateBtn.innerHTML = ' Generate Root CA'; generateBtn.disabled = false; } } } // CA Installation async function handleInstallCA() { installCaBtn.innerHTML = ' Installing...'; installCaBtn.disabled = true; try { await apiRequest('/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'install-ca' }) }); showAlert('Root CA installed successfully', 'success'); hideModalDialog(); // Add a small delay and retry mechanism to ensure CA installation is reflected await waitForCAInstallation(); } catch (error) { showAlert('Failed to install CA: ' + error.message, 'error'); } finally { installCaBtn.innerHTML = ' Install CA'; installCaBtn.disabled = false; } } // Helper function to wait for CA installation to be reflected in status async function waitForCAInstallation(maxRetries = 5, delay = 500) { for (let i = 0; i < maxRetries; i++) { try { // Wait a bit before checking await new Promise(resolve => setTimeout(resolve, delay)); // Check if CA now exists const status = await apiRequest('/status'); if (status.caExists) { // CA is now available, reload the status await loadSystemStatus(); return; } // Double the delay for next attempt (exponential backoff) delay *= 1.5; } catch (error) { console.warn(`Attempt ${i + 1} to check CA status failed:`, error); } } // If we get here, fallback to loading status anyway console.warn('CA status check retries exhausted, loading status anyway'); await loadSystemStatus(); } // Utility functions function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // Alert system function showAlert(message, type = 'info') { const alertId = Date.now(); const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-' + type; alertDiv.id = 'alert-' + alertId; alertDiv.innerHTML = message + ''; const container = document.querySelector('.alerts-container') || document.body; container.appendChild(alertDiv); // Auto-hide after 5 seconds setTimeout(() => { hideAlert(alertId); }, 5000); } function hideAlert(alertId) { const alert = document.getElementById('alert-' + alertId); if (alert) { alert.remove(); } } // Certificate Upload Functionality function setupUploadEventListeners() { console.log('Setting up upload event listeners...', { uploadDropzone: !!uploadDropzone, fileInput: !!fileInput }); if (!uploadDropzone || !fileInput) { console.warn('Upload elements not found, skipping setup'); return; } console.log('Upload elements found, setting up listeners...'); // Click to select files uploadDropzone.addEventListener('click', () => { fileInput.click(); }); // File input change fileInput.addEventListener('change', handleFileSelect); // Drag and drop events uploadDropzone.addEventListener('dragover', handleDragOver); uploadDropzone.addEventListener('dragenter', handleDragEnter); uploadDropzone.addEventListener('dragleave', handleDragLeave); uploadDropzone.addEventListener('drop', handleDrop); // Prevent default drag behaviors on the document document.addEventListener('dragover', (e) => e.preventDefault()); document.addEventListener('drop', (e) => e.preventDefault()); } function handleDragOver(e) { e.preventDefault(); e.stopPropagation(); uploadDropzone.classList.add('dragover'); } function handleDragEnter(e) { e.preventDefault(); e.stopPropagation(); uploadDropzone.classList.add('dragover'); } function handleDragLeave(e) { e.preventDefault(); e.stopPropagation(); // Only remove dragover if we're leaving the dropzone entirely if (!uploadDropzone.contains(e.relatedTarget)) { uploadDropzone.classList.remove('dragover'); } } function handleDrop(e) { e.preventDefault(); e.stopPropagation(); uploadDropzone.classList.remove('dragover'); const files = Array.from(e.dataTransfer.files); handleFiles(files); } function handleFileSelect(e) { const files = Array.from(e.target.files); handleFiles(files); } function handleFiles(files) { if (files.length === 0) return; // Filter valid files const validExtensions = ['.pem', '.crt', '.key', '.cer', '.p7b', '.p7c', '.pfx', '.p12']; const validFiles = files.filter(file => { const ext = '.' + file.name.split('.').pop().toLowerCase(); return validExtensions.includes(ext); }); const invalidFiles = files.filter(file => { const ext = '.' + file.name.split('.').pop().toLowerCase(); return !validExtensions.includes(ext); }); if (invalidFiles.length > 0) { showAlert( `Skipped ${invalidFiles.length} invalid file(s). Only certificate and key files are allowed.`, 'warning' ); } if (validFiles.length === 0) { showAlert('No valid certificate files selected.', 'error'); return; } // Show file list displayFileList(validFiles); // Upload files uploadFiles(validFiles); } function displayFileList(files) { if (!fileList) return; fileList.innerHTML = ''; files.forEach(file => { const ext = '.' + file.name.split('.').pop().toLowerCase(); const isKey = file.name.includes('-key') || file.name.includes('.key') || ext === '.key'; const isCert = ['.pem', '.crt', '.cer'].includes(ext) && !isKey; let icon, type; if (isCert) { icon = 'certificate'; type = 'Certificate'; } else if (isKey) { icon = 'key'; type = 'Private Key'; } else { icon = 'file-alt'; type = 'Certificate File'; } const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = `
${file.name} (${formatFileSize(file.size)})
Pending
`; fileList.appendChild(fileItem); }); } async function uploadFiles(files) { if (!files || files.length === 0) return; // Show progress if (uploadProgress) { uploadProgress.classList.add('visible'); updateProgress(0, 'Preparing upload...'); } // Clear previous results if (uploadResults) { uploadResults.classList.remove('visible'); uploadResults.innerHTML = ''; } try { const formData = new FormData(); files.forEach(file => { formData.append('certificates', file); }); // Update progress updateProgress(25, 'Uploading files...'); const response = await fetch('/api/certificates/upload', { method: 'POST', body: formData, credentials: 'same-origin' }); updateProgress(75, 'Processing files...'); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `Upload failed: ${response.status} ${response.statusText}`); } const result = await response.json(); updateProgress(100, 'Upload complete!'); // Hide progress after a moment setTimeout(() => { if (uploadProgress) { uploadProgress.classList.remove('visible'); } }, 1500); // Update file statuses updateFileStatuses(result); // Show results displayUploadResults(result); // Refresh certificates list if (result.success && result.completePairs > 0) { setTimeout(() => { loadCertificates(); }, 1000); } // Clear file input if (fileInput) { fileInput.value = ''; } } catch (error) { console.error('Upload error:', error); // Hide progress if (uploadProgress) { uploadProgress.classList.remove('visible'); } // Update all file statuses to error const fileStatuses = document.querySelectorAll('.file-status'); fileStatuses.forEach(status => { status.className = 'file-status error'; status.innerHTML = 'Failed'; }); showAlert('Upload failed: ' + error.message, 'error'); } } function updateProgress(percent, text) { const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); if (progressFill) { progressFill.style.width = `${percent}%`; } if (progressText) { progressText.textContent = text; } } function updateFileStatuses(result) { const fileStatuses = document.querySelectorAll('.file-status'); // Mark all as success initially fileStatuses.forEach(status => { status.className = 'file-status success'; status.innerHTML = 'Uploaded'; }); // Update specific files with errors if any if (result.errors && result.errors.length > 0) { result.errors.forEach(error => { // Try to match error to specific files (basic matching) fileStatuses.forEach(status => { const fileName = status.getAttribute('data-file'); if (error.includes(fileName)) { status.className = 'file-status error'; status.innerHTML = 'Error'; } }); }); } // Mark incomplete pairs as warnings if (result.incompletePairs && result.incompletePairs.length > 0) { result.incompletePairs.forEach(incomplete => { fileStatuses.forEach(status => { const fileName = status.getAttribute('data-file'); if (fileName.includes(incomplete.certName)) { status.className = 'file-status warning'; status.innerHTML = `Missing ${incomplete.missing}`; } }); }); } } function displayUploadResults(result) { if (!uploadResults) return; let html = ''; if (result.success && result.completePairs > 0) { html += `
Success! Uploaded ${result.completePairs} certificate pair(s) to the "uploaded" folder.
`; } if (result.incompletePairs && result.incompletePairs.length > 0) { html += `
Incomplete Pairs: ${result.incompletePairs.length} file(s) are missing their certificate or key pair.
`; } if (result.errors && result.errors.length > 0) { html += `
Errors:
`; } if (!result.success && (!result.errors || result.errors.length === 0)) { html += `
Upload failed: No files were successfully processed.
`; } uploadResults.innerHTML = html; uploadResults.classList.add('visible'); // Auto-hide results after 10 seconds setTimeout(() => { if (uploadResults) { uploadResults.classList.remove('visible'); } }, 10000); } // Theme management functions async function initializeTheme() { // If no stored preference, get default from server if (!localStorage.getItem('theme')) { try { const config = await apiRequest('/config/theme'); currentTheme = config.defaultTheme || 'dark'; } catch (error) { console.warn('Failed to fetch default theme config, using dark mode:', error); currentTheme = 'dark'; } } // Set theme based on stored preference or server default document.documentElement.setAttribute('data-theme', currentTheme); updateThemeToggleButton(); } function toggleTheme() { currentTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', currentTheme); localStorage.setItem('theme', currentTheme); updateThemeToggleButton(); } function updateThemeToggleButton() { if (themeToggle) { const icon = themeToggle.querySelector('i'); const text = themeToggle.childNodes[themeToggle.childNodes.length - 1]; if (currentTheme === 'dark') { icon.className = 'fas fa-sun'; text.textContent = ' Light Mode'; } else { icon.className = 'fas fa-moon'; text.textContent = ' Dark Mode'; } } } // Download functions for authenticated file downloads async function downloadFile(url, filename) { try { const response = await fetch(url, { method: 'GET', credentials: 'same-origin' // Include session cookies }); if (!response.ok) { throw new Error(`Download failed: ${response.status} ${response.statusText}`); } const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(downloadUrl); } catch (error) { console.error('Download error:', error); showAlert('Download failed: ' + error.message, 'error'); } } function downloadCert(folderParam, filename) { const url = API_BASE + '/download/cert/' + folderParam + '/' + filename; downloadFile(url, filename); } function downloadKey(folderParam, filename) { const url = API_BASE + '/download/key/' + folderParam + '/' + filename; downloadFile(url, filename); } function downloadBundle(folderParam, certname) { const url = API_BASE + '/download/bundle/' + folderParam + '/' + certname; downloadFile(url, certname + '.zip'); } function downloadRootCA() { const url = API_BASE + '/download/rootca'; downloadFile(url, 'mkcert-rootCA.pem'); } function testPFX(folderParam, certname) { generatePFX(folderParam, certname); } async function generatePFX(folderParam, certname) { console.log('generatePFX called with:', folderParam, certname); // Create a modal for password input const modal = document.createElement('div'); modal.className = 'modal'; modal.style.display = 'block'; // Show the modal modal.innerHTML = ` `; document.body.appendChild(modal); console.log('Modal created and added to DOM'); // Focus on password input const passwordInput = document.getElementById('pfx-password'); passwordInput.focus(); return new Promise((resolve, reject) => { const generateBtn = document.getElementById('generate-pfx-btn'); const cancelBtn = document.getElementById('cancel-pfx-btn'); const cleanup = () => { document.body.removeChild(modal); }; const handleGenerate = async () => { console.log('Generate button clicked!'); const password = passwordInput.value; console.log('Password entered:', password ? 'Yes' : 'No'); generateBtn.innerHTML = ' Generating...'; generateBtn.disabled = true; try { console.log('Making API request...'); const apiUrl = API_BASE + '/generate/pfx/' + folderParam + '/' + encodeURIComponent(certname); console.log('API URL:', apiUrl); const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'same-origin', body: JSON.stringify({ password: password || '' }) }); console.log('Response status:', response.status); if (!response.ok) { const errorData = await response.json(); console.error('Server error:', errorData); throw new Error(errorData.error || `PFX generation failed: ${response.status} ${response.statusText}`); } console.log('Downloading blob...'); const blob = await response.blob(); console.log('Blob size:', blob.size); const downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = certname + '.pfx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(downloadUrl); showAlert('PFX file generated and downloaded successfully', 'success'); cleanup(); resolve(); } catch (error) { console.error('PFX generation error:', error); showAlert('PFX generation failed: ' + error.message, 'error'); generateBtn.innerHTML = ' Generate PFX'; generateBtn.disabled = false; reject(error); } }; const handleCancel = () => { cleanup(); resolve(); }; generateBtn.addEventListener('click', handleGenerate); cancelBtn.addEventListener('click', handleCancel); console.log('Event listeners attached to buttons'); // Allow Enter to trigger generation passwordInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { handleGenerate(); } }); // Allow Escape to cancel modal.addEventListener('keydown', (e) => { if (e.key === 'Escape') { handleCancel(); } }); }); } // ================================ // Notification & Monitoring Functions // ================================ // Setup notification event listeners function setupNotificationEventListeners() { // Email test button const testEmailBtn = document.getElementById('test-email-btn'); if (testEmailBtn) { testEmailBtn.addEventListener('click', testEmailConfiguration); } // SMTP verify button const verifySmtpBtn = document.getElementById('verify-smtp-btn'); if (verifySmtpBtn) { verifySmtpBtn.addEventListener('click', verifySmtpConnection); } // Certificate expiry check button const checkExpiryBtn = document.getElementById('check-expiry-btn'); if (checkExpiryBtn) { checkExpiryBtn.addEventListener('click', checkCertificateExpiry); } // Start monitoring button const startMonitoringBtn = document.getElementById('start-monitoring-btn'); if (startMonitoringBtn) { startMonitoringBtn.addEventListener('click', startCertificateMonitoring); } // Stop monitoring button const stopMonitoringBtn = document.getElementById('stop-monitoring-btn'); if (stopMonitoringBtn) { stopMonitoringBtn.addEventListener('click', stopCertificateMonitoring); } // Load notification status on page load loadNotificationStatus(); } // Load notification and monitoring status async function loadNotificationStatus() { await Promise.all([ loadEmailStatus(), loadMonitoringStatus(), loadExpiringCertificates() ]); } // Load email configuration status async function loadEmailStatus() { try { console.log('Loading email status...'); const response = await apiRequest('/email/status'); const statusElement = document.getElementById('email-status'); const actionsElement = document.getElementById('email-actions'); if (!statusElement) return; let statusHtml = ''; let statusClass = ''; if (!response.enabled) { statusClass = 'status-disabled'; statusHtml = `
Email notifications are disabled

Set EMAIL_NOTIFICATIONS_ENABLED=true to enable email notifications

`; } else if (!response.configured) { statusClass = 'status-error'; statusHtml = `
Email service not configured

Missing required SMTP configuration (host, user, password, recipients)

`; } else { statusClass = 'status-enabled'; statusHtml = `
Email service configured and ready
SMTP Host: ${response.smtp.host}
Port: ${response.smtp.port} ${response.smtp.secure ? '(SSL)' : '(TLS)'}
From: ${response.from}
Recipients: ${response.to ? response.to.join(', ') : 'Not configured'}
`; // Show action buttons for configured email if (actionsElement) { actionsElement.style.display = 'flex'; } } statusElement.className = `notification-status ${statusClass}`; statusElement.innerHTML = statusHtml; } catch (error) { console.error('Failed to load email status:', error); const statusElement = document.getElementById('email-status'); if (statusElement) { statusElement.className = 'notification-status status-error'; statusElement.innerHTML = `
Failed to load email status: ${error.message}
`; } } } // Load certificate monitoring status async function loadMonitoringStatus() { try { console.log('Loading monitoring status...'); const response = await apiRequest('/monitoring/status'); const statusElement = document.getElementById('monitoring-status'); const actionsElement = document.getElementById('monitoring-actions'); if (!statusElement) return; let statusHtml = ''; let statusClass = ''; if (!response.enabled) { statusClass = 'status-disabled'; statusHtml = `
Certificate monitoring is disabled

Set CERT_MONITORING_ENABLED=true to enable automatic monitoring

`; } else { statusClass = response.running ? 'status-enabled' : 'status-error'; const statusIcon = response.running ? 'check-circle' : 'pause-circle'; const statusText = response.running ? 'running' : 'stopped'; statusHtml = `
Certificate monitoring is ${statusText}
Schedule: ${response.schedule}
Warning Period: ${response.warningDays} days
Critical Period: ${response.criticalDays} days
Monitor Uploaded: ${response.includeUploaded ? 'Yes' : 'No'}
Email Alerts: ${response.emailEnabled ? 'Enabled' : 'Disabled'}
`; // Show action buttons if (actionsElement) { actionsElement.style.display = 'flex'; // Show appropriate start/stop buttons const startBtn = document.getElementById('start-monitoring-btn'); const stopBtn = document.getElementById('stop-monitoring-btn'); if (startBtn) { startBtn.style.display = response.running ? 'none' : 'inline-block'; } if (stopBtn) { stopBtn.style.display = response.running ? 'inline-block' : 'none'; } } } statusElement.className = `notification-status ${statusClass}`; statusElement.innerHTML = statusHtml; } catch (error) { console.error('Failed to load monitoring status:', error); const statusElement = document.getElementById('monitoring-status'); if (statusElement) { statusElement.className = 'notification-status status-error'; statusElement.innerHTML = `
Failed to load monitoring status: ${error.message}
`; } } } // Load expiring certificates async function loadExpiringCertificates() { try { console.log('Loading expiring certificates...'); const response = await apiRequest('/monitoring/expiring'); const cardElement = document.getElementById('expiring-certs-card'); const listElement = document.getElementById('expiring-certs-list'); if (!cardElement || !listElement) return; if (response.total === 0) { cardElement.style.display = 'none'; return; } cardElement.style.display = 'block'; let listHtml = `
Found ${response.total} expiring certificate(s): ${response.critical > 0 ? `${response.critical} critical` : ''} ${response.warning > 0 ? `${response.warning} warning` : ''}
`; response.certificates.forEach(cert => { const priority = cert.priority; const priorityText = priority === 'critical' ? '🚨 CRITICAL' : '⚠️ WARNING'; const priorityClass = priority; listHtml += `
${cert.path}
${cert.daysUntilExpiry} day${cert.daysUntilExpiry !== 1 ? 's' : ''} remaining
Expires: ${new Date(cert.expiry).toLocaleDateString()}
${cert.domains ? `
Domains: ${cert.domains.join(', ')}
` : ''}
${priorityText}
`; }); listElement.innerHTML = listHtml; } catch (error) { console.error('Failed to load expiring certificates:', error); const cardElement = document.getElementById('expiring-certs-card'); if (cardElement) { cardElement.style.display = 'none'; } } } // Test email configuration async function testEmailConfiguration() { const button = document.getElementById('test-email-btn'); if (!button) return; const originalText = button.innerHTML; try { button.innerHTML = ' Sending...'; button.disabled = true; const response = await apiRequest('/email/test', 'POST'); showAlert(`Test email sent successfully! Message ID: ${response.messageId}`, 'success'); } catch (error) { console.error('Email test failed:', error); showAlert(`Email test failed: ${error.message}`, 'error'); } finally { button.innerHTML = originalText; button.disabled = false; } } // Verify SMTP connection async function verifySmtpConnection() { const button = document.getElementById('verify-smtp-btn'); if (!button) return; const originalText = button.innerHTML; try { button.innerHTML = ' Verifying...'; button.disabled = true; const response = await apiRequest('/email/verify', 'POST'); showAlert('SMTP connection verified successfully!', 'success'); } catch (error) { console.error('SMTP verification failed:', error); showAlert(`SMTP verification failed: ${error.message}`, 'error'); } finally { button.innerHTML = originalText; button.disabled = false; } } // Check certificate expiry manually async function checkCertificateExpiry() { const button = document.getElementById('check-expiry-btn'); if (!button) return; const originalText = button.innerHTML; try { button.innerHTML = ' Checking...'; button.disabled = true; await apiRequest('/monitoring/check', 'POST'); showAlert('Certificate expiry check completed successfully!', 'success'); // Reload expiring certificates list setTimeout(() => { loadExpiringCertificates(); }, 1000); } catch (error) { console.error('Certificate check failed:', error); showAlert(`Certificate check failed: ${error.message}`, 'error'); } finally { button.innerHTML = originalText; button.disabled = false; } } // Start certificate monitoring async function startCertificateMonitoring() { const button = document.getElementById('start-monitoring-btn'); if (!button) return; const originalText = button.innerHTML; try { button.innerHTML = ' Starting...'; button.disabled = true; await apiRequest('/monitoring/start', 'POST'); showAlert('Certificate monitoring started successfully!', 'success'); // Reload monitoring status setTimeout(() => { loadMonitoringStatus(); }, 1000); } catch (error) { console.error('Failed to start monitoring:', error); showAlert(`Failed to start monitoring: ${error.message}`, 'error'); } finally { button.innerHTML = originalText; button.disabled = false; } } // Stop certificate monitoring async function stopCertificateMonitoring() { const button = document.getElementById('stop-monitoring-btn'); if (!button) return; const originalText = button.innerHTML; try { button.innerHTML = ' Stopping...'; button.disabled = true; await apiRequest('/monitoring/stop', 'POST'); showAlert('Certificate monitoring stopped successfully!', 'success'); // Reload monitoring status setTimeout(() => { loadMonitoringStatus(); }, 1000); } catch (error) { console.error('Failed to stop monitoring:', error); showAlert(`Failed to stop monitoring: ${error.message}`, 'error'); } finally { button.innerHTML = originalText; button.disabled = false; } }