GET/PUT /api/v1/settings/notifications read and replace the event x
{push, chat} matrix for the five events and the reminder time. The PUT
validates every event exactly once and an HH:MM time, refuses unknown
events and fields, and writes the preferences and the household
settings' reminder_time (the one copy) in one transaction, publishing
the new settings to the scheduler as a household settings change does.
/settings/notifications renders mock 1n: the matrix with Toggle
switches, including the next-day "Keep it in rotation?" Rotation check
(D-021); the reminder time edited in a sheet; the push permission row
using pushStatus/subscribePush/unsubscribePush; the iOS InstallHint;
the Google Chat webhook card with the masked URL, "Connected · {space}"
and Send test showing success or the HTTP status; and the preview
bubble. The desktop 2e Notifications & AI tab shows the matrix and
webhook cards in its left column. The settings home row now reads Web
Push on this device and the Google Chat space, and the Household form
takes a reminder time saved elsewhere without undoing it.
Maestro task 308a460d-b22c-4179-acba-d5ea0968ccd5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
3.2 KiB
Go
89 lines
3.2 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/auth"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
|
|
)
|
|
|
|
// notificationsBody is the PUT /settings/notifications body: every event's
|
|
// channels and the reminder time, all required. Unknown fields and null
|
|
// values are refused.
|
|
type notificationsBody struct {
|
|
Prefs *[]notificationPrefBody `json:"prefs"`
|
|
ReminderTime *string `json:"reminder_time"`
|
|
}
|
|
|
|
type notificationPrefBody struct {
|
|
Event *string `json:"event"`
|
|
Push *bool `json:"push"`
|
|
Chat *bool `json:"chat"`
|
|
}
|
|
|
|
// settings is the body as household notification settings, or why it is
|
|
// incomplete.
|
|
func (b notificationsBody) settings() (household.NotificationSettings, error) {
|
|
if b.Prefs == nil || b.ReminderTime == nil {
|
|
return household.NotificationSettings{}, errors.New("prefs and reminder_time are required")
|
|
}
|
|
prefs := make([]household.NotificationPref, len(*b.Prefs))
|
|
for i, p := range *b.Prefs {
|
|
if p.Event == nil || p.Push == nil || p.Chat == nil {
|
|
return household.NotificationSettings{}, fmt.Errorf("prefs[%d]: event, push and chat are required", i)
|
|
}
|
|
prefs[i] = household.NotificationPref{Event: *p.Event, Push: *p.Push, Chat: *p.Chat}
|
|
}
|
|
return household.NotificationSettings{Prefs: prefs, ReminderTime: *b.ReminderTime}, nil
|
|
}
|
|
|
|
func getNotificationSettings(logger *slog.Logger, svc *household.SettingsService) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
settings, err := svc.Notifications(r.Context())
|
|
if err != nil {
|
|
logger.ErrorContext(r.Context(), "get notification settings", "error", err)
|
|
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, settings)
|
|
}
|
|
}
|
|
|
|
// putNotificationSettings replaces the notification preferences and the
|
|
// reminder time, recording the session's actor subject as updated_by.
|
|
func putNotificationSettings(logger *slog.Logger, svc *household.SettingsService) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := auth.ActorFrom(r.Context())
|
|
if !ok {
|
|
logger.ErrorContext(r.Context(), "PUT /api/v1/settings/notifications without a session")
|
|
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
|
return
|
|
}
|
|
body, err := decodePatch[notificationsBody](http.MaxBytesReader(w, r.Body, maxSettingsBody))
|
|
if err != nil {
|
|
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
want, err := body.settings()
|
|
if err != nil {
|
|
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
settings, err := svc.UpdateNotifications(r.Context(), want, actor.Subject)
|
|
var invalid *household.ValidationError
|
|
if errors.As(err, &invalid) {
|
|
writeError(w, r, logger, http.StatusBadRequest, "invalid_settings", invalid.Error())
|
|
return
|
|
}
|
|
if err != nil {
|
|
logger.ErrorContext(r.Context(), "update notification settings", "error", err)
|
|
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, settings)
|
|
}
|
|
}
|