* Add push notifications support * fix(notifications): address push notification review findings - Gate the capability endpoint's apple_push availability on the admin delivery toggle, matching web push: Available now means setup will actually deliver. - Reject direct admin writes to push_relay_deployment_id/api_key; the relay issues them as a pair during registration and a lone write desyncs them (and poisons the next rotation request). - Purge a device's registrations under other profiles when it re-registers, so a profile switch on a shared device stops the old profile's pushes (attempts cascade); adds a DB-backed test. - Extract the shared channelDispatcher core + retry sweep and rebuild the webhook/web push/Apple push dispatchers on it instead of keeping three copies of the worker-pool/retry loop. - Deduplicate relay URL validation (admin setting + register flow) and the push outbox attempt-building loops behind shared helpers. - Cap free-text decline reasons in notification display bodies. - Fix TestHandleApplePushDisplayDB expectations to match the shared display copy (test previously failed under SILO_TEST_DATABASE_URL). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(notifications): route push relay URL writes through registration only Direct writes to notifications.push_relay_url via the admin settings endpoint bypassed the relay registration flow, letting the stored URL drift out of sync with the deployment id / API key pair the relay minted for it. Reject the URL alongside the deployment id and API key in the settings handler; POST /admin/notifications/push/relay/register remains the only path that persists all three together. The admin UI's Relay URL field now edits local draft state and is applied by the Register/Rotate action instead of the settings save. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
76 lines
2.5 KiB
Go
76 lines
2.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
apimw "github.com/Silo-Server/silo-server/internal/api/middleware"
|
|
"github.com/Silo-Server/silo-server/internal/notifications"
|
|
)
|
|
|
|
type applePushRegisterRequest struct {
|
|
DeviceID string `json:"device_id"`
|
|
APNsToken string `json:"apns_token"`
|
|
APNsEnvironment string `json:"apns_environment"`
|
|
APNsTopic string `json:"apns_topic"`
|
|
PushMode string `json:"push_mode"`
|
|
}
|
|
|
|
type applePushRegisterResponse struct {
|
|
ID string `json:"id"`
|
|
ServerDeviceID string `json:"server_device_id"`
|
|
Enabled bool `json:"enabled"`
|
|
PushMode string `json:"push_mode"`
|
|
}
|
|
|
|
func (h *NotificationsHandler) pushDevices() *notifications.PushDeviceService {
|
|
if h == nil || h.system == nil {
|
|
return nil
|
|
}
|
|
return h.system.PushDevices
|
|
}
|
|
|
|
// HandleRegisterApplePushDevice handles POST /devices/push/apple.
|
|
func (h *NotificationsHandler) HandleRegisterApplePushDevice(w http.ResponseWriter, r *http.Request) {
|
|
service := h.pushDevices()
|
|
if service == nil || !service.Available() {
|
|
writeError(w, http.StatusServiceUnavailable, "unavailable", "Apple push registration is not available")
|
|
return
|
|
}
|
|
|
|
var req applePushRegisterRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
|
|
device, err := service.RegisterApple(r.Context(), apimw.GetUserID(r.Context()), apimw.GetProfileID(r.Context()), notifications.ApplePushRegistrationInput{
|
|
DeviceID: req.DeviceID,
|
|
APNsToken: req.APNsToken,
|
|
APNsEnvironment: req.APNsEnvironment,
|
|
APNsTopic: req.APNsTopic,
|
|
PushMode: req.PushMode,
|
|
})
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, notifications.ErrPushDeviceInvalid):
|
|
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
|
|
case errors.Is(err, notifications.ErrPushDeviceUnsupported):
|
|
writeError(w, http.StatusUnprocessableEntity, "unsupported_push_device", err.Error())
|
|
case errors.Is(err, notifications.ErrPushDeviceUnavailable):
|
|
writeError(w, http.StatusServiceUnavailable, "unavailable", "Apple push registration is not available")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to register Apple push device")
|
|
}
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, applePushRegisterResponse{
|
|
ID: device.ID,
|
|
ServerDeviceID: device.ServerDeviceID,
|
|
Enabled: device.Enabled,
|
|
PushMode: device.PushMode,
|
|
})
|
|
}
|