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 /.admin.css exists. The // editor template uses this to conditionally inject the 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/. V1 only ships // `custom.css` (passthrough of /.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) } }