Replaces the super-admin-only /.admin/ surface with a public-by-default /.profile/ page that layers admin tools server-side based on the caller's effective access: - Universal (everyone, anonymous included): identity card, effective access summary, theme picker, localStorage utilities (export / import / clear, landing-presets viewer). - Subtree admins additionally see: editable .zddc files list (linking to the existing form-based editor) and a "Create new project folder" form. - Super-admins additionally see: server config, log viewer, whoami headers (the old /.admin/ JSON endpoints, repointed under /.profile/). Project creation is gated on CanEditZddc(newDir) — the same strict- ancestor rule that already governs .zddc writes — so no new authority concept is introduced. ValidateProjectName mirrors the existing reserved-prefix policy (no leading '.' or '_', no path separators). /.admin/* is hard-cut: no redirect shim. Old URLs fall through to the existing dot-prefix guard and 404. Custom CSS file rename: prefer <root>/.profile.css, fall back to legacy <root>/.admin.css. Per-resource 404 leakage gates preserved on whoami / config / logs / zddc / projects so non-admin callers cannot detect the existence of admin-only sub-resources. Tree-wide gofmt -w applied as a side-effect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.5 KiB
Go
70 lines
2.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
|
|
)
|
|
|
|
// profileCustomCSSName is the preferred on-disk filename for operator-supplied
|
|
// profile / editor theming. The legacy `.admin.css` is honored as a fallback
|
|
// so an operator who already deployed the older name does not see their
|
|
// styling vanish on upgrade; new deployments should use the `.profile.css`
|
|
// name.
|
|
const (
|
|
profileCustomCSSName = ".profile.css"
|
|
adminCustomCSSName = ".admin.css" // legacy fallback
|
|
)
|
|
|
|
// hasCustomProfileCSS reports whether <fsRoot>/.profile.css (or the legacy
|
|
// .admin.css) exists. The editor and profile templates use 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
|
|
}
|
|
|
|
// zddcAssetsPathPrefix is the URL prefix for admin-only static assets.
|
|
// They sit under /.profile/zddc/assets/ rather than /.profile/assets/ so
|
|
// they share the editor's broader auth gate (subtree-or-super-admin)
|
|
// instead of /.profile/'s super-admin-only diagnostics gate — otherwise a
|
|
// subtree admin would 404 on the custom CSS link emitted by the editor.
|
|
const zddcAssetsPathPrefix = ZddcProfilePathPrefix + "/assets"
|
|
|
|
// serveZddcAssets handles /.profile/zddc/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. 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, 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)
|
|
}
|
|
}
|