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:
@@ -12,35 +12,49 @@ import (
|
||||
"git.golem15.com/golem15/summercms/pact"
|
||||
)
|
||||
|
||||
// phase09Routes is the D-09 admin surface mounted by service.mount.
|
||||
// A handler added outside this set, or a protected handler missing the
|
||||
// backend guard, fails TestPhase09PermissionMatrix.
|
||||
// phase09Routes is the admin surface mounted by service.mount. API keys are
|
||||
// method plus the path relative to {backend.uri}/api/v1 (D-03); spa entries
|
||||
// are the public SPA shell routes relative to {backend.uri} and are not part
|
||||
// of the OpenAPI inventory. A handler added outside this set, or a protected
|
||||
// handler missing the backend guard, fails TestPhase09PermissionMatrix.
|
||||
var phase09Routes = []struct {
|
||||
key string
|
||||
public bool
|
||||
spa bool
|
||||
}{
|
||||
{"POST /_admin/api/v1/auth/login", true},
|
||||
{"POST /_admin/api/v1/auth/refresh", true},
|
||||
{"POST /_admin/api/v1/auth/logout", false},
|
||||
{"GET /_admin/api/v1/auth/me", false},
|
||||
{"GET /_admin/api/v1/navigation", false},
|
||||
{"GET /_admin/api/v1/settings", false},
|
||||
{"GET /_admin/api/v1/settings/{code}/schema", false},
|
||||
{"GET /_admin/api/v1/settings/{code}", false},
|
||||
{"PUT /_admin/api/v1/settings/{code}", false},
|
||||
{"GET /_admin/api/v1/{vendor}/{plugin}/{controller}/schema/list", false},
|
||||
{"GET /_admin/api/v1/{vendor}/{plugin}/{controller}/schema/form", false},
|
||||
{"GET /_admin/api/v1/{vendor}/{plugin}/{controller}/schema/relation/{name}", false},
|
||||
{"GET /_admin/api/v1/{vendor}/{plugin}/{controller}", false},
|
||||
{"POST /_admin/api/v1/{vendor}/{plugin}/{controller}", false},
|
||||
{"POST /_admin/api/v1/{vendor}/{plugin}/{controller}/bulk-delete", false},
|
||||
{"GET /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}", false},
|
||||
{"PUT /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}", false},
|
||||
{"DELETE /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}", false},
|
||||
{"GET /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}/relations/{name}", false},
|
||||
{"GET /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}/relations/{name}/candidates", false},
|
||||
{"POST /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}/relations/{name}/link", false},
|
||||
{"POST /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}/relations/{name}/unlink", false},
|
||||
{"POST /auth/login", true, false},
|
||||
{"POST /auth/refresh", true, false},
|
||||
{"POST /auth/logout", false, false},
|
||||
{"GET /auth/me", false, false},
|
||||
{"GET /navigation", false, false},
|
||||
{"GET /settings", false, false},
|
||||
{"GET /settings/{code}/schema", false, false},
|
||||
{"GET /settings/{code}", false, false},
|
||||
{"PUT /settings/{code}", false, false},
|
||||
{"GET /{vendor}/{plugin}/{controller}/schema/list", false, false},
|
||||
{"GET /{vendor}/{plugin}/{controller}/schema/form", false, false},
|
||||
{"GET /{vendor}/{plugin}/{controller}/schema/relation/{name}", false, false},
|
||||
{"GET /{vendor}/{plugin}/{controller}", false, false},
|
||||
{"POST /{vendor}/{plugin}/{controller}", false, false},
|
||||
{"POST /{vendor}/{plugin}/{controller}/bulk-delete", false, false},
|
||||
{"GET /{vendor}/{plugin}/{controller}/{id}", false, false},
|
||||
{"PUT /{vendor}/{plugin}/{controller}/{id}", false, false},
|
||||
{"DELETE /{vendor}/{plugin}/{controller}/{id}", false, false},
|
||||
{"GET /{vendor}/{plugin}/{controller}/{id}/relations/{name}", false, false},
|
||||
{"GET /{vendor}/{plugin}/{controller}/{id}/relations/{name}/candidates", false, false},
|
||||
{"POST /{vendor}/{plugin}/{controller}/{id}/relations/{name}/link", false, false},
|
||||
{"POST /{vendor}/{plugin}/{controller}/{id}/relations/{name}/unlink", false, false},
|
||||
{"GET ", true, true},
|
||||
{"GET /{path...}", true, true},
|
||||
}
|
||||
|
||||
// mountedKey is the full mounted route key for an inventory entry.
|
||||
func mountedKey(key string, spa bool) string {
|
||||
method, rel, _ := strings.Cut(key, " ")
|
||||
if spa {
|
||||
return method + " " + DefaultAdminPrefix + rel
|
||||
}
|
||||
return method + " " + adminAPI(rel)
|
||||
}
|
||||
|
||||
func TestPhase09PermissionMatrix(t *testing.T) {
|
||||
@@ -57,9 +71,10 @@ func TestPhase09PermissionMatrix(t *testing.T) {
|
||||
t.Fatalf("mounted %d admin routes, want %d: %#v", len(got), len(phase09Routes), router.routes)
|
||||
}
|
||||
for _, route := range phase09Routes {
|
||||
mw, ok := got[route.key]
|
||||
key := mountedKey(route.key, route.spa)
|
||||
mw, ok := got[key]
|
||||
if !ok {
|
||||
t.Fatalf("missing mounted route %s", route.key)
|
||||
t.Fatalf("missing mounted route %s in %v", key, router.routes)
|
||||
}
|
||||
hasBackend := false
|
||||
for _, name := range mw {
|
||||
@@ -257,7 +272,7 @@ func phase09DeniedService() *service {
|
||||
}
|
||||
|
||||
func phase09Request(principal *bouncer.Principal) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/_admin/api/v1/acme/demo/widgets/1/relations/editors/link", strings.NewReader(`{}`))
|
||||
req := httptest.NewRequest(http.MethodPost, adminAPI("/acme/demo/widgets/1/relations/editors/link"), strings.NewReader(`{}`))
|
||||
req.SetPathValue("vendor", "acme")
|
||||
req.SetPathValue("plugin", "demo")
|
||||
req.SetPathValue("controller", "widgets")
|
||||
|
||||
Reference in New Issue
Block a user