Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
Copyright (C) 2026 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

Expand Down Expand Up @@ -670,6 +670,7 @@ watch(
v-model="name"
:disabled="id != ''"
:label="t('standalone.openvpn_tunnel.tunnel_name')"
:helper-text="t('standalone.openvpn_tunnel.tunnel_name_max_length')"
:invalid-message="validationErrorBag.getFirstFor('ns_name')"
/>
<template v-if="!isClientTunnel">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
Copyright (C) 2026 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

Expand Down Expand Up @@ -57,6 +57,7 @@ function close() {
kind="warning"
:title="t('standalone.openvpn_tunnel.delete_tunnel')"
:primary-label="t('common.delete')"
:cancel-label="t('common.cancel')"
:primary-button-disabled="isDeleting"
:primary-button-loading="isDeleting"
primary-button-kind="danger"
Expand Down
82 changes: 82 additions & 0 deletions src/components/standalone/openvpn_tunnel/RegenerateCertsModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!--
Copyright (C) 2026 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script setup lang="ts">
import { NeInlineNotification, getAxiosErrorMessage } from '@nethesis/vue-components'
import { NeModal } from '@nethesis/vue-components'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ServerTunnel, ClientTunnel } from './TunnelManager.vue'
import { ubusCall } from '@/lib/standalone/ubus'

const { t } = useI18n()

const props = defineProps<{
visible: boolean
itemToRegenerate: ServerTunnel | ClientTunnel | null
}>()

const emit = defineEmits(['close', 'certs-regenerated'])

const error = ref({
notificationDescription: '',
notificationDetails: ''
})
const isRegenerating = ref(false)

async function regenerateCerts() {
if (props.itemToRegenerate) {
error.value.notificationDescription = ''
error.value.notificationDetails = ''
isRegenerating.value = true
try {
await ubusCall('ns.ovpntunnel', 'regenerate-server-certs', {
id: props.itemToRegenerate.id
})
emit('certs-regenerated')
emit('close')
} catch (err: any) {
error.value.notificationDescription = t(getAxiosErrorMessage(err))
error.value.notificationDetails = err.toString()
} finally {
isRegenerating.value = false
}
}
}

function close() {
error.value.notificationDescription = ''
error.value.notificationDetails = ''
emit('close')
}
</script>

<template>
<NeModal
:visible="visible"
kind="warning"
:title="t('standalone.openvpn_tunnel.regenerate_cert')"
:primary-label="t('standalone.openvpn_tunnel.regenerate_cert_button')"
:cancel-label="t('common.cancel')"
:primary-button-disabled="isRegenerating"
:primary-button-loading="isRegenerating"
:close-aria-label="t('common.close')"
@primary-click="regenerateCerts()"
@close="close()"
>
{{ t('standalone.openvpn_tunnel.regenerate_cert_message') }}
<NeInlineNotification
v-if="error.notificationDescription"
kind="error"
:title="t('error.cannot_regenerate_cert')"
:description="error.notificationDescription"
class="my-2"
>
<template v-if="error.notificationDetails" #details>
{{ error.notificationDetails }}
</template>
</NeInlineNotification>
</NeModal>
</template>
256 changes: 256 additions & 0 deletions src/components/standalone/openvpn_tunnel/TunnelInfoModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<!--
Copyright (C) 2026 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<script setup lang="ts">
import { NeModal } from '@nethesis/vue-components'
import { useI18n } from 'vue-i18n'
import type { ServerTunnel, ClientTunnel } from './TunnelManager.vue'
import { watch, ref, computed } from 'vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { getCertificateStatus } from './TunnelTable.vue'

const { t, locale } = useI18n()

const { itemToShow = null } = defineProps<{
itemToShow?: ServerTunnel | ClientTunnel | null
}>()

const _itemToShow = ref<ServerTunnel | ClientTunnel>()
watch(
() => itemToShow,
(newVal) => {
if (newVal !== null) {
_itemToShow.value = newVal
}
},
{ immediate: true }
)

const emit = defineEmits(['close'])

function isClientTunnel(item: ServerTunnel | ClientTunnel): item is ClientTunnel {
return 'remote_host' in item
}

const certificateStatus = computed(() => {
if (!_itemToShow.value?.cert_expiry_ts) return { show: false }
return getCertificateStatus(
_itemToShow.value.cert_expiry_ts,
isClientTunnel(_itemToShow.value),
true
)
})
</script>

<template>
<NeModal
:visible="itemToShow !== null"
kind="info"
size="lg"
:primary-label="t('common.close')"
:title="t('standalone.openvpn_tunnel.tunnel_details')"
:close-aria-label="t('common.close')"
@close="emit('close')"
@primary-click="emit('close')"
>
<div v-if="_itemToShow !== undefined" class="grid grid-cols-1 gap-4 md:grid-cols-[auto_1fr]">
<!-- fields visible both for server and client tunnels -->

<!-- ns_name -->
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.name') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.ns_name }}
</p>

<!-- id -->
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.tunnel_id') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.id }}
</p>

<!-- port -->
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.port') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.port }}
</p>

<!-- enabled -->
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.status') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{
_itemToShow.enabled
? t('standalone.openvpn_tunnel.enabled')
: t('standalone.openvpn_tunnel.disabled')
}}
</p>

