Files
synapbus/internal/api/push_handler.go
T
Algis DumbrisandClaude Opus 4.6 30d62de350 feat: v0.7.0 — analytics dashboard, PWA, UX fixes, MCP prompts
Analytics: time-series message graph with 5 time spans (1h/4h/24h/7d/30d),
top-5 agents and channels leaderboards, summary cards. 4 new REST endpoints.

PWA: web app manifest, service worker with cache-first static/network-only
API strategy, push notifications via Web Push API with VAPID keys, push
subscription management endpoints, SQLite migration for subscriptions.

UX fixes: auto-resize compose textarea (3-12 lines), inline editable agent
display name, editable human display name in settings, smart mention/channel
highlighting (existing→link, deleted→inactive badge, unknown→plain text),
font size -/+ preference (12-24px persisted in localStorage), version footer
with GitHub link.

MCP: 4 prompts — daily-digest, agent-health-check, channel-overview,
debug-agent. Registered with prompt capabilities enabled.

Code review fixes: scoped push unsubscribe to user, capped analytics limit
at 100, hardened HTML strip regex, bounded SW cache, backend push unsub on
disable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:36:58 +02:00

113 lines
3.4 KiB
Go

package api
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/synapbus/synapbus/internal/push"
)
// PushHandler manages Web Push notification subscription endpoints.
type PushHandler struct {
pushService *push.Service
logger *slog.Logger
}
// NewPushHandler creates a new push notification handler.
func NewPushHandler(pushService *push.Service) *PushHandler {
return &PushHandler{
pushService: pushService,
logger: slog.Default().With("component", "api.push"),
}
}
// subscribeRequest is the JSON body for POST /api/push/subscribe.
type subscribeRequest struct {
Endpoint string `json:"endpoint"`
KeyP256dh string `json:"key_p256dh"`
KeyAuth string `json:"key_auth"`
}
// unsubscribeRequest is the JSON body for DELETE /api/push/subscribe.
type unsubscribeRequest struct {
Endpoint string `json:"endpoint"`
}
// Subscribe handles POST /api/push/subscribe.
// Registers a Web Push subscription for the authenticated user.
func (h *PushHandler) Subscribe(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
var req subscribeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, errorBody("invalid_request", "Invalid JSON body"))
return
}
if req.Endpoint == "" || req.KeyP256dh == "" || req.KeyAuth == "" {
writeJSON(w, http.StatusBadRequest, errorBody("validation_error", "endpoint, key_p256dh, and key_auth are required"))
return
}
userAgent := r.Header.Get("User-Agent")
if err := h.pushService.Subscribe(r.Context(), ownerID, req.Endpoint, req.KeyP256dh, req.KeyAuth, userAgent); err != nil {
h.logger.Error("push subscribe failed",
"user_id", ownerID,
"error", err,
)
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to register subscription"))
return
}
h.logger.Info("push subscription registered",
"user_id", ownerID,
)
writeJSON(w, http.StatusOK, map[string]string{"status": "subscribed"})
}
// Unsubscribe handles DELETE /api/push/subscribe.
// Removes a Web Push subscription for the authenticated user.
func (h *PushHandler) Unsubscribe(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
var req unsubscribeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, errorBody("invalid_request", "Invalid JSON body"))
return
}
if req.Endpoint == "" {
writeJSON(w, http.StatusBadRequest, errorBody("validation_error", "endpoint is required"))
return
}
if err := h.pushService.Unsubscribe(r.Context(), ownerID, req.Endpoint); err != nil {
h.logger.Error("push unsubscribe failed",
"error", err,
)
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to remove subscription"))
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "unsubscribed"})
}
// VAPIDKey handles GET /api/push/vapid-key.
// Returns the VAPID public key needed by clients to subscribe to push notifications.
func (h *PushHandler) VAPIDKey(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
"vapid_public_key": h.pushService.GetVAPIDPublicKey(),
})
}