summaryrefslogtreecommitdiff
path: root/internal/api/push.go
blob: dde54413e70a9e146f0f1cf26abb84d5a9915f75 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package api

import (
	"encoding/json"
	"net/http"

	"github.com/google/uuid"
	"github.com/thepeterstone/claudomator/internal/storage"
	webui "github.com/thepeterstone/claudomator/web"
)

// pushSubscriptionStore is the minimal interface needed by push handlers.
type pushSubscriptionStore interface {
	SavePushSubscription(sub storage.PushSubscription) error
	DeletePushSubscription(endpoint string) error
	ListPushSubscriptions() ([]storage.PushSubscription, error)
}

// SetVAPIDConfig configures VAPID keys and email for web push notifications.
func (s *Server) SetVAPIDConfig(pub, priv, email string) {
	s.vapidPublicKey = pub
	s.vapidPrivateKey = priv
	s.vapidEmail = email
}

// SetPushStore configures the push subscription store.
func (s *Server) SetPushStore(store pushSubscriptionStore) {
	s.pushStore = store
}

// SetDropsDir configures the file drop directory.
func (s *Server) SetDropsDir(dir string) {
	s.dropsDir = dir
}

// handleGetVAPIDKey returns the VAPID public key for client-side push subscription.
func (s *Server) handleGetVAPIDKey(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]string{"public_key": s.vapidPublicKey})
}

// handleServiceWorker serves sw.js with a Service-Worker-Allowed: / header so
// the SW can control the full origin even though it is registered from /api/push/sw.js.
func (s *Server) handleServiceWorker(w http.ResponseWriter, r *http.Request) {
	data, err := webui.Files.ReadFile("sw.js")
	if err != nil {
		http.Error(w, "service worker not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/javascript")
	w.Header().Set("Service-Worker-Allowed", "/")
	w.WriteHeader(http.StatusOK)
	w.Write(data)
}

// handlePushSubscribe saves a new push subscription.
func (s *Server) handlePushSubscribe(w http.ResponseWriter, r *http.Request) {
	var input struct {
		Endpoint string `json:"endpoint"`
		Keys     struct {
			P256DH string `json:"p256dh"`
			Auth   string `json:"auth"`
		} `json:"keys"`
	}
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
		return
	}
	if input.Endpoint == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "endpoint is required"})
		return
	}
	if input.Keys.P256DH == "" || input.Keys.Auth == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "keys.p256dh and keys.auth are required"})
		return
	}

	sub := storage.PushSubscription{
		ID:        uuid.New().String(),
		Endpoint:  input.Endpoint,
		P256DHKey: input.Keys.P256DH,
		AuthKey:   input.Keys.Auth,
	}

	store := s.pushStore
	if store == nil {
		store = s.store
	}

	if err := store.SavePushSubscription(sub); err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusCreated, map[string]string{"id": sub.ID})
}

// handlePushUnsubscribe deletes a push subscription.
func (s *Server) handlePushUnsubscribe(w http.ResponseWriter, r *http.Request) {
	var input struct {
		Endpoint string `json:"endpoint"`
	}
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
		return
	}
	if input.Endpoint == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "endpoint is required"})
		return
	}

	store := s.pushStore
	if store == nil {
		store = s.store
	}

	if err := store.DeletePushSubscription(input.Endpoint); err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	w.WriteHeader(http.StatusNoContent)
}