<!-- topology -->
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.topology') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{
_itemToShow.topology
? _itemToShow.topology === 'subnet'
? t('standalone.openvpn_tunnel.subnet')
: t('standalone.openvpn_tunnel.p2p')
: ''
}}
</p>

<!-- remote_network -->
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.remote_networks') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
<template v-if="_itemToShow.remote_network.length > 0">
{{ _itemToShow.remote_network.join(', ') }}
</template>
<template v-else> - </template>
</p>

<!-- fields visible only for client tunnels (both connected and not) -->

<!-- remote_host -->
<template v-if="isClientTunnel(_itemToShow)">
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.remote_host') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
<template v-if="_itemToShow.remote_host.length > 0">
{{ _itemToShow.remote_host.join(', ') }}
</template>
<template v-else> - </template>
</p>
</template>

<!-- fields visible only for server tunnels (both connected and not) -->

<!-- local_network -->
<template v-if="!isClientTunnel(_itemToShow)">
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.local_networks') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
<template v-if="_itemToShow.local_network.length > 0">
{{ _itemToShow.local_network.join(', ') }}
</template>
<template v-else> - </template>
</p>
</template>

<!-- vpn_network -->
<template v-if="!isClientTunnel(_itemToShow)">
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.vpn_network') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.vpn_network }}
</p>
</template>

<!-- fields visible only for connected server tunnels -->

<!-- real_address -->
<template v-if="!isClientTunnel(_itemToShow) && _itemToShow.connected">
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.real_address') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.real_address }}
</p>
</template>

<!-- virtual_address -->
<template v-if="!isClientTunnel(_itemToShow) && _itemToShow.connected">
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.virtual_address') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.virtual_address }}
</p>
</template>

<!-- connection section -->

<!-- connected -->
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.connection') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{
_itemToShow.connected
? t('standalone.openvpn_tunnel.connected')
: t('standalone.openvpn_tunnel.not_connected')
}}
</p>

<!-- since -->
<template v-if="_itemToShow.connected">
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.since') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.since ? new Date(_itemToShow.since * 1000).toLocaleString(locale) : '-' }}
</p>
</template>

<!-- bytes_sent -->
<template v-if="_itemToShow.connected">
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.bytes_sent') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.bytes_sent ? _itemToShow.bytes_sent : '-' }}
</p>
</template>

<!-- bytes_received -->
<template v-if="_itemToShow.connected">
<p class="text-sm font-semibold">
{{ t('standalone.openvpn_tunnel.bytes_received') }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ _itemToShow.bytes_received ? _itemToShow.bytes_received : '-' }}
</p>
</template>

<!-- cert_expiry_ts -->
<p class="text-sm font-semibold">
{{
isClientTunnel(_itemToShow!)
? t('standalone.openvpn_tunnel.client_cert_expiry')
: t('standalone.openvpn_tunnel.cert_expiry')
}}
</p>
<div class="flex flex-col gap-2">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{
_itemToShow.cert_expiry_ts
? new Date(_itemToShow.cert_expiry_ts * 1000).toLocaleString(locale)
: ''
}}
</p>
<span v-if="certificateStatus.show" class="flex items-center gap-2">
<FontAwesomeIcon
:icon="certificateStatus.icon"
:class="['h-4 w-4', certificateStatus.colorClass]"
aria-hidden="true"
/>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ t(certificateStatus.messageKey!, certificateStatus.messageParams!) }}
</p>
</span>
</div>
</div>
</NeModal>
</template>
Loading
Loading