Files
synapbus/internal/api/deadletters_handler.go
T
Algis DumbrisandClaude Opus 4.6 2575ce2626 feat: OAuth 2.1 with PKCE, MCP auth, dead letters, channel management, and UX polish
- Add OAuth 2.1 identity provider with PKCE S256 (ory/fosite)
- Add RFC 7591 dynamic client registration for MCP clients
- Add RFC 8414 OAuth metadata discovery endpoint
- Add branded OAuth login/authorize pages with SynapBus design
- Add SYNAPBUS_BASE_URL env var for remote/LAN deployments
- Add OAuth bearer token authentication for MCP connections
- Add dead letter queue with Web UI management page
- Add channel leave, member list, and improved channel management
- Add agent auth middleware for MCP-authenticated requests
- Add console printer for structured server startup output
- Hide human accounts from agent management UI
- Fix SSE through middleware (Flush/Unwrap support)
- Fix graceful shutdown by closing SSE clients before server stop
- Fix localhost/127.0.0.1 redirect URI normalization for OAuth
- Remove agent self-registration MCP tools (manage via Web UI only)
- Update README with OAuth setup guide and MCP client config example

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:54:02 +02:00

96 lines
2.8 KiB
Go

package api
import (
"log/slog"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/synapbus/synapbus/internal/messaging"
)
// DeadLettersHandler handles REST API requests for the dead letter queue.
type DeadLettersHandler struct {
deadLetterStore *messaging.DeadLetterStore
logger *slog.Logger
}
// NewDeadLettersHandler creates a new dead letters handler.
func NewDeadLettersHandler(dls *messaging.DeadLetterStore) *DeadLettersHandler {
return &DeadLettersHandler{
deadLetterStore: dls,
logger: slog.Default().With("component", "api.deadletters"),
}
}
// List handles GET /api/dead-letters.
func (h *DeadLettersHandler) List(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
// Parse query params
includeAcknowledged := r.URL.Query().Get("acknowledged") == "true"
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 {
limit = 50
}
letters, total, err := h.deadLetterStore.ListDeadLetters(r.Context(), ownerID, includeAcknowledged, limit)
if err != nil {
h.logger.Error("list dead letters failed", "error", err)
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to list dead letters"))
return
}
writeJSON(w, http.StatusOK, map[string]any{
"dead_letters": letters,
"total": total,
})
}
// Acknowledge handles POST /api/dead-letters/{id}/acknowledge.
func (h *DeadLettersHandler) Acknowledge(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
writeJSON(w, http.StatusBadRequest, errorBody("invalid_id", "Invalid dead letter ID"))
return
}
if err := h.deadLetterStore.AcknowledgeDeadLetter(r.Context(), id, ownerID); err != nil {
h.logger.Error("acknowledge dead letter failed", "error", err)
writeJSON(w, http.StatusBadRequest, errorBody("acknowledge_failed", err.Error()))
return
}
writeJSON(w, http.StatusOK, map[string]any{"acknowledged": true})
}
// Count handles GET /api/dead-letters/count.
func (h *DeadLettersHandler) Count(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
count, err := h.deadLetterStore.CountUnacknowledged(r.Context(), ownerID)
if err != nil {
h.logger.Error("count dead letters failed", "error", err)
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to count dead letters"))
return
}
writeJSON(w, http.StatusOK, map[string]any{"count": count})
}