diff --git a/shell/config/router/navigation-guards/__tests__/route-guard-unregistered-routes.test.js b/shell/config/router/navigation-guards/__tests__/route-guard-unregistered-routes.test.js new file mode 100644 index 00000000000..1614a688b45 --- /dev/null +++ b/shell/config/router/navigation-guards/__tests__/route-guard-unregistered-routes.test.js @@ -0,0 +1,78 @@ +import routes from '@shell/config/router/routes'; +import { handleRouteGuardUnregisteredRoutes } from '@shell/config/router/navigation-guards/route-guard-unregistered-routes'; + +// Flatten the nested routes array +const flattenRoutes = (records, acc = []) => { + records.forEach((route) => { + acc.push(route); + if (route.children?.length) { + flattenRoutes(route.children, acc); + } + }); + + return acc; +}; + +const registeredRoutes = flattenRoutes(routes).filter((r) => !!r.name); + +// Build an allTypes map keyed by resource name with matching route names +const allTypes = registeredRoutes.reduce((acc, route) => { + acc[route.name] = { route: { name: route.name } }; + + return acc; +}, {}); + +// Store mock that returns the allTypes for any product +const store = { getters: { 'type-map/allTypes': jest.fn(() => ({ all: allTypes })) } }; + +describe('route-guard-unregistered-routes', () => { + it('does not redirect registered routes', async() => { + for (const route of registeredRoutes) { + const next = jest.fn(); + + await handleRouteGuardUnregisteredRoutes( + { + name: route.name, + params: { product: 'test-product', resource: route.name }, + }, + {}, + next, + { store } + ); + + expect(next).not.toHaveBeenCalledWith(expect.objectContaining({ name: '404' })); + } + }); + + it('redirects to 404 for an unregistered route/resource with product and resource', async() => { + const next = jest.fn(); + + await handleRouteGuardUnregisteredRoutes( + { + name: 'not-registered', + params: { product: 'test-product', resource: 'not-registered' }, + }, + {}, + next, + { store } + ); + + expect(next).toHaveBeenCalledWith({ name: '404' }); + }); + + it('redirects to 404 for an unregistered route/resource with product', async() => { + const next = jest.fn(); + + await handleRouteGuardUnregisteredRoutes( + { + name: 'not-registered', + params: { product: 'test-product' }, + }, + {}, + next, + { store } + ); + + expect(next).toHaveBeenCalledWith({ name: '404' }); + }); +}); diff --git a/shell/config/router/navigation-guards/index.js b/shell/config/router/navigation-guards/index.js index 99d7daa9bf5..8cbd9244709 100644 --- a/shell/config/router/navigation-guards/index.js +++ b/shell/config/router/navigation-guards/index.js @@ -9,6 +9,7 @@ import { install as installClusters } from '@shell/config/router/navigation-guar import { install as installHandleInstallRedirect } from '@shell/config/router/navigation-guards/install-redirect'; import { install as installPageTitle } from '@shell/config/router/navigation-guards/page-title'; import { install as installServerUpgradeGrowl } from '@shell/config/router/navigation-guards/server-upgrade-growl'; +import { install as installRouteGuardUnregisteredRoutes } from '@shell/config/router/navigation-guards/route-guard-unregistered-routes'; /** * Install our router navigation guards. i.e. router.beforeEach(), router.afterEach() @@ -17,7 +18,7 @@ export function installNavigationGuards(router, context) { // NOTE: the order of the installation matters. // Be intentional when adding, removing or modifying the guards that are installed. - const navigationGuardInstallers = [installLoadInitialSettings, installAttemptFirstLogin, installAuthentication, installProducts, installClusters, installRuntimeExtensionRoute, installI18N, installHandleInstallRedirect, installPageTitle, installRecordLastRoute, installServerUpgradeGrowl]; + const navigationGuardInstallers = [installLoadInitialSettings, installAttemptFirstLogin, installAuthentication, installProducts, installClusters, installRuntimeExtensionRoute, installI18N, installHandleInstallRedirect, installPageTitle, installRecordLastRoute, installServerUpgradeGrowl, installRouteGuardUnregisteredRoutes]; navigationGuardInstallers.forEach((installer) => installer(router, context)); } diff --git a/shell/config/router/navigation-guards/route-guard-unregistered-routes.js b/shell/config/router/navigation-guards/route-guard-unregistered-routes.js new file mode 100644 index 00000000000..91d3571f6b4 --- /dev/null +++ b/shell/config/router/navigation-guards/route-guard-unregistered-routes.js @@ -0,0 +1,35 @@ +export function install(router, context) { + router.afterEach((to, from, next) => handleRouteGuardUnregisteredRoutes(to, from, next, context)); +} + +/* + Guard to handle unregistered routes for dynamic products/resources. + If a route is not found for a given product/resource, redirect to 404. + + Especially useful for extensions. +*/ +export async function handleRouteGuardUnregisteredRoutes(to, from, next, { store }) { + const product = to.params?.product || to.meta?.product; + const resource = to.params?.resource; + + if (product) { + // get all types for the product + const allTypes = store.getters['type-map/allTypes'](to.params?.product)?.all; + + if (allTypes) { + let matchFound = false; + + // check if resource matches any registered type via key + if (resource && allTypes[resource]) { + matchFound = true; + } else { + // fallback: check if route name matches any registered type route name + matchFound = Object.entries(allTypes).find(([key, value]) => value.route?.name === to.name); + } + + if (!matchFound) { + return next({ name: '404' }); + } + } + } +}