feat(10-01): serve the embedded admin SPA at backend.uri with cookie login
- backend.uri prefix (default /backend) mounts the admin API at {prefix}/api/v1
and the embedded SPA shell at {prefix} with an api/ JSON 404 fallback
- cookie transport: an X-Requested-With login sets the HttpOnly summer_admin
cookie and returns no token; the backend guard reads the cookie after Bearer
- CSRF wrapper refuses cookie-only POST/PUT/DELETE without X-Requested-With
- boardwalk package embeds boardwalk/dist, rewrites index.html once per prefix
and sets cache and security headers
- framework admin OpenAPI pipeline (swag, swagger2openapi, openapi-typescript)
with prefix-relative paths and typed envelopes for the tracer routes
- admin/ Vite SPA: login, plugin rail, section panel and read-only list
through the openapi-fetch client typed by the generated schema
This commit is contained in:
63
admin/src/app/router.ts
Normal file
63
admin/src/app/router.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { createRouter, createWebHistory, type RouterHistory } from 'vue-router'
|
||||
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 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
|
||||
}
|
||||
|
||||
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: '/:vendor/:plugin/:controller', name: 'list', component: ListView, 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
|
||||
})
|
||||
return router
|
||||
}
|
||||
Reference in New Issue
Block a user