// backend/notification_preferences.go package main import ( "context" "errors" "net/http" "strings" "github.com/jackc/pgx/v5" ) type notificationPreferencesResponse struct { SoundEnabled bool `json:"soundEnabled"` SoundName string `json:"soundName"` EmailEnabled bool `json:"emailEnabled"` DeviceUpdates bool `json:"deviceUpdates"` DeviceJournals bool `json:"deviceJournals"` ChatMessages bool `json:"chatMessages"` ChannelMessages bool `json:"channelMessages"` DesktopEnabled bool `json:"desktopEnabled"` PushChatMessages bool `json:"pushChatMessages"` PushChannelMessages bool `json:"pushChannelMessages"` } func defaultNotificationPreferences() notificationPreferencesResponse { return notificationPreferencesResponse{ SoundEnabled: false, SoundName: "default", EmailEnabled: false, DeviceUpdates: true, DeviceJournals: true, ChatMessages: true, ChannelMessages: true, DesktopEnabled: false, PushChatMessages: true, PushChannelMessages: true, } } // shouldPushNotification entscheidet anhand der granularen Push-Einstellungen, // ob für den jeweiligen Benachrichtigungstyp ein Web-Push gesendet werden soll. // Es deckt aktuell die Nachrichtentypen ab, für die der Web-Push-Payload korrekt // aufgebaut werden kann (Chat- und Channel-Nachrichten). func shouldPushNotification( preferences notificationPreferencesResponse, notificationType string, ) bool { switch notificationType { case "channel.message": return preferences.PushChannelMessages case "chat.message": return preferences.PushChatMessages default: return false } } func normalizeNotificationSoundName(value string) string { value = strings.TrimSpace(strings.ToLower(value)) switch value { case "default", "chime", "soft", "alert", "success": return value default: return "default" } } func (s *Server) handleGetNotificationPreferences(w http.ResponseWriter, r *http.Request) { user, ok := userFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "Unauthorized") return } settings, err := s.getNotificationPreferences(r.Context(), user.ID) if errors.Is(err, pgx.ErrNoRows) { writeJSON(w, http.StatusOK, map[string]any{ "settings": defaultNotificationPreferences(), }) return } if err != nil { writeError(w, http.StatusInternalServerError, "Benachrichtigungseinstellungen konnten nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "settings": settings, }) } func (s *Server) handleUpdateNotificationPreferences(w http.ResponseWriter, r *http.Request) { user, ok := userFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "Unauthorized") return } var input notificationPreferencesResponse if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } input.SoundName = normalizeNotificationSoundName(input.SoundName) _, err := s.db.Exec( r.Context(), ` INSERT INTO notification_preferences ( user_id, sound_enabled, sound_name, email_enabled, device_updates, device_journals, chat_messages, channel_messages, desktop_enabled, push_chat_messages, push_channel_messages ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (user_id) DO UPDATE SET sound_enabled = EXCLUDED.sound_enabled, sound_name = EXCLUDED.sound_name, email_enabled = EXCLUDED.email_enabled, device_updates = EXCLUDED.device_updates, device_journals = EXCLUDED.device_journals, chat_messages = EXCLUDED.chat_messages, channel_messages = EXCLUDED.channel_messages, desktop_enabled = EXCLUDED.desktop_enabled, push_chat_messages = EXCLUDED.push_chat_messages, push_channel_messages = EXCLUDED.push_channel_messages, updated_at = now() `, user.ID, input.SoundEnabled, input.SoundName, input.EmailEnabled, input.DeviceUpdates, input.DeviceJournals, input.ChatMessages, input.ChannelMessages, input.DesktopEnabled, input.PushChatMessages, input.PushChannelMessages, ) if err != nil { writeError(w, http.StatusInternalServerError, "Benachrichtigungseinstellungen konnten nicht gespeichert werden") return } settings, err := s.getNotificationPreferences(r.Context(), user.ID) if err != nil { writeError(w, http.StatusInternalServerError, "Benachrichtigungseinstellungen wurden gespeichert, konnten aber nicht neu geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "settings": settings, }) } func (s *Server) getNotificationPreferences(ctx context.Context, userID string) (notificationPreferencesResponse, error) { settings := defaultNotificationPreferences() err := s.db.QueryRow( ctx, ` SELECT sound_enabled, COALESCE(sound_name, 'default'), email_enabled, device_updates, device_journals, chat_messages, channel_messages, desktop_enabled, push_chat_messages, push_channel_messages FROM notification_preferences WHERE user_id = $1 LIMIT 1 `, userID, ).Scan( &settings.SoundEnabled, &settings.SoundName, &settings.EmailEnabled, &settings.DeviceUpdates, &settings.DeviceJournals, &settings.ChatMessages, &settings.ChannelMessages, &settings.DesktopEnabled, &settings.PushChatMessages, &settings.PushChannelMessages, ) settings.SoundName = normalizeNotificationSoundName(settings.SoundName) return settings, err } func (s *Server) getNotificationPreferencesOrDefault(ctx context.Context, userID string) (notificationPreferencesResponse, error) { settings, err := s.getNotificationPreferences(ctx, userID) if errors.Is(err, pgx.ErrNoRows) { return defaultNotificationPreferences(), nil } return settings, err }