ZDDC/zddc/internal/handler/zddc_assets.go
ZDDC cb46c2ef8c feat(zddc-server): user profile page replaces /.admin/
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>
2026-04-29 16:32:02 -05:00

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)
}
}