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:
@@ -9,14 +9,15 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPhase09ContractInventory fails when the committed OpenAPI document
|
||||
// drops a D-09 route or a protected route's 401 response.
|
||||
// TestPhase09ContractInventory fails when the committed framework admin
|
||||
// OpenAPI document (admin/openapi/admin.json, D-15) drops an admin API route
|
||||
// or a protected route's 401 response. Paths are prefix-relative (D-03).
|
||||
func TestPhase09ContractInventory(t *testing.T) {
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("caller")
|
||||
}
|
||||
specPath := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "fonoteka.go", "docs", "openapi.json"))
|
||||
specPath := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "admin", "openapi", "admin.json"))
|
||||
raw, err := os.ReadFile(specPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", specPath, err)
|
||||
@@ -37,11 +38,16 @@ func TestPhase09ContractInventory(t *testing.T) {
|
||||
t.Fatal("openapi is missing the BackendBearer scheme")
|
||||
}
|
||||
public := map[string]bool{
|
||||
"POST /_admin/api/v1/auth/login": true,
|
||||
"POST /_admin/api/v1/auth/refresh": true,
|
||||
"POST /auth/login": true,
|
||||
"POST /auth/refresh": true,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
apiRoutes := 0
|
||||
for _, route := range phase09Routes {
|
||||
if route.spa {
|
||||
continue
|
||||
}
|
||||
apiRoutes++
|
||||
method, path, ok := splitRoute(route.key)
|
||||
if !ok {
|
||||
t.Fatalf("bad route key %s", route.key)
|
||||
@@ -76,8 +82,11 @@ func TestPhase09ContractInventory(t *testing.T) {
|
||||
t.Fatalf("%s has no 401 response", key)
|
||||
}
|
||||
}
|
||||
if len(seen) != len(phase09Routes) {
|
||||
t.Fatalf("contract routes=%d want %d", len(seen), len(phase09Routes))
|
||||
if len(seen) != apiRoutes {
|
||||
t.Fatalf("contract routes=%d want %d", len(seen), apiRoutes)
|
||||
}
|
||||
if len(spec.Paths) != len(pathsOf(phase09Routes)) {
|
||||
t.Fatalf("openapi lists %d paths, the mounted API has %d", len(spec.Paths), len(pathsOf(phase09Routes)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,3 +98,20 @@ func splitRoute(key string) (method, path string, ok bool) {
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func pathsOf(routes []struct {
|
||||
key string
|
||||
public bool
|
||||
spa bool
|
||||
}) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, route := range routes {
|
||||
if route.spa {
|
||||
continue
|
||||
}
|
||||
if _, path, ok := splitRoute(route.key); ok {
|
||||
out[path] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user