ZDDC/zddc/internal/handler/zddc_assets.go
ZDDC e44ccc3500 feat(zddc-server): delegated subtree admins + built-in .zddc editor
Generalize the admin model from "single root super-admin" to a
delegated chain: a `<dir>/.zddc/admins` list grants admin authority
for that subtree, with a strict-ancestor rule preventing
self-elevation (you cannot edit the .zddc that grants your own
authority — only files strictly below it).

Add a guided server-rendered editor at /.admin/zddc/edit?path=<dir>
so subtree admins can manage their fiefdoms without filesystem
access. JSON API at /.admin/zddc covers GET (file + effective chain
+ can_edit), POST (atomic write + cache invalidation), DELETE,
plus a /tree endpoint listing every .zddc visible to the caller.
Optional theming via <root>/.admin.css.

Validation: glob syntax check, root-self-demotion rejection,
reserved-prefix path guard, YAML round-trip sanity. Writes are
atomic (temp file + fsync + rename) and invalidate the policy
cache.

Also includes the prior in-flight `Title` field on ProjectInfo
so per-project .zddc titles surface on the landing-page picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:52:06 -05:00

58 lines
2 KiB
Go

package handler
import (
"net/http"
"os"
"path/filepath"
"strings"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
)
// adminCustomCSSName is the on-disk filename a server operator places at
// the root to theme the admin pages. It deliberately uses the .admin.css
// suffix (not just custom.css) so it pattern-matches the .zddc / .admin
// reserved-prefix family, and so anyone scanning the root tree sees it
// is admin-related.
const adminCustomCSSName = ".admin.css"
// hasCustomAdminCSS reports whether <fsRoot>/.admin.css exists. The
// editor template uses this to conditionally inject the <link> tag.
func hasCustomAdminCSS(fsRoot string) bool {
_, err := os.Stat(filepath.Join(fsRoot, adminCustomCSSName))
return err == nil
}
// zddcAssetsPathPrefix is the URL prefix for admin-only static assets.
// They sit under /.admin/zddc/assets/ rather than /.admin/assets/ so
// they share the editor's broader auth gate (subtree-or-super-admin)
// instead of /.admin/'s super-admin-only gate — otherwise a subtree
// admin would 404 on the custom CSS link emitted by the editor page.
const zddcAssetsPathPrefix = ZddcAdminPathPrefix + "/assets"
// serveZddcAssets handles /.admin/zddc/assets/<file>. V1 only ships
// `custom.css` (passthrough of <root>/.admin.css when present); other
// paths return 404 so we don't accidentally expose arbitrary files.
// hasAnyAdminScope has already gated the request via ServeZddc.
func serveZddcAssets(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, zddcAssetsPathPrefix+"/")
switch rest {
case "custom.css":
path := filepath.Join(cfg.Root, adminCustomCSSName)
fi, err := os.Stat(path)
if 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)
}
}