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

@@ -98,7 +98,7 @@ type handler struct {
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
setSecurityHeaders(w.Header())
SetSecurityHeaders(w.Header())
rel := strings.TrimPrefix(r.URL.Path, h.prefix)
rel = strings.TrimPrefix(rel, "/")
if rel == "api" || strings.HasPrefix(rel, "api/") {
@@ -138,7 +138,7 @@ func (h *handler) serveFile(w http.ResponseWriter, r *http.Request, name string)
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", contentType(name))
w.Header().Set("Content-Type", ContentType(name))
if strings.HasPrefix(name, "assets/") {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
} else {
@@ -147,7 +147,10 @@ func (h *handler) serveFile(w http.ResponseWriter, r *http.Request, name string)
http.ServeContent(w, r, path.Base(name), time.Time{}, bytes.NewReader(body))
}
func contentType(name string) string {
// ContentType is the Content-Type the admin serves for a file name: explicit
// UTF-8 JavaScript and CSS types (module scripts and nosniff'd stylesheets
// need them), then the platform MIME table, then application/octet-stream.
func ContentType(name string) string {
ext := strings.ToLower(path.Ext(name))
if ct, ok := contentTypes[ext]; ok {
return ct
@@ -158,7 +161,10 @@ func contentType(name string) string {
return "application/octet-stream"
}
func setSecurityHeaders(h http.Header) {
// SetSecurityHeaders sets the admin response headers: nosniff, a same-origin
// referrer policy, no framing, the admin Content-Security-Policy
// (script-src 'self') and noindex.
func SetSecurityHeaders(h http.Header) {
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "same-origin")
h.Set("X-Frame-Options", "DENY")