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)
}
|