admin.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. // M13a W5: admin HTTP routes for deliverd-fcm. Two routes
  2. // are registered when BA_AUTHD_JWT_SECRET is set:
  3. //
  4. // GET /v1/admin/dlq — list DLQ rows for the
  5. // `fcm` channel. Filters: company_id, alert_id, include=
  6. // all. Read-only. Any authenticated user.
  7. // GET /v1/admin/dlq/{id} — fetch one row, including
  8. // payload (for inspection before replay). Read-only.
  9. //
  10. // These mirror the global /v1/dlq endpoints in admind, but
  11. // scoped to the FCM channel so a per-channel deliverd can
  12. // answer "what's stuck in my DLQ" without needing access
  13. // to admind.
  14. //
  15. // When BA_AUTHD_JWT_SECRET is unset, no admin routes are
  16. // registered.
  17. package main
  18. import (
  19. "encoding/json"
  20. "log/slog"
  21. "net/http"
  22. "strconv"
  23. "git3.techno-world.net/lrosales/broad-announce/internal/authd"
  24. "git3.techno-world.net/lrosales/broad-announce/internal/dlq"
  25. "git3.techno-world.net/lrosales/broad-announce/internal/postgres"
  26. )
  27. // wireAdminRoutes wires the admin routes onto mux.
  28. func wireAdminRoutes(mux *http.ServeMux, pool *postgres.Pool, logger *slog.Logger) {
  29. if !authd.EnvEnabled() {
  30. logger.Warn("admin routes are DISABLED (set BA_AUTHD_JWT_SECRET to enable)")
  31. return
  32. }
  33. ad, err := authd.NewFromEnv()
  34. if err != nil {
  35. logger.Error("BA_AUTHD_JWT_SECRET set but authd init failed; admin routes DISABLED", "err", err)
  36. return
  37. }
  38. logger.Info("admin routes enabled with JWT gate (fcm)")
  39. mux.Handle("GET /v1/admin/dlq",
  40. ad.RequireAuth(http.HandlerFunc(handleListDLQ(pool, logger))))
  41. mux.Handle("GET /v1/admin/dlq/{id}",
  42. ad.RequireAuth(http.HandlerFunc(handleGetDLQ(pool, logger))))
  43. }
  44. func handleListDLQ(pool *postgres.Pool, logger *slog.Logger) http.HandlerFunc {
  45. return func(w http.ResponseWriter, r *http.Request) {
  46. limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
  47. offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
  48. rows, err := dlq.List(r.Context(), pool, dlq.ListFilters{
  49. Channel: fcmChannel,
  50. CompanyID: r.URL.Query().Get("company_id"),
  51. AlertID: r.URL.Query().Get("alert_id"),
  52. IncludeDiscarded: r.URL.Query().Get("include") == "all",
  53. Limit: limit,
  54. Offset: offset,
  55. })
  56. if err != nil {
  57. logger.Error("dlq list (fcm)", "err", err)
  58. http.Error(w, "dlq list: "+err.Error(), http.StatusInternalServerError)
  59. return
  60. }
  61. w.Header().Set("Content-Type", "application/json")
  62. _ = json.NewEncoder(w).Encode(map[string]any{
  63. "channel": fcmChannel,
  64. "rows": rows,
  65. "limit": effectiveLimit(limit),
  66. "offset": effectiveOffset(offset),
  67. })
  68. }
  69. }
  70. func handleGetDLQ(pool *postgres.Pool, logger *slog.Logger) http.HandlerFunc {
  71. return func(w http.ResponseWriter, r *http.Request) {
  72. raw := r.PathValue("id")
  73. id, err := strconv.ParseInt(raw, 10, 64)
  74. if err != nil || id <= 0 {
  75. http.Error(w, "bad id", http.StatusBadRequest)
  76. return
  77. }
  78. row, err := dlq.Get(r.Context(), pool, id)
  79. if err != nil {
  80. logger.Error("dlq get (fcm)", "err", err, "id", id)
  81. http.Error(w, "dlq get: "+err.Error(), http.StatusInternalServerError)
  82. return
  83. }
  84. if row == nil {
  85. http.Error(w, "not found", http.StatusNotFound)
  86. return
  87. }
  88. // Cross-channel safety: this deliverd-fcm binary only
  89. // knows about the fcm channel. If a row from another
  90. // channel ends up in the URL, treat it as not-found
  91. // rather than leaking data.
  92. if row.Channel != fcmChannel {
  93. http.Error(w, "not found", http.StatusNotFound)
  94. return
  95. }
  96. w.Header().Set("Content-Type", "application/json")
  97. _ = json.NewEncoder(w).Encode(row)
  98. }
  99. }
  100. func effectiveLimit(n int) int {
  101. if n <= 0 || n > 500 {
  102. return 50
  103. }
  104. return n
  105. }
  106. func effectiveOffset(n int) int {
  107. if n < 0 {
  108. return 0
  109. }
  110. return n
  111. }