This commit is contained in:
Mateusz Gruszczyński
2025-11-04 09:56:37 +01:00
parent 32ef62e4ac
commit addb21bc3e
34 changed files with 3864 additions and 367 deletions

219
static/js/cert_manager.js Normal file
View File

@@ -0,0 +1,219 @@
/**
* Certificate Manager - Upload, List, Delete
*/
let currentCertId = null;
document.addEventListener('DOMContentLoaded', function() {
loadCertificates();
document.getElementById('uploadBtn').addEventListener('click', uploadCertificate);
document.getElementById('exportBtn').addEventListener('click', exportCertificate);
});
function loadCertificates() {
fetch('/api/certificates')
.then(r => r.json())
.then(data => {
if (data.success) {
renderCertificates(data.certificates);
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => console.error('Error loading certificates:', e));
}
function renderCertificates(certs) {
const tbody = document.getElementById('certsList');
if (certs.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No certificates uploaded yet</td></tr>';
return;
}
tbody.innerHTML = certs.map(c => {
const expiresDate = c.expires_at ? new Date(c.expires_at) : null;
const isExpired = c.is_expired;
const expiresText = expiresDate ? formatDate(expiresDate) : 'Unknown';
return `
<tr ${isExpired ? 'class="table-danger"' : ''}>
<td><strong>${escapeHtml(c.name)}</strong></td>
<td><code>${escapeHtml(c.common_name || 'N/A')}</code></td>
<td>
${isExpired ? '<span class="badge bg-danger">EXPIRED</span>' : ''}
<small>${expiresText}</small>
</td>
<td>
${c.vhost_count > 0
? `<span class="badge bg-info">${c.vhost_count}</span>`
: '<span class="text-muted">Not used</span>'}
</td>
<td>
${isExpired
? '<span class="badge bg-danger">Expired</span>'
: '<span class="badge bg-success">Valid</span>'}
</td>
<td>
<button class="btn btn-sm btn-info" onclick="viewDetails(${c.id})">
<i class="bi bi-eye"></i> View
</button>
${c.vhost_count === 0 ? `
<button class="btn btn-sm btn-danger" onclick="deleteCert(${c.id}, '${escapeHtml(c.name)}')">
<i class="bi bi-trash"></i>
</button>
` : ''}
</td>
</tr>
`;
}).join('');
}
function uploadCertificate() {
const name = document.getElementById('cert_name').value;
const file = document.getElementById('cert_file').files[0];
if (!name || !file) {
showAlert('Certificate name and file are required', 'warning');
return;
}
const formData = new FormData();
formData.append('name', name);
formData.append('cert_file', file);
fetch('/api/certificates', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('uploadModal')).hide();
document.getElementById('uploadForm').reset();
loadCertificates();
showAlert('Certificate uploaded successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function viewDetails(certId) {
currentCertId = certId;
fetch(`/api/certificates/${certId}`)
.then(r => r.json())
.then(data => {
if (data.success) {
const c = data.certificate;
document.getElementById('detailsName').textContent = c.name;
const san = c.subject_alt_names && c.subject_alt_names.length > 0
? c.subject_alt_names.join(', ')
: 'No SAN';
const vhosts = c.vhosts && c.vhosts.length > 0
? c.vhosts.map(v => `<li>${v.name} (${v.hostname})</li>`).join('')
: '<li class="text-muted">Not used</li>';
const detailsHTML = `
<table class="table table-sm">
<tr>
<th>Common Name</th>
<td><code>${escapeHtml(c.common_name || 'N/A')}</code></td>
</tr>
<tr>
<th>Subject Alt Names</th>
<td><code>${escapeHtml(san)}</code></td>
</tr>
<tr>
<th>Issued</th>
<td>${c.issued_at ? formatDate(new Date(c.issued_at)) : 'Unknown'}</td>
</tr>
<tr>
<th>Expires</th>
<td>
${c.expires_at ? formatDate(new Date(c.expires_at)) : 'Unknown'}
${new Date(c.expires_at) < new Date() ? '<span class="badge bg-danger ms-2">EXPIRED</span>' : ''}
</td>
</tr>
<tr>
<th>Created</th>
<td>${formatDate(new Date(c.created_at))}</td>
</tr>
</table>
<h6>Used by VHosts:</h6>
<ul>${vhosts}</ul>
`;
document.getElementById('detailsContent').innerHTML = detailsHTML;
new bootstrap.Modal(document.getElementById('detailsModal')).show();
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function exportCertificate() {
fetch(`/api/certificates/${currentCertId}/export`)
.then(r => r.json())
.then(data => {
if (data.success) {
// Create download link
const blob = new Blob([data.content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${data.name}.pem`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function deleteCert(certId, name) {
if (!confirm(`Delete certificate '${name}'? This cannot be undone.`)) return;
fetch(`/api/certificates/${certId}`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
if (data.success) {
loadCertificates();
showAlert('Certificate deleted successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.card').parentElement.insertBefore(alertDiv, document.querySelector('.card'));
setTimeout(() => alertDiv.remove(), 5000);
}
function formatDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
}
function escapeHtml(text) {
const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
return text.replace(/[&<>"']/g, m => map[m]);
}