// M13a W5: admin HTTP routes for deliverd-fcm. Two routes // are registered when BA_AUTHD_JWT_SECRET is set: // // GET /v1/admin/dlq — list DLQ rows for the // `fcm` channel. Filters: company_id, alert_id, include= // all. Read-only. Any authenticated user. // GET /v1/admin/dlq/{id} — fetch one row, including // payload (for inspection before replay). Read-only. // // These mirror the global /v1/dlq endpoints in admind, but // scoped to the FCM channel so a per-channel deliverd can // answer "what's stuck in my DLQ" without needing access // to admind. // // When BA_AUTHD_JWT_SECRET is unset, no admin routes are // registered. package main import ( "encoding/json" "log/slog" "net/http" "strconv" "git3.techno-world.net/lrosales/broad-announce/internal/authd" "git3.techno-world.net/lrosales/broad-announce/internal/dlq" "git3.techno-world.net/lrosales/broad-announce/internal/postgres" ) // wireAdminRoutes wires the admin routes onto mux. func wireAdminRoutes(mux *http.ServeMux, pool *postgres.Pool, logger *slog.Logger) { if !authd.EnvEnabled() { logger.Warn("admin routes are DISABLED (set BA_AUTHD_JWT_SECRET to enable)") return } ad, err := authd.NewFromEnv() if err != nil { logger.Error("BA_AUTHD_JWT_SECRET set but authd init failed; admin routes DISABLED", "err", err) return } logger.Info("admin routes enabled with JWT gate (fcm)") mux.Handle("GET /v1/admin/dlq", ad.RequireAuth(http.HandlerFunc(handleListDLQ(pool, logger)))) mux.Handle("GET /v1/admin/dlq/{id}", ad.RequireAuth(http.HandlerFunc(handleGetDLQ(pool, logger)))) } func handleListDLQ(pool *postgres.Pool, logger *slog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) rows, err := dlq.List(r.Context(), pool, dlq.ListFilters{ Channel: fcmChannel, CompanyID: r.URL.Query().Get("company_id"), AlertID: r.URL.Query().Get("alert_id"), IncludeDiscarded: r.URL.Query().Get("include") == "all", Limit: limit, Offset: offset, }) if err != nil { logger.Error("dlq list (fcm)", "err", err) http.Error(w, "dlq list: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "channel": fcmChannel, "rows": rows, "limit": effectiveLimit(limit), "offset": effectiveOffset(offset), }) } } func handleGetDLQ(pool *postgres.Pool, logger *slog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { raw := r.PathValue("id") id, err := strconv.ParseInt(raw, 10, 64) if err != nil || id <= 0 { http.Error(w, "bad id", http.StatusBadRequest) return } row, err := dlq.Get(r.Context(), pool, id) if err != nil { logger.Error("dlq get (fcm)", "err", err, "id", id) http.Error(w, "dlq get: "+err.Error(), http.StatusInternalServerError) return } if row == nil { http.Error(w, "not found", http.StatusNotFound) return } // Cross-channel safety: this deliverd-fcm binary only // knows about the fcm channel. If a row from another // channel ends up in the URL, treat it as not-found // rather than leaking data. if row.Channel != fcmChannel { http.Error(w, "not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(row) } } func effectiveLimit(n int) int { if n <= 0 || n > 500 { return 50 } return n } func effectiveOffset(n int) int { if n < 0 { return 0 } return n }