package hammerhead import ( "errors" "net/http" "net/url" "sort" "time" ) // The same handlers serve an organization's channels and the server-level ones // (for edges, which belong to nobody); only the scope differs. type alertScope struct { org string // "" = server level trusted bool // may allow private webhook targets actor string } type channelView struct { AlertChannel Preview string `json:"preview"` } func preview(ch AlertChannel) string { if ch.Kind != "webhook" { return ch.Target } if u, err := url.Parse(ch.Target); err == nil { return u.Scheme + "://" + u.Host + "/…" } return "webhook" } type alertRow struct { Key string `json:"key"` Kind string `json:"kind"` Name string `json:"name"` Message string `json:"message"` Since time.Time `json:"since"` Fired bool `json:"fired"` } func (s *Server) alertOverview(w http.ResponseWriter, sc alertScope) { s.mu.Lock() chans := []channelView{} for _, c := range s.st.Channels { if c.OrgID == sc.org { v := channelView{AlertChannel: *c, Preview: preview(*c)} v.Target = "" // webhook URLs often contain tokens if c.Kind == "email" { v.Target = c.Target } chans = append(chans, v) } } active := []alertRow{} for _, a := range s.alerts { if a.OrgID == sc.org { active = append(active, alertRow{a.Key, a.Kind, a.Name, a.Message, a.Since, a.Fired}) } } sort.Slice(active, func(i, j int) bool { return active[i].Since.Before(active[j].Since) }) settings := map[string]int{"debounceMin": 2, "repeatHours": 0} if o := s.findOrg(sc.org); o != nil { if o.AlertDebounceMin > 0 { settings["debounceMin"] = o.AlertDebounceMin } settings["repeatHours"] = o.AlertRepeatHours } mailOK := s.st.Settings.SMTP.Host != "" s.mu.Unlock() writeJSON(w, 200, map[string]any{"channels": chans, "active": active, "history": s.db.queryAlertEvents(sc.org, 100), "settings": settings, "emailConfigured": mailOK}) } func (s *Server) saveChannel(w http.ResponseWriter, r *http.Request, sc alertScope) { var in AlertChannel if !decode(w, r, &in) { return } s.mu.Lock() defer s.mu.Unlock() id := r.PathValue("id") var existing *AlertChannel if id != "" { for _, c := range s.st.Channels { if c.ID == id && c.OrgID == sc.org { existing = c } } if existing == nil { fail(w, 404, errors.New("no such channel")) return } if in.Target == "" { in.Target = existing.Target // blank keeps the saved URL/address } } trusted := sc.trusted if err := s.validateChannel(&in, trusted); err != nil { fail(w, 400, err) return } in.OrgID = sc.org if existing != nil { in.ID = id *existing = in } else { in.ID = newID() s.st.Channels = append(s.st.Channels, &in) } s.save() s.audit(sc.org, sc.actor, "alert.channel.save", in.Name) ok(w) } func (s *Server) deleteChannel(w http.ResponseWriter, r *http.Request, sc alertScope) { id := r.PathValue("id") s.mu.Lock() defer s.mu.Unlock() s.st.Channels = filter(s.st.Channels, func(c *AlertChannel) bool { return !(c.ID == id && c.OrgID == sc.org) }) s.save() ok(w) } func (s *Server) testChannel(w http.ResponseWriter, r *http.Request, sc alertScope) { id := r.PathValue("id") s.mu.Lock() var ch *AlertChannel for _, c := range s.st.Channels { if c.ID == id && c.OrgID == sc.org { cp := *c ch = &cp } } name := "server" if o := s.findOrg(sc.org); o != nil { name = o.Name } s.mu.Unlock() if ch == nil { fail(w, 404, errors.New("no such channel")) return } now := time.Now() if err := s.send(*ch, alertNote{Status: "test", Org: name, Kind: "test", Name: "Test alert", Message: "test", Since: now, Time: now}); err != nil { fail(w, 400, err) return } ok(w) } func (s *Server) alertSettings(w http.ResponseWriter, r *http.Request, sc alertScope) { var in struct{ DebounceMin, RepeatHours int } if !decode(w, r, &in) { return } if in.DebounceMin < 1 || in.DebounceMin > 120 || in.RepeatHours < 0 || in.RepeatHours > 168 { fail(w, 400, errors.New("Wait 1 to 120 minutes before alerting, and remind every 0 to 168 hours (0 means no reminders).")) return } s.mu.Lock() defer s.mu.Unlock() o := s.findOrg(sc.org) if o == nil { fail(w, 404, errors.New("organization settings only")) return } o.AlertDebounceMin, o.AlertRepeatHours = in.DebounceMin, in.RepeatHours s.save() ok(w) } // wrappers: an organization's admins manage its channels; server admins manage the server-level ones func (s *Server) orgAlertScope(c *orgCtx) alertScope { return alertScope{org: c.Org.ID, trusted: c.Org.AllowLocal || c.User.ServerAdmin, actor: c.Actor} } func (s *Server) withOrgAlerts(fn func(http.ResponseWriter, *http.Request, alertScope)) http.HandlerFunc { return s.withOrg(true, func(w http.ResponseWriter, r *http.Request, c *orgCtx) { fn(w, r, s.orgAlertScope(c)) }) } func (s *Server) withServerAlerts(fn func(http.ResponseWriter, *http.Request, alertScope)) http.HandlerFunc { return s.withServerAdmin(func(w http.ResponseWriter, r *http.Request, p *principal) { fn(w, r, alertScope{org: "", trusted: true, actor: p.User.Email}) }) } func (s *Server) registerAlertRoutes(mux *http.ServeMux) { overview := func(w http.ResponseWriter, r *http.Request, sc alertScope) { s.alertOverview(w, sc) } mux.HandleFunc("GET /api/orgs/{org}/alerts", s.withOrgAlerts(overview)) mux.HandleFunc("POST /api/orgs/{org}/alerts/channels", s.withOrgAlerts(s.saveChannel)) mux.HandleFunc("PUT /api/orgs/{org}/alerts/channels/{id}", s.withOrgAlerts(s.saveChannel)) mux.HandleFunc("DELETE /api/orgs/{org}/alerts/channels/{id}", s.withOrgAlerts(s.deleteChannel)) mux.HandleFunc("POST /api/orgs/{org}/alerts/channels/{id}/test", s.withOrgAlerts(s.testChannel)) mux.HandleFunc("PUT /api/orgs/{org}/alerts/settings", s.withOrgAlerts(s.alertSettings)) mux.HandleFunc("GET /api/admin/alerts", s.withServerAlerts(overview)) mux.HandleFunc("POST /api/admin/alerts/channels", s.withServerAlerts(s.saveChannel)) mux.HandleFunc("PUT /api/admin/alerts/channels/{id}", s.withServerAlerts(s.saveChannel)) mux.HandleFunc("DELETE /api/admin/alerts/channels/{id}", s.withServerAlerts(s.deleteChannel)) mux.HandleFunc("POST /api/admin/alerts/channels/{id}/test", s.withServerAlerts(s.testChannel)) }