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>
58 lines
2 KiB
Go
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)
|
|
}
|
|
}
|