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

View File

@@ -0,0 +1,44 @@
// Admin session state. The JWT travels in an HttpOnly cookie (D-19): the SPA
// keeps only the profile and the access lifetime, never a token.
import { readonly, ref } from 'vue'
import { api } from '../api/client'
import type { AdminProfile } from '../api/types'
const user = ref<AdminProfile | null>(null)
const expiresIn = ref<number | null>(null)
export const currentUser = readonly(user)
/** Logs in over cookie transport. Returns false on invalid credentials. */
export async function login(identifier: string, password: string): Promise<boolean> {
const { data, response } = await api.POST('/auth/login', {
body: { login: identifier, password },
})
if (!response.ok || !data) {
return false
}
expiresIn.value = typeof data.data.expires_in === 'number' ? data.data.expires_in : null
return true
}
/** Loads the signed-in admin; null when the session is missing or expired. */
export async function me(): Promise<AdminProfile | null> {
const { data, response } = await api.GET('/auth/me')
user.value = response.ok && data ? data.data : null
return user.value
}
export function clearUser(): void {
user.value = null
expiresIn.value = null
}
export function useAuth() {
return {
user: currentUser,
expiresIn: readonly(expiresIn),
login,
me,
clearUser,
}
}