feat(10-01): harden the admin cookie session and prefix boot guards

- refresh and logout read the Bearer header first, then the summer_admin
  cookie; a cookie refresh rotates the cookie without a token in the body and
  logout always expires the cookie
- backend.cookie_secure (default true) may drop Secure outside production only
- activation rejects controller vendor segments api, assets, login, settings
- BuildRouter rejects non-cabana routes at or under the admin prefix
- SPA single-flights refresh on 401, replays once, and refreshes proactively
  at 80 percent of expires_in; dist rebuilt
- scripts/check-admin-dist.sh rebuilds the SPA and fails on dist drift
- tests: TestPhase10CookieAuth, TestPhase10CSRF, TestPhase10Prefix,
  TestPhase10AdminPrefixCollision, boardwalk serving and header tests
This commit is contained in:
Jakub Zych
2026-09-27 15:34:19 +02:00
parent 5f9353841b
commit dafdb18234
15 changed files with 1283 additions and 18 deletions

View File

@@ -529,6 +529,9 @@ func BuildRouter(app *backpack.App, plugins []party.Plugin) (*Router, error) {
}
r.BindPlugin("summercms.cabana")
admin.Mount(r)
if err := r.checkAdminPrefix(admin.Prefix); err != nil {
return nil, err
}
}
for _, rt := range r.routes {
if _, err := r.wrap(rt); err != nil {
@@ -538,6 +541,23 @@ func BuildRouter(app *backpack.App, plugins []party.Plugin) (*Router, error) {
return r, nil
}
// checkAdminPrefix fails boot when a plugin other than cabana owns a route at
// or under the admin prefix: the admin SPA and API own that whole subtree.
func (r *Router) checkAdminPrefix(prefix string) error {
if prefix == "" {
return nil
}
for _, rt := range r.routes {
if rt.pluginID == "summercms.cabana" {
continue
}
if rt.path == prefix || strings.HasPrefix(rt.path, prefix+"/") {
return fmt.Errorf("surf: route %s %s (plugin %q) is under the admin prefix backend.uri %s", rt.method, rt.path, rt.pluginID, prefix)
}
}
return nil
}
func requiredBytes(app *backpack.App, key string) (int64, error) {
raw, ok := app.Config.Lookup(key)
if !ok {