Files
Hammerhead/alerts_api.go
T

207 lines
6.3 KiB
Go

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 == "email" {
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, v.Token = "", "" // 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
}
if id != "" && in.Token == "" && in.Kind == "sandbar" {
in.Token = existing.Token // blank keeps the saved API key
}
}
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)
writeJSON(w, 200, map[string]any{"ok": true, "id": in.ID})
}
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))
}