Skip to content
Draft
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
1 change: 1 addition & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="vite/client" />
declare const UI_VERSION: string
declare const REQUIRED_API_VERSION: string
declare const MIN_UI_VERSION_FOR_DIRECT_SERVE: string
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="%BASE_URL%favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NethSecurity</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script src="/branding.js"></script>
<script src="%BASE_URL%branding.js"></script>
</body>
</html>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "nethsecurity-ui",
"version": "2.23.3",
"config": {
"requiredApiVersion": "~3.7.0"
"requiredApiVersion": "~3.7.0",
"minUiVersionForDirectServe": "2.24.0"
},
"private": true,
"type": "module",
Expand Down
4 changes: 2 additions & 2 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ControllerApp from './ControllerApp.vue'
import StandaloneApp from './StandaloneApp.vue'
import { useThemeStore } from '@/stores/theme'
import { onMounted } from 'vue'
import { isStandaloneMode } from './lib/config'
import { isStandaloneBuild } from './lib/config'

const welcomeMsg = [
' _ _ _ _ _____ _ _ ',
Expand All @@ -30,7 +30,7 @@ onMounted(() => {
</script>

<template>
<template v-if="isStandaloneMode()">
<template v-if="isStandaloneBuild()">
<StandaloneApp />
</template>
<template v-else>
Expand Down
8 changes: 4 additions & 4 deletions src/ControllerApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useI18n } from 'vue-i18n'
import { useNotificationsStore } from './stores/notifications'
import { useFavicon, useTitle } from '@vueuse/core'
import { getControllerApiEndpoint, getProductName } from './lib/config'
import { getUnitIdFromApiUrl } from '@/lib/deployment'

const loginStore = useLoginStore()
const { locale } = useI18n({ useScope: 'global' })
Expand All @@ -38,7 +39,7 @@ onMounted(async () => {

// set favicon
const favIcon = useFavicon()
favIcon.value = '/favicon-controller.ico'
favIcon.value = `${import.meta.env.BASE_URL}favicon-controller.ico`
})

function configureAxios() {
Expand Down Expand Up @@ -92,10 +93,9 @@ function configureAxios() {
'Detected error 401, removing unit token from local storage'
)

const matched = error.config.url.match(/^(.+:\/\/)(.+)\/(.+)\/api\/ubus\/call$/)
const unitId = getUnitIdFromApiUrl(error.config.url)

if (matched.length == 4) {
const unitId = matched[3]
if (unitId) {
deleteFromStorage(`unit-${unitId}`)

// show error notification
Expand Down
16 changes: 10 additions & 6 deletions src/StandaloneApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
<script setup lang="ts">
import StandaloneAppShell from '@/components/standalone/StandaloneAppShell.vue'
import StandaloneAppLogin from '@/components/standalone/StandaloneAppLogin.vue'
import UnitSessionExpired from '@/components/standalone/UnitSessionExpired.vue'
import { isProxiedByController } from '@/lib/deployment'
import { TOKEN_REFRESH_INTERVAL, useLoginStore } from '@/stores/standalone/standaloneLogin'
import { onMounted, ref } from 'vue'
import axios, { type AxiosRequestConfig } from 'axios'
import { getStandaloneApiEndpoint, isStandaloneMode } from './lib/config'
import { getStandaloneApiEndpoint, isStandaloneBuild } from './lib/config'
import { useUnitsStore } from './stores/controller/units'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { getPreference } from '@nethesis/vue-components'
import { getUiPreference } from '@/lib/storage'
import { getUbusReproductionCommand } from '@/lib/axiosErrorCommand'
import { UnauthorizedAction, useSudoStore } from '@/stores/standalone/sudo.ts'
import AskSudoPasswordModal from '@/components/standalone/AskSudoPasswordModal.vue'
Expand All @@ -30,14 +32,14 @@ const sudoStore = useSudoStore()
const isLoaded = ref(false)

onMounted(async () => {
if (isStandaloneMode()) {
if (isStandaloneBuild()) {
await loginStore.loadUserFromStorage()
// Setup localization
let username = 'root'
if (loginStore.isLoggedIn) {
username = loginStore.username
}
locale.value = getPreference('locale', username) || navigator.language
locale.value = getUiPreference('locale', username) || navigator.language
} else {
// a controller is managing this unit
await unitsStore.load()
Expand Down Expand Up @@ -104,7 +106,7 @@ function configureAxios() {
}

if (error.response?.status == 401) {
if (isStandaloneMode()) {
if (isStandaloneBuild()) {
if (error.response?.data?.message !== 'incorrect Username or Password') {
console.warn('[interceptor]', 'Detected error 401, logout')
loginStore.isSessionExpired = true
Expand Down Expand Up @@ -167,7 +169,9 @@ function configureAxios() {
<AskSudoPasswordModal />
</template>
<template v-else>
<StandaloneAppLogin />
<!-- The unit's credentials belong to the controller, so a login form is a dead end here. -->
<UnitSessionExpired v-if="isProxiedByController()" />
<StandaloneAppLogin v-else />
</template>
</template>
<VueQueryDevtools />
Expand Down
File renamed without changes
File renamed without changes
File renamed without changes
9 changes: 5 additions & 4 deletions src/components/ChangeLangCombobox.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<script lang="ts" setup>
import { NeCombobox, type NeComboboxOption, savePreference } from '@nethesis/vue-components'
import { NeCombobox, type NeComboboxOption } from '@nethesis/vue-components'
import { saveUiPreference } from '@/lib/storage'
import { useI18n } from 'vue-i18n'
import { computed, watch } from 'vue'
import { isStandaloneMode } from '@/lib/config'
import { isStandaloneBuild } from '@/lib/config'
import { useLoginStore as useStandaloneLoginStore } from '@/stores/standalone/standaloneLogin'
import { useLoginStore as useControllerLoginStore } from '@/stores/controller/controllerLogin'
import { capitalize } from 'lodash-es'

const { t, locale, availableLocales } = useI18n({ useScope: 'global' })
const loginStore = isStandaloneMode() ? useStandaloneLoginStore() : useControllerLoginStore()
const loginStore = isStandaloneBuild() ? useStandaloneLoginStore() : useControllerLoginStore()

const supportedLanguages = computed((): NeComboboxOption[] => {
return availableLocales.map((locale) => {
Expand All @@ -23,7 +24,7 @@ const supportedLanguages = computed((): NeComboboxOption[] => {
})

watch(locale, () => {
savePreference('locale', locale.value, loginStore.username)
saveUiPreference('locale', locale.value, loginStore.username)
})
</script>

Expand Down
3 changes: 2 additions & 1 deletion src/components/controller/ControllerAppLogin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MessageBag, validateRequired, validateSixDigitCode } from '@/lib/valida
import { getControllerRoutePrefix } from '@/lib/router'
import router from '@/router'
import { ValidationError } from '@/lib/standalone/ubus'
import loginLogoUrl from '@/assets/login_logo.svg'

const username = ref('')
const password = ref('')
Expand Down Expand Up @@ -246,7 +247,7 @@ function isFormInvalid() {
class="relative hidden w-0 flex-1 items-center justify-center bg-linear-to-t from-gray-950 to-primary-800 lg:flex"
>
<div class="flex w-2/3 flex-col items-center xl:w-2/5 3xl:w-1/3 5xl:w-1/4">
<img src="/login_logo.svg" :alt="`${getCompanyName()} logo`" class="" />
<img :src="loginLogoUrl" :alt="`${getCompanyName()} logo`" class="" />
<span class="text-xl text-white">Controller</span>
</div>
</div>
Expand Down
15 changes: 3 additions & 12 deletions src/components/controller/ControllerShellLogo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,15 @@
-->

<script setup lang="ts">
import { computed } from 'vue'
import { getCompanyName } from '@/lib/config'
import { useThemeStore } from '@/stores/theme'
import { useAppLogo } from '@/composables/useAppLogo'

const themeStore = useThemeStore()

const logoFilename = computed(() => {
if (themeStore.isLight) {
return 'logo_light.svg'
} else {
return 'logo_dark.svg'
}
})
const { logoUrl } = useAppLogo()
</script>

<template>
<div class="flex h-16 shrink-0 flex-col items-start justify-center pt-3 pl-3">
<img class="h-7 w-auto" :src="`/${logoFilename}`" :alt="`${getCompanyName()} logo`" />
<img class="h-7 w-auto" :src="logoUrl" :alt="`${getCompanyName()} logo`" />
<span class="text-base text-primary-700 dark:text-primary-500">Controller</span>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ValidationError } from '@/lib/standalone/ubus'
import { useLoginStore as useStandaloneLoginStore } from '@/stores/standalone/standaloneLogin'
import { useLoginStore as useControllerLoginStore } from '@/stores/controller/controllerLogin'
import { useNotificationsStore } from '@/stores/notifications'
import { isStandaloneMode } from '@/lib/config'
import { isStandaloneBuild } from '@/lib/config'
import { useQRCode } from '@vueuse/integrations/useQRCode'

const props = defineProps({
Expand Down Expand Up @@ -117,7 +117,7 @@ async function verifyOtp() {
return
}
loading.value.verifyOtp = true
const loginStore = isStandaloneMode() ? useStandaloneLoginStore() : useControllerLoginStore()
const loginStore = isStandaloneBuild() ? useStandaloneLoginStore() : useControllerLoginStore()

try {
await verifyTwoFaOtp(loginStore.username, loginStore.token, otp.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
NeModal,
NeTextInput
} from '@nethesis/vue-components'
import { isStandaloneMode } from '@/lib/config'
import { isStandaloneBuild } from '@/lib/config'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'

Expand All @@ -30,7 +30,7 @@ const props = defineProps({
const emit = defineEmits(['close', 'reloadData'])

const { t } = useI18n()
const loginStore = isStandaloneMode() ? useStandaloneLoginStore() : useControllerLoginStore()
const loginStore = isStandaloneBuild() ? useStandaloneLoginStore() : useControllerLoginStore()
const notificationsStore = useNotificationsStore()
const otp = ref('')
const otpRef = ref()
Expand Down
32 changes: 31 additions & 1 deletion src/components/controller/units/UnitsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import router from '@/router'
import { onMounted, type PropType, ref } from 'vue'
import { useLoginStore } from '@/stores/controller/controllerLogin'
import RemoveUnitModal from '@/components/controller/units/RemoveUnitModal.vue'
import { coerce, outside, satisfies } from 'semver'
import { coerce, gte, outside, satisfies } from 'semver'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import {
faArrowsRotate,
Expand Down Expand Up @@ -97,6 +97,20 @@ onMounted(() => {
hideOpenUnitPopupsTooltip.value = getPreference('hideOpenUnitPopupsTooltip', loginStore.username)
})

/**
* True when the unit ships a UI new enough to be served under the controller's per-unit path
* prefix, so we can hand the browser over to the unit's own UI instead of rendering our embedded
* copy of it.
*
* Gated on ui_version rather than api_version on purpose: this is a property of the ns-ui package,
* and the two are versioned independently. Units that do not report ui_version at all fail closed
* to the embedded copy, which is the correct answer for them.
*/
function servesItsOwnUi(unit: Unit) {
const uiVersion = coerce(unit.info?.ui_version)
return uiVersion != null && gte(uiVersion, MIN_UI_VERSION_FOR_DIRECT_SERVE)
}

async function openUnit(unit: Unit, versionCheck = true) {
error.value.openUnit = ''
currentUnit.value = unit
Expand All @@ -107,6 +121,22 @@ async function openUnit(unit: Unit, versionCheck = true) {
await unitsStore.getUnits()
// Find the now updated unit, as the currentUnit might have changed
currentUnit.value = unitsStore.units.find((u) => u.id == unit.id)!

if (servesItsOwnUi(currentUnit.value)) {
// The unit serves its own UI at its own version, so there is no API version to keep in
// lockstep with ours. checkUnitToken writes unit-<id>, which the unit's UI reads back on
// load - same origin, so this is the session handover.
await unitsStore.checkUnitToken(unit.id)
// Trailing slash is mandatory: the unit's bundle uses a relative base, so without it every
// asset would resolve against the controller root. 'noopener' keeps the unit's JS from
// reaching back into this tab through window.opener.
window.open(`${window.location.origin}/${unit.id}/`, '_blank', 'noopener')
showGreaterApiModal.value = false
return
}

// Legacy path: render our embedded copy of the standalone UI, which does have to match the
// unit's API version.
// Logic on which message is shown is inside the ObsoleteApiModal component
let version = coerce(currentUnit.value?.info.api_version)
if (version == null) {
Expand Down
6 changes: 3 additions & 3 deletions src/components/standalone/SideMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script setup lang="ts">
import { getStandaloneRoutePrefix } from '@/lib/router'
import { useLoginStore } from '@/stores/standalone/standaloneLogin'
import { getPreference, savePreference } from '@nethesis/vue-components'
import { getUiPreference, saveUiPreference } from '@/lib/storage'
import { isEmpty } from 'lodash-es'
import { onMounted, ref, type Ref } from 'vue'
import { useI18n } from 'vue-i18n'
Expand Down Expand Up @@ -245,14 +245,14 @@ function toggleExpand(menuItem: MenuItem) {
const newValue = !menuExpanded.value[menuItem.to]
menuExpanded.value[menuItem.to] = newValue
const username = loginStore.username || 'root'
savePreference(`${menuItem.to}MenuExpanded`, newValue, username)
saveUiPreference(`${menuItem.to}MenuExpanded`, newValue, username)
}

function loadMenuItemsExpanded() {
const username = loginStore.username || 'root'

for (const menuName of Object.keys(menuExpanded.value)) {
const isMenuExpanded = getPreference(`${menuName}MenuExpanded`, username)
const isMenuExpanded = getUiPreference(`${menuName}MenuExpanded`, username)

if (isMenuExpanded || isCurrentRoute(menuName)) {
menuExpanded.value[menuName] = true
Expand Down
14 changes: 10 additions & 4 deletions src/components/standalone/StandaloneAppLogin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import { getProductName, getCompanyName, getPrivacyPolicyUrl } from '@/lib/confi
import { jwtDecode } from 'jwt-decode'
import { verifyTwoFaOtp } from '@/lib/twoFa'
import { ValidationError } from '@/lib/standalone/ubus'
import { getScopedStorageKey } from '@/lib/storage'
import loginLogoUrl from '@/assets/login_logo.svg'

// scoped because every proxied unit UI shares the controller's origin, and therefore its
// localStorage namespace
const rememberedUsernameKey = getScopedStorageKey('standaloneUsername')

const username = ref('')
const usernameRef = ref()
Expand Down Expand Up @@ -62,7 +68,7 @@ watch(step, () => {

onMounted(() => {
// read username from storage, if present
const usernameFromStorage = getStringFromStorage('standaloneUsername')
const usernameFromStorage = getStringFromStorage(rememberedUsernameKey)

if (usernameFromStorage) {
rememberMe.value = true
Expand All @@ -85,9 +91,9 @@ async function login() {

// set or remove username to/from local storage
if (rememberMe.value) {
saveToStorage('standaloneUsername', username.value)
saveToStorage(rememberedUsernameKey, username.value)
} else {
deleteFromStorage('standaloneUsername')
deleteFromStorage(rememberedUsernameKey)
}

// check if 2fa is enabled
Expand Down Expand Up @@ -329,7 +335,7 @@ async function verifyOtp() {
class="relative hidden w-0 flex-1 items-center justify-center bg-linear-to-t from-gray-950 to-primary-800 lg:flex"
>
<img
src="/login_logo.svg"
:src="loginLogoUrl"
:alt="`${getCompanyName()} logo`"
class="w-2/3 xl:w-2/5 3xl:w-1/3 5xl:w-1/4"
/>
Expand Down
Loading
Loading