This commit is contained in:
2026-09-16 06:26:16 +02:00
parent 95b0779db0
commit 4f8e0bbb04
51 changed files with 6290 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
package httpserver
import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"net/http"
"os"
"strconv"
"time"
"github.com/example/notify-gateway/internal/gateway"
"github.com/example/notify-gateway/internal/mailingress"
"github.com/example/notify-gateway/internal/outbox"
)
func (s *Server) UseQueue(q *gateway.Queue, p *mailingress.Poller) { s.queue = q; s.mail = p }
func (s *Server) ready(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if s.queue == nil || s.queue.Store.Ping(ctx) != nil {
writeJSON(w, 503, map[string]any{"ok": false})
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) deliveries(w http.ResponseWriter, r *http.Request) {
if s.queue == nil {
http.Error(w, "outbox unavailable", 503)
return
}
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
jobs, err := s.queue.Store.List(r.Context(), 50, offset)
if err != nil {
http.Error(w, "outbox unavailable", 503)
return
}
counts, err := s.queue.Store.Counts(r.Context())
if err != nil {
http.Error(w, "outbox unavailable", 503)
return
}
statuses := []mailingress.Status{}
if s.mail != nil {
statuses = s.mail.Statuses()
}
writeJSON(w, 200, map[string]any{"deliveries": jobs, "counts": counts, "mail": statuses})
}
func (s *Server) deliveryHistory(w http.ResponseWriter, r *http.Request) {
if s.queue == nil {
http.Error(w, "outbox unavailable", 503)
return
}
rows, err := s.queue.Store.History(r.Context(), r.PathValue("id"))
if err != nil {
http.Error(w, "outbox unavailable", 503)
return
}
writeJSON(w, 200, rows)
}
func (s *Server) retryDelivery(w http.ResponseWriter, r *http.Request) {
if s.queue == nil {
http.Error(w, "outbox unavailable", 503)
return
}
err := s.queue.Store.Retry(r.Context(), r.PathValue("id"))
if errors.Is(err, outbox.ErrNotRetryable) {
http.Error(w, err.Error(), 409)
return
}
if err != nil {
http.Error(w, "outbox unavailable", 503)
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) metrics(w http.ResponseWriter, r *http.Request) {
token := os.Getenv("GATEWAY_METRICS_TOKEN")
if token == "" {
s.requireAdmin(s.writeMetrics)(w, r)
return
}
got := r.Header.Get("Authorization")
if subtle.ConstantTimeCompare([]byte(got), []byte("Bearer "+token)) != 1 {
http.Error(w, "unauthorized", 401)
return
}
s.writeMetrics(w, r)
}
func (s *Server) writeMetrics(w http.ResponseWriter, r *http.Request) {
if s.queue == nil {
http.Error(w, "outbox unavailable", 503)
return
}
counts, err := s.queue.Store.Counts(r.Context())
if err != nil {
http.Error(w, "outbox unavailable", 503)
return
}
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
fmt.Fprintln(w, "# HELP notify_gateway_deliveries Persisted deliveries by current state.\n# TYPE notify_gateway_deliveries gauge")
for _, state := range []string{"pending", "sending", "succeeded", "dry_run", "dead"} {
fmt.Fprintf(w, "notify_gateway_deliveries{state=%q} %d\n", state, counts[state])
}
}
func csrfToken(secret, session string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte("csrf:" + session))
return hex.EncodeToString(mac.Sum(nil))
}
func (s *Server) csrf(w http.ResponseWriter, r *http.Request) {
c, _ := r.Cookie("ng_session")
writeJSON(w, 200, map[string]string{"token": csrfToken(s.store.Get().Server.SessionSecret, c.Value)})
}