229 lines
8.0 KiB
JavaScript
229 lines
8.0 KiB
JavaScript
|
|
// certificates.js - OpenVPN Certificate Statistics Logic
|
||
|
|
|
||
|
|
// Access globals defined in PHP
|
||
|
|
const { apiUrl, refreshTime } = window.AppConfig;
|
||
|
|
|
||
|
|
let refreshInterval;
|
||
|
|
let allCertificatesData = [];
|
||
|
|
let hideExpired = false;
|
||
|
|
|
||
|
|
function getStatusBadge(daysRemaining) {
|
||
|
|
if (!daysRemaining || daysRemaining === 'N/A') {
|
||
|
|
return '<span class="status-badge text-muted">Unknown</span>';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (daysRemaining.includes('Expired')) {
|
||
|
|
return '<span class="status-badge status-expired"><i class="fas fa-times-circle me-1"></i>Expired</span>';
|
||
|
|
} else if (daysRemaining.includes('days')) {
|
||
|
|
const days = parseInt(daysRemaining);
|
||
|
|
if (days <= 30) {
|
||
|
|
return '<span class="status-badge status-expiring"><i class="fas fa-exclamation-triangle me-1"></i>Expiring Soon</span>';
|
||
|
|
} else {
|
||
|
|
return '<span class="status-badge status-valid"><i class="fas fa-check-circle me-1"></i>Valid</span>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return '<span class="status-badge text-muted">Unknown</span>';
|
||
|
|
}
|
||
|
|
|
||
|
|
function categorizeCertificates(certificates) {
|
||
|
|
const active = [];
|
||
|
|
const expired = [];
|
||
|
|
|
||
|
|
certificates.forEach(cert => {
|
||
|
|
if (!cert.days_remaining || cert.days_remaining === 'N/A') {
|
||
|
|
// If days_remaining is not available, check not_after date
|
||
|
|
if (cert.not_after && cert.not_after !== 'N/A') {
|
||
|
|
try {
|
||
|
|
const expDate = new Date(cert.not_after);
|
||
|
|
const now = new Date();
|
||
|
|
if (expDate < now) {
|
||
|
|
expired.push(cert);
|
||
|
|
} else {
|
||
|
|
active.push(cert);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
// If we can't parse the date, consider it active
|
||
|
|
active.push(cert);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// If no expiration info, consider it active
|
||
|
|
active.push(cert);
|
||
|
|
}
|
||
|
|
} else if (cert.days_remaining.includes('Expired')) {
|
||
|
|
expired.push(cert);
|
||
|
|
} else {
|
||
|
|
active.push(cert);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Sort active certificates by days remaining (ascending)
|
||
|
|
active.sort((a, b) => {
|
||
|
|
try {
|
||
|
|
const aDays = a.days_remaining && a.days_remaining !== 'N/A' ? parseInt(a.days_remaining) : 9999;
|
||
|
|
const bDays = b.days_remaining && b.days_remaining !== 'N/A' ? parseInt(b.days_remaining) : 9999;
|
||
|
|
return aDays - bDays;
|
||
|
|
} catch (e) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Sort expired certificates by days expired (descending)
|
||
|
|
expired.sort((a, b) => {
|
||
|
|
try {
|
||
|
|
const aMatch = a.days_remaining ? a.days_remaining.match(/\d+/) : [0];
|
||
|
|
const bMatch = b.days_remaining ? b.days_remaining.match(/\d+/) : [0];
|
||
|
|
const aDays = parseInt(aMatch[0]);
|
||
|
|
const bDays = parseInt(bMatch[0]);
|
||
|
|
return bDays - aDays;
|
||
|
|
} catch (e) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return { active, expired };
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderSingleTable(active, expired, tableId) {
|
||
|
|
const tableBody = document.getElementById(tableId);
|
||
|
|
if ((!active || active.length === 0) && (!expired || expired.length === 0)) {
|
||
|
|
tableBody.innerHTML = `
|
||
|
|
<tr>
|
||
|
|
<td colspan="4" class="empty-state">
|
||
|
|
<i class="fas fa-certificate"></i>
|
||
|
|
<p>No certificates found</p>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let html = '';
|
||
|
|
|
||
|
|
// Render Active
|
||
|
|
if (active && active.length > 0) {
|
||
|
|
html += `<tr class="section-divider"><td colspan="4">Active Certificates (${active.length})</td></tr>`;
|
||
|
|
active.forEach(cert => {
|
||
|
|
html += generateRow(cert, false);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render Expired
|
||
|
|
if (expired && expired.length > 0 && !hideExpired) {
|
||
|
|
html += `<tr class="section-divider"><td colspan="4">Expired Certificates (${expired.length})</td></tr>`;
|
||
|
|
expired.forEach(cert => {
|
||
|
|
html += generateRow(cert, true);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
tableBody.innerHTML = html;
|
||
|
|
}
|
||
|
|
|
||
|
|
function generateRow(cert, isExpired) {
|
||
|
|
const commonName = cert.common_name || cert.subject || 'N/A';
|
||
|
|
const clientName = commonName.replace('CN=', '').trim();
|
||
|
|
let daysText = cert.days_remaining || 'N/A';
|
||
|
|
|
||
|
|
if (isExpired && daysText.includes('Expired')) {
|
||
|
|
daysText = daysText.replace('Expired (', '').replace(' days ago)', '') + ' days ago';
|
||
|
|
}
|
||
|
|
|
||
|
|
return `
|
||
|
|
<tr>
|
||
|
|
<td>
|
||
|
|
<div class="fw-semibold">${clientName}</div>
|
||
|
|
<div class="certificate-file">${cert.file || 'N/A'}</div>
|
||
|
|
</td>
|
||
|
|
<td>${formatCertDate(cert.not_after)}</td>
|
||
|
|
<td class="fw-semibold ${isExpired ? 'text-danger' :
|
||
|
|
(daysText !== 'N/A' && parseInt(daysText) <= 30 ? 'text-warning' : 'text-success')
|
||
|
|
}">
|
||
|
|
${daysText}
|
||
|
|
</td>
|
||
|
|
<td>${getStatusBadge(cert.days_remaining)}</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateSummaryStats(certificates) {
|
||
|
|
const totalCertificates = document.getElementById('totalCertificates');
|
||
|
|
const activeCertificates = document.getElementById('activeCertificates');
|
||
|
|
const expiredCertificates = document.getElementById('expiredCertificates');
|
||
|
|
const expiringSoon = document.getElementById('expiringSoon');
|
||
|
|
const activeCount = document.getElementById('activeCount');
|
||
|
|
const expiredCount = document.getElementById('expiredCount');
|
||
|
|
const certCount = document.getElementById('certCount');
|
||
|
|
|
||
|
|
const { active, expired } = categorizeCertificates(certificates);
|
||
|
|
|
||
|
|
let expiringSoonCount = 0;
|
||
|
|
active.forEach(cert => {
|
||
|
|
if (cert.days_remaining && cert.days_remaining !== 'N/A') {
|
||
|
|
const days = parseInt(cert.days_remaining);
|
||
|
|
if (days <= 30) {
|
||
|
|
expiringSoonCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
totalCertificates.textContent = certificates.length;
|
||
|
|
activeCertificates.textContent = active.length;
|
||
|
|
expiredCertificates.textContent = expired.length;
|
||
|
|
expiringSoon.textContent = expiringSoonCount;
|
||
|
|
// Update counts in badges
|
||
|
|
document.getElementById('activeCount').textContent = active.length;
|
||
|
|
document.getElementById('expiredCount').textContent = expired.length;
|
||
|
|
certCount.textContent = certificates.length + ' certificate' + (certificates.length !== 1 ? 's' : '');
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterCertificates() {
|
||
|
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||
|
|
|
||
|
|
let filteredCertificates = allCertificatesData;
|
||
|
|
|
||
|
|
if (searchTerm) {
|
||
|
|
filteredCertificates = allCertificatesData.filter(cert => {
|
||
|
|
const commonName = (cert.common_name || cert.subject || '').toLowerCase();
|
||
|
|
const fileName = (cert.file || '').toLowerCase();
|
||
|
|
return commonName.includes(searchTerm) || fileName.includes(searchTerm);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const { active, expired } = categorizeCertificates(filteredCertificates);
|
||
|
|
|
||
|
|
renderSingleTable(active, expired, 'certificatesTable');
|
||
|
|
updateSummaryStats(filteredCertificates);
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleExpiredCertificates() {
|
||
|
|
hideExpired = document.getElementById('hideExpired').checked;
|
||
|
|
const expiredCard = document.getElementById('certificatesCard'); // We just re-render
|
||
|
|
filterCertificates();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchData() {
|
||
|
|
document.getElementById('refreshIcon').classList.add('refresh-indicator');
|
||
|
|
try {
|
||
|
|
const response = await fetch(apiUrl);
|
||
|
|
const json = await response.json();
|
||
|
|
if (json.success) {
|
||
|
|
allCertificatesData = json.data;
|
||
|
|
filterCertificates(); // This also renders tables
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error("Fetch error:", e);
|
||
|
|
} finally {
|
||
|
|
document.getElementById('refreshIcon').classList.remove('refresh-indicator');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ensure functionality is available for HTML event attributes
|
||
|
|
window.fetchData = fetchData;
|
||
|
|
window.filterCertificates = filterCertificates;
|
||
|
|
window.toggleExpiredCertificates = toggleExpiredCertificates;
|
||
|
|
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
initTheme();
|
||
|
|
fetchData();
|
||
|
|
refreshInterval = setInterval(fetchData, refreshTime);
|
||
|
|
});
|