mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-13 00:07:26 +00:00
dashboard: Add certificate widget that displays CAs and Certs sorted by expiration date (#8105)
* dashboard: Add certificate widget that displays CAs and Certs sorted by expiration date * dashboard: Certificate widget, fix certificate hiding configuration, refresh immediately on config change, increase tick timeout * dashboard: Certificate widget, different text for expired certificates * dashboard: Certificate widget, create links that fill the search-field of the bootgrid to display the certificate directly * dashboard: Certificate widget, search for uuid in bootgrid and call corresponding form
This commit is contained in:
parent
f4b9017cd9
commit
397a3dcdce
1
plist
1
plist
@ -2070,6 +2070,7 @@
|
||||
/usr/local/opnsense/www/js/widgets/BaseTableWidget.js
|
||||
/usr/local/opnsense/www/js/widgets/BaseWidget.js
|
||||
/usr/local/opnsense/www/js/widgets/Carp.js
|
||||
/usr/local/opnsense/www/js/widgets/Certificates.js
|
||||
/usr/local/opnsense/www/js/widgets/Cpu.js
|
||||
/usr/local/opnsense/www/js/widgets/Disk.js
|
||||
/usr/local/opnsense/www/js/widgets/Firewall.js
|
||||
|
||||
@ -142,7 +142,7 @@ class CaController extends ApiMutableModelControllerBase
|
||||
{
|
||||
return $this->searchBase(
|
||||
'ca',
|
||||
['refid', 'descr', 'caref', 'name', 'refcount', 'valid_from', 'valid_to'],
|
||||
['uuid', 'refid', 'descr', 'caref', 'name', 'refcount', 'valid_from', 'valid_to'],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -186,7 +186,7 @@ class CertController extends ApiMutableModelControllerBase
|
||||
return $this->searchBase(
|
||||
'cert',
|
||||
[
|
||||
'refid', 'descr', 'caref', 'rfc3280_purpose', 'name',
|
||||
'uuid', 'refid', 'descr', 'caref', 'rfc3280_purpose', 'name',
|
||||
'valid_from', 'valid_to' , 'in_use', 'is_user', 'commonname'
|
||||
],
|
||||
null,
|
||||
|
||||
@ -160,6 +160,34 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* For certificate dashboard widget */
|
||||
function handleSearchAndEdit() {
|
||||
const hash = window.location.hash;
|
||||
|
||||
if (hash.includes('#SearchPhrase=')) {
|
||||
const searchPhrase = decodeURIComponent(hash.split('=')[1]);
|
||||
const searchField = $('.search-field');
|
||||
|
||||
if (searchField.val() !== searchPhrase) {
|
||||
searchField.val(searchPhrase).trigger('keyup');
|
||||
|
||||
// Wait for grid to reload after search and simulate edit button click
|
||||
$('#grid-cert').one("loaded.rs.jquery.bootgrid", function () {
|
||||
const editButton = $(`#grid-cert .command-edit[data-row-id="${searchPhrase}"]`);
|
||||
if (editButton.length) {
|
||||
editButton.trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
history.replaceState(null, null, window.location.pathname + window.location.search);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#grid-cert').on("loaded.rs.jquery.bootgrid", handleSearchAndEdit);
|
||||
$(window).on('hashchange', handleSearchAndEdit);
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@ -281,6 +281,33 @@
|
||||
}
|
||||
});
|
||||
|
||||
/* For certificate dashboard widget */
|
||||
function handleSearchAndEdit() {
|
||||
const hash = window.location.hash;
|
||||
|
||||
if (hash.includes('#SearchPhrase=')) {
|
||||
const searchPhrase = decodeURIComponent(hash.split('=')[1]);
|
||||
const searchField = $('.search-field');
|
||||
|
||||
if (searchField.val() !== searchPhrase) {
|
||||
searchField.val(searchPhrase).trigger('keyup');
|
||||
|
||||
// Wait for grid to reload after search and simulate edit button click
|
||||
$('#grid-cert').one("loaded.rs.jquery.bootgrid", function () {
|
||||
const editButton = $(`#grid-cert .command-edit[data-row-id="${searchPhrase}"]`);
|
||||
if (editButton.length) {
|
||||
editButton.trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
history.replaceState(null, null, window.location.pathname + window.location.search);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#grid-cert').on("loaded.rs.jquery.bootgrid", handleSearchAndEdit);
|
||||
$(window).on('hashchange', handleSearchAndEdit);
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
188
src/opnsense/www/js/widgets/Certificates.js
Normal file
188
src/opnsense/www/js/widgets/Certificates.js
Normal file
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Cedrik Pischem
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
export default class Certificates extends BaseTableWidget {
|
||||
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.tickTimeout = 180;
|
||||
this.configurable = true;
|
||||
this.configChanged = false;
|
||||
}
|
||||
|
||||
getGridOptions() {
|
||||
return {
|
||||
sizeToContent: 650
|
||||
};
|
||||
}
|
||||
|
||||
getMarkup() {
|
||||
const $container = $('<div></div>');
|
||||
const $certificateTable = this.createTable('certificateTable', {
|
||||
headerPosition: 'none'
|
||||
});
|
||||
|
||||
$container.append($certificateTable);
|
||||
return $container;
|
||||
}
|
||||
|
||||
async onWidgetTick() {
|
||||
const cas = (await this.ajaxCall('/api/trust/ca/search')).rows || [];
|
||||
const certs = (await this.ajaxCall('/api/trust/cert/search')).rows || [];
|
||||
|
||||
if (cas.length === 0 && certs.length === 0) {
|
||||
this.displayError(`${this.translations.noitems}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearError();
|
||||
await this.processCertificates(cas, certs);
|
||||
}
|
||||
|
||||
displayError(message) {
|
||||
const $error = $(`<div class="error-message">${message}</div>`);
|
||||
$('#certificateTable').empty().append($error);
|
||||
}
|
||||
|
||||
clearError() {
|
||||
$('#certificateTable .error-message').remove();
|
||||
}
|
||||
|
||||
processItems(items, type, hiddenItems, rows) {
|
||||
items.forEach(item => {
|
||||
if (!hiddenItems.includes(item.descr)) {
|
||||
const validTo = new Date(parseInt(item.valid_to) * 1000);
|
||||
const now = new Date();
|
||||
const remainingDays = Math.max(0, Math.floor((validTo - now) / (1000 * 60 * 60 * 24)));
|
||||
|
||||
const colorClass = remainingDays === 0
|
||||
? 'text-danger'
|
||||
: remainingDays < 14
|
||||
? 'text-warning'
|
||||
: 'text-success';
|
||||
|
||||
const statusText = remainingDays === 0 ? this.translations.expired : this.translations.valid;
|
||||
const iconClass = remainingDays === 0 ? 'fa fa-unlock' : 'fa fa-lock';
|
||||
|
||||
const expirationText = remainingDays === 0
|
||||
? `${this.translations.expiredon} ${validTo.toLocaleString()}`
|
||||
: `${this.translations.expiresin} ${remainingDays} ${this.translations.days}, ${validTo.toLocaleString()}`;
|
||||
|
||||
const descrContent = (type === 'cert' || type === 'ca')
|
||||
? `<a href="/ui/trust/${type === 'cert' ? 'cert' : 'ca'}#SearchPhrase=${encodeURIComponent(item.uuid)}" class="${type}-link">${item.descr}</a>`
|
||||
: `<b>${item.descr}</b>`;
|
||||
|
||||
const row = `
|
||||
<div>
|
||||
<i class="${iconClass} ${colorClass} certificate-tooltip" style="cursor: pointer;"
|
||||
data-tooltip="${type}-${item.descr}" title="${statusText}">
|
||||
</i>
|
||||
|
||||
<span>${descrContent}</span>
|
||||
<br/>
|
||||
<div style="margin-top: 5px; margin-bottom: 5px;">
|
||||
${expirationText}
|
||||
</div>
|
||||
</div>`;
|
||||
rows.push({ html: row, expirationDate: validTo });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async processCertificates(cas, certs) {
|
||||
const config = await this.getWidgetConfig() || {};
|
||||
|
||||
if (!this.dataChanged('certificates', { cas, certs }) && !this.configChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.configChanged) {
|
||||
this.configChanged = false;
|
||||
}
|
||||
|
||||
$('.certificate-tooltip').tooltip('hide');
|
||||
|
||||
const hiddenItems = config.hiddenItems || [];
|
||||
const rows = [];
|
||||
|
||||
if (cas.length > 0) {
|
||||
this.processItems(cas, 'ca', hiddenItems, rows);
|
||||
}
|
||||
|
||||
if (certs.length > 0) {
|
||||
this.processItems(certs, 'cert', hiddenItems, rows);
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
this.displayError(`${this.translations.noitems}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort rows by expiration date from earliest to latest
|
||||
rows.sort((a, b) => a.expirationDate - b.expirationDate);
|
||||
|
||||
const sortedRows = rows.map(row => [row.html]);
|
||||
super.updateTable('certificateTable', sortedRows);
|
||||
|
||||
$('.certificate-tooltip').tooltip({ container: 'body' });
|
||||
}
|
||||
|
||||
async getWidgetOptions() {
|
||||
const [caResponse, certResponse] = await Promise.all([
|
||||
this.ajaxCall('/api/trust/ca/search'),
|
||||
this.ajaxCall('/api/trust/cert/search')
|
||||
]);
|
||||
|
||||
const hiddenItemOptions = [];
|
||||
|
||||
if (caResponse.rows) {
|
||||
caResponse.rows.forEach(ca => {
|
||||
hiddenItemOptions.push({ value: `${ca.descr}`, label: ca.descr });
|
||||
});
|
||||
}
|
||||
|
||||
if (certResponse.rows) {
|
||||
certResponse.rows.forEach(cert => {
|
||||
hiddenItemOptions.push({ value: `${cert.descr}`, label: cert.descr });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hiddenItems: {
|
||||
title: this.translations.hiddenitems,
|
||||
type: 'select_multiple',
|
||||
options: hiddenItemOptions,
|
||||
default: [],
|
||||
required: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async onWidgetOptionsChanged() {
|
||||
this.configChanged = true;
|
||||
await this.onWidgetTick();
|
||||
}
|
||||
}
|
||||
@ -348,4 +348,22 @@
|
||||
<nopicture>No picture was supplied for this system.</nopicture>
|
||||
</translations>
|
||||
</picture>
|
||||
<certificates>
|
||||
<filename>Certificates.js</filename>
|
||||
<link>/ui/trust/cert</link>
|
||||
<endpoints>
|
||||
<endpoint>/api/trust/ca/search</endpoint>
|
||||
<endpoint>/api/trust/cert/search</endpoint>
|
||||
</endpoints>
|
||||
<translations>
|
||||
<title>Certificates</title>
|
||||
<noitems>No certificates to display.</noitems>
|
||||
<expired>Expired</expired>
|
||||
<valid>Valid</valid>
|
||||
<expiresin>Expires in</expiresin>
|
||||
<expiredon>Expired on</expiredon>
|
||||
<days>days</days>
|
||||
<hiddenitems>Hidden Certificates</hiddenitems>
|
||||
</translations>
|
||||
</certificates>
|
||||
</metadata>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user