secubox-openwrt/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/certificates.js
2025-12-29 02:02:44 +01:00

215 lines
6.2 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require vhost-manager/api as API';
'require secubox-theme/theme as Theme';
'require vhost-manager/ui as VHostUI';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
function normalizeCerts(payload) {
if (Array.isArray(payload))
return payload;
if (payload && Array.isArray(payload.certificates))
return payload.certificates;
return [];
}
function daysUntil(dateStr) {
if (!dateStr)
return null;
var ts = Date.parse(dateStr);
if (isNaN(ts))
return null;
return Math.round((ts - Date.now()) / (1000 * 60 * 60 * 24));
}
function formatDate(dateStr) {
if (!dateStr)
return _('N/A');
try {
return new Date(dateStr).toLocaleString();
} catch (err) {
return dateStr;
}
}
return L.view.extend({
load: function() {
return Promise.all([
API.listCerts(),
API.getStatus()
]);
},
render: function(data) {
var certs = normalizeCerts(data[0]);
var status = data[1] || {};
return E('div', { 'class': 'vhost-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/dashboard.css') }),
VHostUI.renderTabs('certificates'),
this.renderHeader(certs, status),
this.renderRequestCard(),
this.renderCertTable(certs)
]);
},
renderHeader: function(certs, status) {
var expiringSoon = certs.filter(function(cert) {
var days = daysUntil(cert.expires);
return days !== null && days <= 30;
}).length;
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🔐'),
_('SSL Certificates')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Request Let\'s Encrypt certificates and monitor expiry across all proxies.'))
]),
E('div', { 'class': 'sh-stats-grid' }, [
this.renderStatBadge(certs.length, _('Installed')),
this.renderStatBadge(expiringSoon, _('Expiring < 30d')),
this.renderStatBadge(status.acme_available ? _('ACME Ready') : _('ACME Missing'), _('Automation')),
this.renderStatBadge(status.nginx_running ? _('Nginx OK') : _('Nginx down'), _('Web server'))
])
]);
},
renderStatBadge: function(value, label) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, value.toString()),
E('div', { 'class': 'sh-stat-label' }, label)
]);
},
renderRequestCard: function() {
var domainInput = E('input', { 'type': 'text', 'placeholder': 'cloud.example.com' });
var emailInput = E('input', { 'type': 'email', 'placeholder': 'admin@example.com' });
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🪄', _('Request Certificate')]),
E('p', { 'class': 'vhost-card-meta' }, _('Issue a Let\'s Encrypt certificate using HTTP-01 validation.')),
E('div', { 'class': 'vhost-form-grid' }, [
E('div', {}, [
E('label', {}, _('Domain')),
domainInput
]),
E('div', {}, [
E('label', {}, _('Contact Email')),
emailInput
])
]),
E('div', { 'class': 'vhost-actions' }, [
E('button', {
'class': 'sh-btn-primary',
'click': this.requestCert.bind(this, domainInput, emailInput)
}, _('Request certificate'))
])
]);
},
renderCertTable: function(certs) {
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['📋', _('Installed Certificates')]),
certs.length ? E('table', { 'class': 'vhost-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('Domain')),
E('th', {}, _('Issuer')),
E('th', {}, _('Expires')),
E('th', {}, _('Status')),
E('th', {}, _('Actions'))
])),
E('tbody', {},
certs.map(this.renderCertRow, this))
]) : E('div', { 'class': 'vhost-empty' }, _('No certificates issued yet.'))
]);
},
renderCertRow: function(cert) {
var days = daysUntil(cert.expires);
var pill = 'success';
var label = _('Valid');
if (days === null) {
pill = 'danger';
label = _('Unknown');
} else if (days <= 7) {
pill = 'danger';
label = _('Expiring in %d days').format(days);
} else if (days <= 30) {
pill = 'warn';
label = _('Renew soon (%d days)').format(days);
}
return E('tr', {}, [
E('td', {}, cert.domain),
E('td', {}, cert.issuer || _('Unknown')),
E('td', {}, formatDate(cert.expires)),
E('td', {}, E('span', { 'class': 'vhost-pill ' + pill }, label)),
E('td', {}, E('button', {
'class': 'cbi-button cbi-button-action',
'click': function(ev) {
ev.preventDefault();
ui.showModal(_('Certificate Details'), [
E('p', {}, [
E('strong', {}, _('Domain: ')),
E('span', {}, cert.domain)
]),
E('p', {}, [
E('strong', {}, _('Subject: ')),
E('span', {}, cert.subject || _('Unknown'))
]),
E('p', {}, [
E('strong', {}, _('Issuer: ')),
E('span', {}, cert.issuer || _('Unknown'))
]),
E('p', {}, [
E('strong', {}, _('Expires: ')),
E('span', {}, formatDate(cert.expires))
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.hideModal
}, _('Close'))
])
]);
}
}, _('Details')))
]);
},
requestCert: function(domainInput, emailInput, ev) {
if (ev)
ev.preventDefault();
var domain = domainInput.value.trim();
var email = emailInput.value.trim();
if (!domain || !email) {
ui.addNotification(null, E('p', _('Domain and email are required')), 'error');
return;
}
ui.addNotification(null, E('p', _('Requesting certificate... This may take a few minutes.')), 'info');
API.requestCert(domain, email).then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', '✓ ' + _('Certificate obtained successfully')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', '✗ ' + (result.message || _('Request failed'))), 'error');
}
});
}
});