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.
';
}).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.