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:
Jakub Zych
2026-09-27 15:21:48 +02:00
parent 8c3e131111
commit 5f9353841b
79 changed files with 10745 additions and 147 deletions

44
admin/src/api/client.ts Normal file
View File

@@ -0,0 +1,44 @@
// The only HTTP client of the SPA: openapi-fetch typed by the generated
// paths (D-15). Every request carries X-Requested-With (the CSRF header the
// admin API requires on state-changing cookie requests, D-19) and same-origin
// credentials; the JWT lives in an HttpOnly cookie the SPA never reads.
import createClient, { type Middleware } from 'openapi-fetch'
import type { paths } from './schema'
import { runtime } from '../app/runtime'
export const REQUESTED_WITH = 'XMLHttpRequest'
type UnauthorizedHandler = () => void
let unauthorizedHandler: UnauthorizedHandler | null = null
/** Registers what happens when an API call other than login returns 401. */
export function onUnauthorized(handler: UnauthorizedHandler | null): void {
unauthorizedHandler = handler
}
function isLoginRequest(request: Request): boolean {
return new URL(request.url, 'http://local').pathname.endsWith('/auth/login')
}
export const transport: Middleware = {
onRequest({ request }) {
request.headers.set('X-Requested-With', REQUESTED_WITH)
return request
},
onResponse({ request, response }) {
if (response.status === 401 && !isLoginRequest(request)) {
unauthorizedHandler?.()
}
return response
},
}
export const api = createClient<paths>({
baseUrl: runtime.api,
credentials: 'same-origin',
// Resolve fetch per call so tests can replace globalThis.fetch.
fetch: (request: Request) => globalThis.fetch(request),
})
api.use(transport)