diff --git a/plist b/plist index 6cc20c9df..5ec1bc5be 100644 --- a/plist +++ b/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 diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CaController.php b/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CaController.php index edc460d32..5972c1994 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CaController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CaController.php @@ -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'], ); } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CertController.php b/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CertController.php index bd5a1801c..fa2cbdf88 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CertController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CertController.php @@ -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, diff --git a/src/opnsense/mvc/app/views/OPNsense/Trust/ca.volt b/src/opnsense/mvc/app/views/OPNsense/Trust/ca.volt index 01225bf89..81673bf68 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Trust/ca.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Trust/ca.volt @@ -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); + }); diff --git a/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt b/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt index a2a6cd662..f7e12d662 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt @@ -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); + }); diff --git a/src/opnsense/www/js/widgets/Certificates.js b/src/opnsense/www/js/widgets/Certificates.js new file mode 100644 index 000000000..09b80d563 --- /dev/null +++ b/src/opnsense/www/js/widgets/Certificates.js @@ -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 = $('
'); + 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 = $(`
${message}
`); + $('#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') + ? `${item.descr}` + : `${item.descr}`; + + const row = ` +
+ + +   + ${descrContent} +
+
+ ${expirationText} +
+
`; + 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(); + } +} diff --git a/src/opnsense/www/js/widgets/Metadata/Core.xml b/src/opnsense/www/js/widgets/Metadata/Core.xml index 750fef0bd..90dbd7e2c 100644 --- a/src/opnsense/www/js/widgets/Metadata/Core.xml +++ b/src/opnsense/www/js/widgets/Metadata/Core.xml @@ -348,4 +348,22 @@ No picture was supplied for this system. + + Certificates.js + /ui/trust/cert + + /api/trust/ca/search + /api/trust/cert/search + + + Certificates + No certificates to display. + Expired + Valid + Expires in + Expired on + days + Hidden Certificates + +