package handlers import ( "context" "fmt" "net/http" "time" "crowdsec-dashy/internal/crowdsec" ) // BouncersHandler manages the bouncers page and its POST actions. type BouncersHandler struct { deps Deps } func NewBouncersHandler(deps Deps) *BouncersHandler { return &BouncersHandler{deps: deps} } // BouncersData is passed to the bouncers template. type BouncersData struct { PageData Bouncers []crowdsec.Bouncer NewBouncer *crowdsec.AddBouncerResult // set immediately after add; shown exactly once } // List renders the bouncers list. func (h *BouncersHandler) List(w http.ResponseWriter, r *http.Request) { pd := NewPageData(r, "Bouncers", h.deps.CLIAvailable, h.deps.PollInterval) if f := readFlash(r); f.Message != "" { pd.Flash = f } var bouncers []crowdsec.Bouncer if h.deps.CLIAvailable { ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() var err error bouncers, err = h.deps.CLI.ListBouncers(ctx) if err != nil { pd.Flash = FlashMessage{Type: "error", Message: fmt.Sprintf("cscli error: %v", err)} } } h.deps.Renderer.Render(w, "bouncers", BouncersData{PageData: pd, Bouncers: bouncers}) } // Add registers a new bouncer and renders the API key reveal page (no redirect). // The key is shown exactly once in the POST response and never stored by the UI. func (h *BouncersHandler) Add(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 4096) if err := r.ParseForm(); err != nil { flashRedirect(w, r, "/bouncers", "error", "invalid form data") return } if !checkCSRF(r) { http.Error(w, "forbidden", http.StatusForbidden) return } if !h.deps.CLIAvailable { flashRedirect(w, r, "/bouncers", "error", "cscli not available") return } name := r.FormValue("name") if ok, _ := matchName(name); !ok { flashRedirect(w, r, "/bouncers", "error", "invalid name: use 1-64 alphanumeric/dash/underscore characters") return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() result, err := h.deps.CLI.AddBouncer(ctx, name) if err != nil { flashRedirect(w, r, "/bouncers", "error", fmt.Sprintf("failed to add bouncer: %v", err)) return } bouncers, _ := h.deps.CLI.ListBouncers(ctx) pd := NewPageData(r, "Bouncers", h.deps.CLIAvailable, h.deps.PollInterval) h.deps.Renderer.Render(w, "bouncers", BouncersData{ PageData: pd, Bouncers: bouncers, NewBouncer: result, }) } // Delete removes a bouncer by name. func (h *BouncersHandler) Delete(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 4096) if err := r.ParseForm(); err != nil { flashRedirect(w, r, "/bouncers", "error", "invalid form data") return } if !checkCSRF(r) { http.Error(w, "forbidden", http.StatusForbidden) return } if !h.deps.CLIAvailable { flashRedirect(w, r, "/bouncers", "error", "cscli not available") return } name := r.FormValue("name") if ok, _ := matchName(name); !ok { flashRedirect(w, r, "/bouncers", "error", "invalid bouncer name") return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() if err := h.deps.CLI.DeleteBouncer(ctx, name); err != nil { flashRedirect(w, r, "/bouncers", "error", fmt.Sprintf("failed to delete bouncer: %v", err)) return } flashRedirect(w, r, "/bouncers", "success", fmt.Sprintf("Bouncer %q deleted", name)) }