Polish pass after the big refactor in 2d114fc.
== Header elevation slot propagated ==
shared/elevation.{js,css} surface a header checkbox for admins.
30-minute sudo-style cookie window (Max-Age=1800, SameSite=Lax).
Only renders when /.profile/access reports can_elevate=true; quiet
for non-admins. Slot added to all 7 tool templates and concat'd
into all 7 build.sh files; admin in any tool now sees the toggle.
Three text-rename ride-alongs in archive/classifier/transmittal
templates: "Add Local Directory" → "Use Local Directory" (the same
rename that landed in browse earlier in this branch).
== Docs ==
- CLAUDE.md gets an "Admin elevation is sudo-style" paragraph in
the "Things that bite if you forget" section.
- AGENTS.md gets a dedicated "Admin elevation (sudo-style)" section
alongside "Bearer tokens" — same depth as the existing auth docs.
== Helper file splits ==
The retired form editor's shared helpers got bundled into a single
zddc_admin.go in the cleanup; that name is now misleading. Split by
concern:
- admin_helpers.go: hasAnyAdminScope (the only admin-specific helper)
- paths.go: resolvePath, urlPathOf, chainDirs (URL ↔ filesystem path
math — used by several profile / zddc-file handlers)
- profile_assets.go (renamed from zddc_admin_assets.go): custom CSS
pipeline. URL renamed from /.profile/zddc/assets/ → /.profile/assets/
since /.profile/zddc/ no longer hosts an editor.
- treeEntry moves to profilehandler.go (alongside AccessView, its
only consumer).
- writeError moves to profileprojects.go (its only consumer).
== Smell cleanup ==
- zddc.HasAnyAdminGrant(fsRoot, email) — new elevation-independent
primitive that walks the cascade and reports whether email is named
in any admin: list anywhere. Replaces the synthetic-elevated probe
hack in enumerateAccess (`Principal{Email, Elevated: true}` was
"lying" to the elevation gate to ask what it would say). The handler's
hasAnyAdminScope collapses to a 4-line wrapper that gates on
p.Elevated and delegates.
- Access-log middleware records `elevated` per request, so forensics
can distinguish "admin acting as user" from "admin exercising power."
- browse/js/app.js's ?file= deep link walks multi-segment paths. Each
intermediate segment is matched + expanded; the leaf gets
selected/previewed. Auto-shows hidden when any segment starts with
. or _. Silently no-ops on unresolved segments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.4 KiB
Go
71 lines
2.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
|
|
)
|
|
|
|
// Custom CSS pipeline. Lets an operator drop `.profile.css` (or the
|
|
// legacy `.admin.css`) at the deployment root and have it picked up
|
|
// automatically as styling for the profile page. Previously lived
|
|
// alongside the retired form editor; kept because the profile page
|
|
// still relies on it.
|
|
|
|
const (
|
|
profileCustomCSSName = ".profile.css"
|
|
adminCustomCSSName = ".admin.css" // legacy fallback
|
|
)
|
|
|
|
// hasCustomProfileCSS reports whether <fsRoot>/.profile.css (or the
|
|
// legacy .admin.css) exists. The profile template uses this to decide
|
|
// whether to inject the <link> tag.
|
|
func hasCustomProfileCSS(fsRoot string) bool {
|
|
if _, err := os.Stat(filepath.Join(fsRoot, profileCustomCSSName)); err == nil {
|
|
return true
|
|
}
|
|
if _, err := os.Stat(filepath.Join(fsRoot, adminCustomCSSName)); err == nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// profileAssetsPathPrefix is the URL prefix for admin static assets.
|
|
// Lived at /.profile/zddc/assets/ during the form-editor era; renamed
|
|
// once the form editor retired. The only consumer is the profile page,
|
|
// which emits a <link> to /custom.css when an operator has placed one
|
|
// at root.
|
|
const profileAssetsPathPrefix = ProfilePathPrefix + "/assets"
|
|
|
|
// serveProfileAssets handles GET /.profile/assets/<file>. V1 only
|
|
// ships `custom.css` (passthrough of <root>/.profile.css when
|
|
// present, falling back to <root>/.admin.css); other paths return
|
|
// 404 so we don't accidentally expose arbitrary files. The caller
|
|
// (profilehandler.go) has already gated on admin scope.
|
|
func serveProfileAssets(cfg config.Config, w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.Header().Set("Allow", "GET")
|
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
rest := strings.TrimPrefix(r.URL.Path, profileAssetsPathPrefix+"/")
|
|
switch rest {
|
|
case "custom.css":
|
|
path := filepath.Join(cfg.Root, profileCustomCSSName)
|
|
if fi, err := os.Stat(path); err != nil || fi.IsDir() {
|
|
path = filepath.Join(cfg.Root, adminCustomCSSName)
|
|
if fi, err := os.Stat(path); err != nil || fi.IsDir() {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
http.ServeFile(w, r, path)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}
|