package handlers import ( "context" "fmt" "log" "net/http" "strconv" "strings" "time" "crowdsec-dashy/internal/crowdsec" ) // AlertsHandler manages the alerts page and its POST actions. type AlertsHandler struct { deps Deps } func NewAlertsHandler(deps Deps) *AlertsHandler { return &AlertsHandler{deps: deps} } const alertsPerPage = 50 // AlertsData is passed to the alerts template. type AlertsData struct { PageData Alerts []crowdsec.Alert Filter crowdsec.AlertFilter Page int HasNext bool ShowUpdates bool } // List renders the paginated alerts list. func (h *AlertsHandler) List(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } showUpdates := r.URL.Query().Get("show_updates") == "1" filter := crowdsec.AlertFilter{ Limit: alertsPerPage + 1, Offset: (page - 1) * alertsPerPage, Scenario: r.URL.Query().Get("scenario"), IP: r.URL.Query().Get("ip"), Since: r.URL.Query().Get("since"), } alerts, err := h.deps.LAPI.ListAlerts(ctx, filter) if err != nil { log.Printf("alerts: LAPI error: %v", err) pd := NewPageData(r, "Alerts", h.deps.CLIAvailable, h.deps.PollInterval) if crowdsec.IsForbidden(err) { pd = pd.WithFlash("error", "Access denied: re-register machine with admin rights: cscli machines delete crowdsec-dashy && cscli machines add crowdsec-dashy -a") } else { pd = pd.WithFlash("error", "Failed to fetch alerts from LAPI.") } h.deps.Renderer.Render(w, "alerts", AlertsData{PageData: pd}) return } hasNext := len(alerts) > alertsPerPage if hasNext { alerts = alerts[:alertsPerPage] } pd := NewPageData(r, "Alerts", h.deps.CLIAvailable, h.deps.PollInterval) if f := readFlash(r); f.Message != "" { pd.Flash = f } h.deps.Renderer.Render(w, "alerts", AlertsData{ PageData: pd, Alerts: alerts, Filter: filter, Page: page, HasNext: hasNext, ShowUpdates: showUpdates, }) } // Delete processes alert deletion (POST; `id` field may repeat for bulk). func (h *AlertsHandler) Delete(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 4096) if err := r.ParseForm(); err != nil { flashRedirect(w, r, "/alerts", "error", "invalid form data") return } if !checkCSRF(r) { http.Error(w, "forbidden", http.StatusForbidden) return } ids := r.Form["id"] if len(ids) == 0 { flashRedirect(w, r, "/alerts", "error", "no alert selected") return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() var errs []string deleted := 0 for _, s := range ids { id, err := strconv.ParseInt(s, 10, 64) if err != nil || id <= 0 { errs = append(errs, fmt.Sprintf("invalid id: %q", s)) continue } if err := h.deps.LAPI.DeleteAlert(ctx, id); err != nil { errs = append(errs, fmt.Sprintf("id %d: %v", id, err)) continue } deleted++ } if len(errs) > 0 { flashRedirect(w, r, "/alerts", "error", strings.Join(errs, "; ")) return } flashRedirect(w, r, "/alerts", "success", fmt.Sprintf("%d alert(s) deleted", deleted)) }