- GET /plugins (backend.view_plugins, superusers always pass) lists the plugins from party.Installed, or party.Activated IDs for older builds, with the built framework version and whether the registry is configured - Settings -> System -> Plugins entry with the new SettingsEntry.path, added only for principals allowed to open it - PluginsView at /system/plugins with an Installed tab (reka-ui Tabs); settings panel, rail, breadcrumbs and page title know the route - OpenAPI and SPA types regenerated; contract, token-class and conformance inventories list the route; en/pl strings; docs
114 lines
4.5 KiB
TypeScript
114 lines
4.5 KiB
TypeScript
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 PluginsView from '../views/PluginsView.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 } },
|
|
// The read-only Plugins screen (Settings → System, backend.view_plugins).
|
|
{ path: '/system/plugins', name: 'plugins', component: PluginsView, 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
|
|
}
|