From 19bac2645d0abced06fee561c3a8f168193fd873 Mon Sep 17 00:00:00 2001 From: Matteo Di Lorenzi Date: Wed, 25 Feb 2026 11:51:24 +0100 Subject: [PATCH 1/8] feat(openvpn-rw): add certificate management features for OpenVPN Road Warrior --- .../openvpn_rw/RWAccountsManager.vue | 4 +- .../standalone/openvpn_rw/RWAccountsTable.vue | 350 ++++++++++++------ .../standalone/openvpn_rw/RWServerDetails.vue | 146 ++++++-- .../RegenerateRWAllCertificatesModal.vue | 97 +++++ .../RenewRWServerCertificateModal.vue | 86 +++++ src/i18n/en.json | 22 +- src/i18n/it.json | 22 +- .../standalone/vpn/OpenvpnRoadWarriorView.vue | 27 ++ 8 files changed, 616 insertions(+), 138 deletions(-) create mode 100644 src/components/standalone/openvpn_rw/RegenerateRWAllCertificatesModal.vue create mode 100644 src/components/standalone/openvpn_rw/RenewRWServerCertificateModal.vue diff --git a/src/components/standalone/openvpn_rw/RWAccountsManager.vue b/src/components/standalone/openvpn_rw/RWAccountsManager.vue index 340bdcc3b..25318bda6 100644 --- a/src/components/standalone/openvpn_rw/RWAccountsManager.vue +++ b/src/components/standalone/openvpn_rw/RWAccountsManager.vue @@ -279,7 +279,7 @@ function clearFilters() { {{ t('standalone.openvpn_rw.download_all_configs') }} - + diff --git a/src/components/standalone/openvpn_rw/RWAccountsTable.vue b/src/components/standalone/openvpn_rw/RWAccountsTable.vue index 131049573..747ae3a68 100644 --- a/src/components/standalone/openvpn_rw/RWAccountsTable.vue +++ b/src/components/standalone/openvpn_rw/RWAccountsTable.vue @@ -16,13 +16,17 @@ import { NeTableRow, NeTableCell, NePaginator, - useItemPagination + useItemPagination, + NeSortDropdown, + type SortEvent, + NeBadge, + NeAvatar } from '@nethesis/vue-components' import type { RWAuthenticationMode, RWAccount } from '@/views/standalone/vpn/OpenvpnRoadWarriorView.vue' -import { ref } from 'vue' +import { ref, computed } from 'vue' import { faArrowsRotate, faCircleArrowDown, @@ -30,7 +34,10 @@ import { faCircleXmark, faCircleInfo, faTrash, - faPenToSquare + faPenToSquare, + faXmark, + faTriangleExclamation, + faCircleExclamation } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' @@ -51,8 +58,54 @@ const emit = defineEmits<{ regenerateCertificate: [item: RWAccount] }>() +const sortKey = ref<'name' | 'connected'>('connected') +const sortDescending = ref(false) + +const usersData = computed(() => props.users) + +// custom sorted items that keeps disabled items always at the end when sorting by connection status +const sortedItems = computed(() => { + let items = [...usersData.value] + + if (sortKey.value === 'connected') { + // separate enabled and disabled items + const enabledItems = items.filter(item => item.openvpn_enabled !== '0') + const disabledItems = items.filter(item => item.openvpn_enabled === '0') + + // check if all enabled items have the same connection status + const allConnected = enabledItems.every(item => item.connected === true) + const allNotConnected = enabledItems.every(item => item.connected === false) + const allSameStatus = allConnected || allNotConnected + + // only sort if items have different connection statuses + if (!allSameStatus) { + enabledItems.sort((a, b) => { + const aConnected = a.connected ? 1 : 0 + const bConnected = b.connected ? 1 : 0 + return bConnected - aConnected + }) + + // apply descending if needed + if (sortDescending.value) { + enabledItems.reverse() + } + } + + return [...enabledItems, ...disabledItems] + } else { + // for 'name' sorting, use standard sort + items.sort((a, b) => a.name.localeCompare(b.name)) + + if (sortDescending.value) { + items.reverse() + } + + return items + } +}) + const pageSize = ref(10) -const { currentPage, paginatedItems } = useItemPagination(() => props.users, { +const { currentPage, paginatedItems } = useItemPagination(() => sortedItems.value, { itemsPerPage: pageSize }) @@ -121,21 +174,60 @@ function getDropdownItems(item: RWAccount) { function getCellClasses(item: RWAccount) { return item.openvpn_enabled === '0' ? ['opacity-50'] : [] } + +const onSort = (payload: SortEvent) => { + sortKey.value = payload.key as 'name' | 'connected' + sortDescending.value = payload.descending +} + +// certificates expiration warning +const CERT_EXPIRY_WARNING_DAYS = 30 + +function getDaysUntilExpiry(expiryTimestamp: number): number { + const secondsUntilExpiry = expiryTimestamp - Date.now() / 1000 + return Math.floor(secondsUntilExpiry / 86400) +} + +function isCertificateExpiringSoon(expiryTimestamp: number): boolean { + const daysUntilExpiry = getDaysUntilExpiry(expiryTimestamp) + return daysUntilExpiry < CERT_EXPIRY_WARNING_DAYS && daysUntilExpiry >= 0 +} +