mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-11 06:05:36 +00:00
137 lines
4.7 KiB
Go
137 lines
4.7 KiB
Go
package core
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jleight/meshtender/internal/web"
|
|
)
|
|
|
|
// This file backs the repeater "registry" tabs: site Documentation (public and
|
|
// internal) and Maintenance history. Documentation is owner-edited; maintenance
|
|
// entries can be logged by anyone with access (co-stewards), but only the owner
|
|
// can delete them.
|
|
|
|
const (
|
|
maxDocLen = 20000
|
|
maxMaintNoteLen = 2000
|
|
maxMaintAuthorLen = 120
|
|
)
|
|
|
|
// pageRepeaterDocs renders the Documentation tab. Anyone with access reads it;
|
|
// the owner gets edit forms. The internal section is only shown to people with
|
|
// access (which everyone reaching this page has) — never on the public page.
|
|
func (s *Handlers) pageRepeaterDocs(w http.ResponseWriter, r *http.Request) {
|
|
rep, _, ok := s.requireRepeaterAccess(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
isOwner := !rep.Shared
|
|
s.Render(w, r, "repeater_docs.html", map[string]any{
|
|
"Repeater": rep,
|
|
"Nav": web.RepeaterNav(rep.PublicID, rep.Name, rep.OwnerName(), isOwner, "docs"),
|
|
"IsOwner": isOwner,
|
|
"Error": r.URL.Query().Get("error"),
|
|
})
|
|
}
|
|
|
|
// handleRepeaterDocs saves the public and internal documentation (owner only).
|
|
func (s *Handlers) handleRepeaterDocs(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := s.requireOwned(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
docPublic := web.Clip(r.FormValue("doc_public"), maxDocLen)
|
|
docInternal := web.Clip(r.FormValue("doc_internal"), maxDocLen)
|
|
if err := s.Store.UpdateRepeaterDocs(r.Context(), s.Auth.CurrentUserID(r.Context()), id, docPublic, docInternal); err != nil {
|
|
web.RedirectErr(w, r, docsPath(repeaterParam(r)), "Could not save documentation.")
|
|
return
|
|
}
|
|
http.Redirect(w, r, docsPath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
|
}
|
|
|
|
// pageRepeaterMaintenance renders the Maintenance tab: the history plus, for
|
|
// anyone with access, a form to log a new entry.
|
|
func (s *Handlers) pageRepeaterMaintenance(w http.ResponseWriter, r *http.Request) {
|
|
rep, id, ok := s.requireRepeaterAccess(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
entries, err := s.Store.ListMaintenance(r.Context(), id)
|
|
if err != nil {
|
|
s.ServerError(w, r, "could not load maintenance history", err)
|
|
return
|
|
}
|
|
isOwner := !rep.Shared
|
|
s.Render(w, r, "repeater_maintenance.html", map[string]any{
|
|
"Repeater": rep,
|
|
"Nav": web.RepeaterNav(rep.PublicID, rep.Name, rep.OwnerName(), isOwner, "maintenance"),
|
|
"IsOwner": isOwner,
|
|
"Entries": entries,
|
|
"Today": time.Now().Format("2006-01-02"),
|
|
"Error": r.URL.Query().Get("error"),
|
|
})
|
|
}
|
|
|
|
// handleAddMaintenance logs a maintenance entry. Anyone with access may add one;
|
|
// the author is recorded for the history.
|
|
func (s *Handlers) handleAddMaintenance(w http.ResponseWriter, r *http.Request) {
|
|
rep, id, ok := s.requireRepeaterAccess(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
note := strings.TrimSpace(r.FormValue("note"))
|
|
if note == "" {
|
|
web.RedirectErr(w, r, maintPath(repeaterParam(r)), "A note is required.")
|
|
return
|
|
}
|
|
note = web.Clip(note, maxMaintNoteLen)
|
|
|
|
// performed_at comes from a <input type=date>; default to today if absent or
|
|
// unparseable so a bad value never blocks logging.
|
|
performed := time.Now()
|
|
if v := r.FormValue("performed_at"); v != "" {
|
|
if t, err := time.Parse("2006-01-02", v); err == nil {
|
|
performed = t
|
|
}
|
|
}
|
|
|
|
uid := s.Auth.CurrentUserID(r.Context())
|
|
authorName := ""
|
|
if u, err := s.Store.GetUserByID(r.Context(), uid); err == nil {
|
|
authorName = web.Clip(u.Name(), maxMaintAuthorLen)
|
|
}
|
|
if authorName == "" {
|
|
authorName = rep.OwnerName()
|
|
}
|
|
|
|
if err := s.Store.AddMaintenanceEntry(r.Context(), id, uid, authorName, note, performed); err != nil {
|
|
web.RedirectErr(w, r, maintPath(repeaterParam(r)), "Could not log maintenance entry.")
|
|
return
|
|
}
|
|
http.Redirect(w, r, maintPath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
|
}
|
|
|
|
// handleDeleteMaintenance removes a maintenance entry (owner only).
|
|
func (s *Handlers) handleDeleteMaintenance(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := s.requireOwned(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
entryID, err := strconv.ParseInt(r.FormValue("entry_id"), 10, 64)
|
|
if err != nil {
|
|
web.RedirectErr(w, r, maintPath(repeaterParam(r)), "Invalid entry.")
|
|
return
|
|
}
|
|
if err := s.Store.DeleteMaintenanceEntry(r.Context(), id, entryID); err != nil {
|
|
web.RedirectErr(w, r, maintPath(repeaterParam(r)), "Could not delete entry.")
|
|
return
|
|
}
|
|
http.Redirect(w, r, maintPath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
|
}
|
|
|
|
func docsPath(publicID string) string { return "/repeaters/" + publicID + "/docs" }
|
|
func maintPath(publicID string) string { return "/repeaters/" + publicID + "/maintenance" }
|