package handler import ( "html/template" "net/http" "os" "path/filepath" "codeberg.org/VARASYS/ZDDC/zddc/internal/config" "codeberg.org/VARASYS/ZDDC/zddc/internal/zddc" ) // editorView is the data passed to the editor template. Field naming is // kept short for template ergonomics. type editorView struct { Path string IsRoot bool CanEdit bool Exists bool Email string HasCustomCSS bool File zddc.ZddcFile EffectiveChain []chainEntry ProfilePathPrefix string // /.profile AssetsPathPrefix string // /.profile/zddc/assets } // serveZddcEditor renders the form-based .zddc editor at // GET /.profile/zddc/edit?path=. The form posts JSON back to // /.profile/zddc?path=; the inline JS shim handles dynamic-row // add/remove and surfaces field errors from the JSON response. func serveZddcEditor(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 } email := EmailFromContext(r) abs, err := resolvePath(cfg.Root, r.URL.Query().Get("path")) if err != nil { http.NotFound(w, r) return } zf, err := zddc.ParseFile(filepath.Join(abs, ".zddc")) if err != nil { http.Error(w, "Cannot parse existing .zddc: "+err.Error(), http.StatusBadRequest) return } exists := false if _, err := os.Stat(filepath.Join(abs, ".zddc")); err == nil { exists = true } chain, _ := zddc.EffectivePolicy(cfg.Root, abs) dirs := chainDirs(cfg.Root, abs) entries := make([]chainEntry, 0, len(chain.Levels)) for i, level := range chain.Levels { levelDir := dirs[i] levelExists := false if _, err := os.Stat(filepath.Join(levelDir, ".zddc")); err == nil { levelExists = true } entries = append(entries, chainEntry{ Dir: urlPathOf(cfg.Root, levelDir), Exists: levelExists, Title: level.Title, ACL: level.ACL, Admins: level.Admins, }) } view := editorView{ Path: urlPathOf(cfg.Root, abs), IsRoot: abs == cfg.Root, CanEdit: zddc.CanEditZddc(cfg.Root, abs, email), Exists: exists, Email: email, HasCustomCSS: hasCustomProfileCSS(cfg.Root), File: zf, EffectiveChain: entries, ProfilePathPrefix: ProfilePathPrefix, AssetsPathPrefix: zddcAssetsPathPrefix, } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") if err := editorTemplate.Execute(w, view); err != nil { // Headers may already be flushed; best effort. http.Error(w, err.Error(), http.StatusInternalServerError) } } // editorTemplate is the html/template body for the editor page. // // Style choices: // - inline CSS uses the same custom-property naming as shared/base.css // so a future server-side merge with shared/base.css remains trivial. // - inline JS is one IIFE, ~80 lines, handling: add/remove row, // collect-into-JSON-on-submit, render server-side field errors. // - the form falls back to a plain HTTP POST (urlencoded) without JS; // a tiny same-handler endpoint accepts urlencoded too. (V1: JS only; // no-JS fallback is documented as a TODO in the file header.) var editorTemplate = template.Must(template.New("editor").Parse(` .zddc editor — {{ .Path }} {{ if .HasCustomCSS }}{{ end }}

.zddc editor

{{ if .IsRoot }}Editing the root /.zddc.{{ else }}Editing {{ .Path }}/.zddc.{{ end }} You are signed in as {{ .Email }}.

{{ if not .CanEdit }}
Read-only. You can view this file's contents and the inherited rules below, but you do not have permission to edit it. Subtree admins cannot edit the .zddc file that grants their own authority — only an admin from a higher level can.
{{ end }}

Title

Surfaced on the project picker for this folder. Optional — projects without a title show their directory name.

ACL — Allow

Email-glob patterns for users granted access here. Examples: *@example.com, alice@*, alice@example.com. * matches any non-empty email but does not cross the @ boundary.

{{ range $i, $v := .File.ACL.Allow }}
{{ end }}

ACL — Deny

Deny is checked first; a parent allow cannot override a deeper deny. Same glob syntax as Allow.

{{ range $i, $v := .File.ACL.Deny }}
{{ end }}

{{ if .IsRoot }}Super-admins (bootstrap){{ else }}Subtree admins of {{ .Path }}{{ end }}

{{ if .IsRoot }}Anyone here is an unrestricted admin of the entire server. They can edit any .zddc file, including this one. The very first super-admin is created by hand-editing this file at server install time. You cannot remove yourself from this list. {{ else }}Anyone here can edit .zddc files anywhere below this directory. They cannot edit this file (where their authority comes from), so they cannot remove their delegator or add peers at their own level. {{ end }}

{{ range $i, $v := .File.Admins }}
{{ end }}
Effective chain (inherited rules) {{ range .EffectiveChain }}
{{ .Dir }}/.zddc {{ if not .Exists }}(no file at this level){{ end }}
title: {{ .Title }}
allow: {{ range .ACL.Allow }}{{ . }} {{ end }}
deny:  {{ range .ACL.Deny }}{{ . }} {{ end }}
admins:{{ range .Admins }} {{ . }}{{ end }}
{{ end }}
Cancel / refresh
`))