package handler import ( "encoding/json" "log/slog" "net/http" "os" "path/filepath" "strings" "codeberg.org/VARASYS/ZDDC/zddc/internal/config" "codeberg.org/VARASYS/ZDDC/zddc/internal/zddc" ) // ProjectInfo is a single entry in the project list response. // // Title is read from the project's own .zddc file (its `title:` field) when // present; absent or empty means the landing page shows just the directory // name. omitempty keeps the JSON small for projects without titles. type ProjectInfo struct { Name string `json:"name"` URL string `json:"url"` Title string `json:"title,omitempty"` } // ServeProjectList handles GET / with Accept: application/json. // It returns all top-level directories under cfg.Root that the requesting // user has access to, as a JSON array of ProjectInfo. func ServeProjectList(cfg config.Config, w http.ResponseWriter, r *http.Request) { email := EmailFromContext(r) entries, err := os.ReadDir(cfg.Root) if err != nil { slog.Error("reading root directory", "err", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } var projects []ProjectInfo for _, entry := range entries { if !entry.IsDir() { continue } name := entry.Name() // Skip hidden directories. Both '.' and '_' are reserved prefixes: // '.' for system/internal state (matches the listing-pipeline filter // and the dispatch dot-prefix guard); '_' for operator-managed // scaffolding like install.zip's _template/ directory that should // be reachable by direct URL but not appear in the project picker. if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") { continue } absPath := filepath.Join(cfg.Root, name) chain, err := zddc.EffectivePolicy(cfg.Root, absPath) if err != nil { slog.Warn("ACL policy error", "path", absPath, "err", err) } if !zddc.AllowedWithChain(chain, email) { continue } // Title comes from /.zddc — optional, ignored on parse error. var title string if zf, err := zddc.ParseFile(filepath.Join(absPath, ".zddc")); err == nil { title = zf.Title } projects = append(projects, ProjectInfo{ Name: name, URL: "/" + name + "/", Title: title, }) } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-cache") if err := json.NewEncoder(w).Encode(projects); err != nil { slog.Error("encoding project list", "err", err) } }