import { createRouter, createWebHistory, type RouteLocationNormalized, type RouterHistory } from 'vue-router' import { controllerIdFromParams } from './controllerRoutes' import { activateStyles } from './pluginAssets' import { runtime } from './runtime' import { currentUser } from '../state/useAuth' import { homePath } from '../state/useNavigation' import LoginView from '../views/LoginView.vue' import ListView from '../views/ListView.vue' import FormView from '../views/FormView.vue' import PreviewView from '../views/PreviewView.vue' import ReorderView from '../views/ReorderView.vue' import SettingsFormView from '../views/SettingsFormView.vue' import SettingsIndexView from '../views/SettingsIndexView.vue' import AccountView from '../views/AccountView.vue' import NotFoundView from '../views/NotFoundView.vue' declare module 'vue-router' { interface RouteMeta { /** Reachable without a session. */ public?: boolean /** Rendered inside the navigation shell. */ shell?: boolean } } /** * Accepts a post-login redirect only when it is an in-app path: it must start * with exactly one slash, which rejects absolute and protocol-relative URLs. */ export function safeRedirect(value: unknown): string | null { if (typeof value !== 'string' || value.length === 0) { return null } if (!value.startsWith('/') || value.startsWith('//') || value.startsWith('/\\')) { return null } return value } const CONTROLLER_ROUTES = new Set(['list', 'create', 'record', 'preview', 'reorder']) /** * The controller a route shows, or '' for every other screen (settings, * login, not found). Plugin stylesheets follow it (D-16, T-10.1-16). */ export function routeControllerId(route: RouteLocationNormalized): string { if (typeof route.name !== 'string' || !CONTROLLER_ROUTES.has(route.name)) { return '' } const { vendor, plugin, controller } = route.params if (typeof vendor !== 'string' || typeof plugin !== 'string' || typeof controller !== 'string') { return '' } return controllerIdFromParams({ vendor, plugin, controller }) } export function createAdminRouter(history: RouterHistory = createWebHistory(runtime.base)) { const router = createRouter({ history, routes: [ { path: '/login', name: 'login', component: LoginView, meta: { public: true } }, { path: '/', name: 'home', component: NotFoundView, props: { home: true }, meta: { shell: true }, beforeEnter: () => homePath() ?? true, }, { path: '/settings', name: 'settings', component: SettingsIndexView, meta: { shell: true } }, { path: '/settings/:code', name: 'settings-form', component: SettingsFormView, meta: { shell: true } }, // My account: the signed-in admin's own profile and password. { path: '/account', name: 'account', component: AccountView, meta: { shell: true } }, { path: '/:vendor/:plugin/:controller', name: 'list', component: ListView, meta: { shell: true } }, { path: '/:vendor/:plugin/:controller/create', name: 'create', component: FormView, meta: { shell: true } }, // The tree reorder page of a list with config_reorder.yaml (D-09). { path: '/:vendor/:plugin/:controller/reorder', name: 'reorder', component: ReorderView, meta: { shell: true } }, { path: '/:vendor/:plugin/:controller/:id(\\d+)', name: 'record', component: FormView, meta: { shell: true } }, // The read-only record screen of a form with a preview block (D-11). { path: '/:vendor/:plugin/:controller/:id(\\d+)/preview', name: 'preview', component: PreviewView, meta: { shell: true }, }, { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundView, meta: { shell: true } }, ], }) router.beforeEach((to) => { const signedIn = currentUser.value !== null if (to.meta.public) { if (signedIn && to.name === 'login') { return safeRedirect(to.query.redirect) ?? '/' } return true } if (!signedIn) { return { name: 'login', query: { redirect: to.fullPath } } } return true }) // Plugin CSS is global: enable only the links of the controller on screen, // and none on any other screen (D-16, T-10.1-16). router.afterEach((to, _from, failure) => { if (!failure) { activateStyles(routeControllerId(to)) } }) return router }