diff --git a/GUI/src/Layout.vue b/GUI/src/Layout.vue index 682e9d1..e37d60d 100644 --- a/GUI/src/Layout.vue +++ b/GUI/src/Layout.vue @@ -5,12 +5,11 @@ import { useDisplay, useTheme } from 'vuetify' import { useRoute, useRouter } from 'vue-router' import iconImage from '@/assets/images/icon.svg' -import { Visibility, routes } from '@/plugins/routesLayout' +import { useSidebarRoutes } from '@/composables/useSidebarRoutes' +import { routes } from '@/plugins/routesLayout' import { AuthRequestError, - ROLE_ADMIN, fetchCurrentUser, - hasRole, logout, type CurrentUser, } from '@/services/authSession' @@ -28,90 +27,7 @@ const isLoggingOut = ref(false) const appBannersStore = useAppBannersStore() const { banners } = storeToRefs(appBannersStore) -function normalizeRoutePath(path: string) { - if (!path || path === '/') { - return '/' - } - - return path.endsWith('/') ? path.slice(0, -1) : path -} - -function isWithinRoutePath(currentPath: string, targetPath: string) { - const normalizedCurrentPath = normalizeRoutePath(currentPath) - const normalizedTargetPath = normalizeRoutePath(targetPath) - - if (normalizedTargetPath === '/') { - return normalizedCurrentPath === '/' - } - - return ( - normalizedCurrentPath === normalizedTargetPath || - normalizedCurrentPath.startsWith(`${normalizedTargetPath}/`) - ) -} - -function resolveVisibilityRoutes(path: string, visibilityRoute?: string | string[]) { - if (Array.isArray(visibilityRoute)) { - return visibilityRoute.map((entry) => entry.trim()).filter((entry) => entry.length > 0) - } - - const normalizedVisibilityRoute = visibilityRoute?.trim() - if (normalizedVisibilityRoute && normalizedVisibilityRoute.length > 0) { - return [normalizedVisibilityRoute] - } - - return [path] -} - -const sidebarRoutes = computed(() => - routes.filter((item) => { - if (item.visible === Visibility.Public) { - return true - } - - if (item.visible === Visibility.Authenticated) { - return currentUser.value !== null - } - - if (item.visible === Visibility.Unauthenticated) { - return currentUser.value === null - } - - if (item.visible === Visibility.Authorized) { - if (!currentUser.value) { - return false - } - - if (!item.requiredRoles || item.requiredRoles.length === 0) { - return true - } - - return item.requiredRoles.every((role) => hasRole(currentUser.value, role)) - } - - if (item.visible !== Visibility.Route) { - return false - } - - return resolveVisibilityRoutes(item.path, item.visibilityRoute).some((targetPath) => - isWithinRoutePath(route.path, targetPath), - ) - }), -) -const adminSidebarRoutes = computed(() => - sidebarRoutes.value.filter( - (item) => - item.visible === Visibility.Authorized && - Array.isArray(item.requiredRoles) && - item.requiredRoles.some((role) => role.trim().toLowerCase() === ROLE_ADMIN), - ), -) -const primarySidebarRoutes = computed(() => - sidebarRoutes.value.filter( - (item) => !adminSidebarRoutes.value.some((adminItem) => adminItem.path === item.path), - ), -) -const footerRoutes = computed(() => routes.filter((x) => x.visible === Visibility.Footer)) +const { adminSidebarRoutes, primarySidebarRoutes, footerRoutes } = useSidebarRoutes(currentUser) const activeRoute = computed(() => { const byName = routes.find((x) => x.meta?.name === route.name) diff --git a/GUI/src/composables/useSidebarRoutes.ts b/GUI/src/composables/useSidebarRoutes.ts new file mode 100644 index 0000000..d670f61 --- /dev/null +++ b/GUI/src/composables/useSidebarRoutes.ts @@ -0,0 +1,98 @@ +import { computed, type Ref } from 'vue' +import { useRoute } from 'vue-router' + +import { Visibility, routes, type LayoutRoute } from '@/plugins/routesLayout' +import { ROLE_ADMIN, hasRole, type CurrentUser } from '@/services/authSession' + +function normalizeRoutePath(path: string) { + if (!path || path === '/') { + return '/' + } + + return path.endsWith('/') ? path.slice(0, -1) : path +} + +function isWithinRoutePath(currentPath: string, targetPath: string) { + const normalizedCurrent = normalizeRoutePath(currentPath) + const normalizedTarget = normalizeRoutePath(targetPath) + + if (normalizedTarget === '/') { + return normalizedCurrent === '/' + } + + return ( + normalizedCurrent === normalizedTarget || + normalizedCurrent.startsWith(`${normalizedTarget}/`) + ) +} + +function resolveVisibilityRoutes(path: string, visibilityRoute?: string | string[]) { + if (Array.isArray(visibilityRoute)) { + return visibilityRoute.map((entry) => entry.trim()).filter((entry) => entry.length > 0) + } + + const normalized = visibilityRoute?.trim() + if (normalized && normalized.length > 0) { + return [normalized] + } + + return [path] +} + +function isVisible(item: LayoutRoute, currentPath: string, user: CurrentUser | null) { + switch (item.visible) { + case Visibility.Public: + return true + case Visibility.Authenticated: + return user !== null + case Visibility.Unauthenticated: + return user === null + case Visibility.Authorized: + if (!user) { + return false + } + if (!item.requiredRoles || item.requiredRoles.length === 0) { + return true + } + return item.requiredRoles.every((role) => hasRole(user, role)) + case Visibility.Route: + return resolveVisibilityRoutes(item.path, item.visibilityRoute).some((target) => + isWithinRoutePath(currentPath, target), + ) + default: + return false + } +} + +function isAdminRoute(item: LayoutRoute) { + return ( + item.visible === Visibility.Authorized && + Array.isArray(item.requiredRoles) && + item.requiredRoles.some((role) => role.trim().toLowerCase() === ROLE_ADMIN) + ) +} + +export function useSidebarRoutes(currentUser: Ref) { + const route = useRoute() + + const sidebarRoutes = computed(() => + routes.filter((item) => isVisible(item, route.path, currentUser.value)), + ) + + const adminSidebarRoutes = computed(() => sidebarRoutes.value.filter(isAdminRoute)) + + const primarySidebarRoutes = computed(() => + sidebarRoutes.value.filter((item) => !isAdminRoute(item)), + ) + + const footerRoutes = computed(() => + routes.filter((item) => item.visible === Visibility.Footer), + ) + + return { + sidebarRoutes, + adminSidebarRoutes, + primarySidebarRoutes, + footerRoutes, + } +} diff --git a/GUI/src/stores/counter.ts b/GUI/src/stores/counter.ts deleted file mode 100644 index b6757ba..0000000 --- a/GUI/src/stores/counter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ref, computed } from 'vue' -import { defineStore } from 'pinia' - -export const useCounterStore = defineStore('counter', () => { - const count = ref(0) - const doubleCount = computed(() => count.value * 2) - function increment() { - count.value++ - } - - return { count, doubleCount, increment } -})