feat(10.1-01): serve controller JS/CSS and run registered toolbar actions

- boardwalk exports ContentType and SetSecurityHeaders
- pact.AdminClientAssets files are read and hashed at boot and served by exact
  key under {prefix}/assets/{vendor}/{plugin}/ with nosniff, CSP, CORP,
  no-cache and an ETag; a miss falls through to the SPA
- list and form schemas carry assets URLs with a ?v= hash
- toolbar.buttons resolves create, delete and registered actions after decode;
  toolbarActions is permission-filtered per admin
- POST .../toolbar/{action} behind requireAjax and action permissions
This commit is contained in:
Jakub Zych
2026-09-28 23:41:17 +02:00
parent f9281949a6
commit 8b1cb244de
23 changed files with 772 additions and 64 deletions

View File

@@ -223,6 +223,9 @@ func (s *service) mount(r pact.Router) {
g.Post("/{vendor}/{plugin}/{controller}/widgets/{field}", requireAjax(s.widgetAction))
constrainController(g)
g.Where("field", "[A-Za-z_][A-Za-z0-9_]*")
g.Post("/{vendor}/{plugin}/{controller}/toolbar/{action}", requireAjax(s.toolbarAction))
constrainController(g)
g.Where("action", "[A-Za-z_][A-Za-z0-9_]*")
g.Get("/{vendor}/{plugin}/{controller}/{id}", s.show)
constrainController(g)
g.Put("/{vendor}/{plugin}/{controller}/{id}", requireAjax(s.update))
@@ -244,6 +247,11 @@ func (s *service) mount(r pact.Router) {
// The SPA shell: public, no guard. ServeMux prefers every API pattern
// above over the {path...} wildcard.
r.GroupRaw(s.adminPrefix(), nil, func(g pact.Router) {
// Declared plugin JS and CSS (D-16); a miss falls through to the SPA,
// which serves its own dist assets under the same /assets/ path.
g.Get("/assets/{vendor}/{plugin}/{file...}", s.pluginAsset)
g.Where("vendor", "[A-Za-z0-9_-]+")
g.Where("plugin", "[A-Za-z0-9_-]+")
g.Get("", s.serveSPA)
g.Get("/{path...}", s.serveSPA)
})
@@ -509,6 +517,7 @@ func (s *service) formSchema(w http.ResponseWriter, r *http.Request) {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
return
}
view.Assets = s.controllerAssets(cc)
meta := map[string]any{}
if view.Meta.Locale != "" {
meta["locale"] = view.Meta.Locale
@@ -528,6 +537,16 @@ func (s *service) listSchema(w http.ResponseWriter, r *http.Request) {
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
return
}
// Only the registered toolbar actions this admin may run are offered.
principal, _ := bouncer.User(r.Context())
allowed := make([]ToolbarAction, 0, len(view.ToolbarActions))
for _, action := range view.ToolbarActions {
if registered, ok := cc.Actions[action.Name]; ok && Allows(principal, registered.Permissions) {
allowed = append(allowed, action)
}
}
view.ToolbarActions = allowed
view.Assets = s.controllerAssets(cc)
meta := map[string]any{}
if view.Meta != nil {
meta["locale"] = view.Meta.Locale