feat: notification enhancement

This commit is contained in:
Krispire Admin
2026-06-11 00:31:35 +05:30
parent cb62adf2de
commit 187bcd1442
11 changed files with 441 additions and 79 deletions
+29 -4
View File
@@ -1,6 +1,7 @@
package handler
import (
"encoding/base64"
"encoding/json"
"net/http"
"net/url"
@@ -19,6 +20,11 @@ const (
maxEndpointLen = 2048
maxKeyLen = 256
maxDevicesPerUser = 10
// RFC 8291 fixes the key sizes: p256dh is an uncompressed P-256 point,
// auth a 16-byte secret. Anything else is guaranteed to fail at send time.
p256dhLen = 65
authLen = 16
)
// validatePushEndpoint requires an https URL belonging to a known browser push
@@ -46,6 +52,21 @@ func validatePushEndpoint(endpoint string) bool {
return false
}
// validateSubscriptionKey checks that a client key is base64(url)-decodable
// to exactly `want` bytes — the same decoding webpush-go applies at send
// time, so anything accepted here is actually deliverable.
func validateSubscriptionKey(key string, want int) bool {
if l := len(key); l == 0 || l > maxKeyLen {
return false
}
trimmed := strings.TrimRight(key, "=")
b, err := base64.RawURLEncoding.DecodeString(trimmed)
if err != nil {
b, err = base64.RawStdEncoding.DecodeString(trimmed)
}
return err == nil && len(b) == want
}
// PushHandler serves Web Push subscription management. The VAPID public key
// is handed to the browser so it can subscribe; subscriptions are stored per
// user. All routes require auth.
@@ -98,8 +119,9 @@ func (h *PushHandler) Subscribe(w http.ResponseWriter, r *http.Request) {
response.Error(w, http.StatusBadRequest, "invalid push endpoint")
return
}
if len(req.Keys.P256dh) > maxKeyLen || len(req.Keys.Auth) > maxKeyLen {
response.Error(w, http.StatusBadRequest, "subscription keys too long")
if !validateSubscriptionKey(req.Keys.P256dh, p256dhLen) ||
!validateSubscriptionKey(req.Keys.Auth, authLen) {
response.Error(w, http.StatusBadRequest, "invalid subscription keys")
return
}
if err := h.repo.Upsert(r.Context(), uid, req.Endpoint, req.Keys.P256dh, req.Keys.Auth, r.UserAgent()); err != nil {
@@ -115,18 +137,21 @@ type unsubscribeReq struct {
Endpoint string `json:"endpoint"`
}
// Unsubscribe — POST /push/unsubscribe. Removes a subscription by endpoint.
// Unsubscribe — POST /push/unsubscribe. Removes one of the caller's own
// subscriptions by endpoint (scoped to the user so nobody can delete another
// account's subscription by guessing/replaying an endpoint).
func (h *PushHandler) Unsubscribe(w http.ResponseWriter, r *http.Request) {
if h.repo == nil {
response.NoContent(w)
return
}
uid := middleware.UserID(r.Context())
var req unsubscribeReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Endpoint) == "" {
response.Error(w, http.StatusBadRequest, "missing endpoint")
return
}
if err := h.repo.DeleteByEndpoint(r.Context(), req.Endpoint); err != nil {
if err := h.repo.DeleteByEndpointForUser(r.Context(), uid, req.Endpoint); err != nil {
response.Error(w, http.StatusInternalServerError, "could not remove subscription")
return
}