Skip to content

fix(notifications): honor category prefs before push delivery - #1943

Merged
durdana3105 merged 2 commits into
durdana3105:mainfrom
nyxsky404:fix/1900-notification-preference-delivery
Aug 10, 2026
Merged

fix(notifications): honor category prefs before push delivery#1943
durdana3105 merged 2 commits into
durdana3105:mainfrom
nyxsky404:fix/1900-notification-preference-delivery

Conversation

@nyxsky404

@nyxsky404 nyxsky404 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Push delivery was sending to every subscription for a user without reading notification_preferences. Settings also showed email switches with no email path behind them.

  • sendPushNotification and cron dispatchPushNotifications load prefs and skip when the category's inApp channel is disabled
  • Settings only exposes push (inApp) toggles now
  • opted-out cron rows get push_sent_at so they don't retry forever

Testing

  • npx vitest run --project backend tests/dispatchPushNotifications.test.js tests/notificationPreferences.test.js

Fixes #1900

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added notification preference controls for push notifications by category.
    • Notifications are automatically skipped when the relevant category is disabled.
    • Added documentation for authenticated file uploads and profile-photo uploads.
  • Bug Fixes

    • Improved notification preference handling, including defaults and category mapping.
    • Ensured skipped notifications are processed without attempting delivery.
    • Restricted profile-photo uploads to 2 MB and verified user-specific storage paths.
  • Documentation

    • Documented upload validation, supported destinations, file types, content checks, and rate limits.

Load notification_preferences for recipients and skip push when the
mapped category's inApp channel is off. Drop email toggles from
Settings since there is no email delivery path.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

@nyxsky404 is attempting to deploy a commit to the durdana3105's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds notification preference helpers and applies category-based push suppression to direct and scheduled delivery. Settings now expose push preferences only. Upload route documentation and profile-photo upload tests were updated for user-scoped storage paths and a 2 MB limit.

Changes

Notification preference enforcement

Layer / File(s) Summary
Preference mapping and eligibility contracts
backend/utils/notificationPreferences.js, backend/tests/notificationPreferences.test.js
Adds preference defaults, notification type mapping, category resolution, push eligibility checks, and tests for default and disabled settings.
Direct push preference checks
backend/controllers/notificationController.js
Direct push requests resolve notification categories and skip delivery when the recipient disables that category.
Scheduled dispatch preference checks
backend/controllers/cronController.js, backend/tests/dispatchPushNotifications.test.js
Scheduled dispatch loads recipient preferences, rolls back claims after preference-query errors, and marks disabled notifications as processed without sending.
Push-only notification settings
src/pages/Settings.tsx
Settings normalize stored preferences, remove email controls, and provide one push toggle for each category.

Upload documentation and test updates

Layer / File(s) Summary
Upload route documentation and validation tests
docs/api.md, backend/tests/uploadPhoto.test.js
Documents upload validation and updates profile-photo tests for user-scoped storage paths, storage calls, and the 2 MB size limit.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: type:bug

Suggested reviewers: mayurigade-hub, riddhima25bet10005-a11y

Sequence Diagram(s)

sequenceDiagram
  participant NotificationRequest
  participant NotificationController
  participant Profiles
  participant WebPush
  NotificationRequest->>NotificationController: submit type and category
  NotificationController->>Profiles: load recipient preferences
  Profiles-->>NotificationController: return notification preferences
  NotificationController->>NotificationController: resolve category and check inApp
  NotificationController->>WebPush: deliver allowed push
  NotificationController-->>NotificationRequest: return delivery or preference_disabled response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Upload test changes and upload API documentation are unrelated to the notification preference requirements in issue #1900. Move the upload test and documentation changes to a separate pull request or link them to an appropriate upload-related issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: category preferences now control push delivery.
Linked Issues check ✅ Passed The changes satisfy issue #1900 by applying category preferences to push delivery and removing unsupported email controls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Update profile-photo assertions for Supabase storage paths and the 2MB limit, and document /api/upload plus /api/users/upload-photo so docs completeness checks pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nyxsky404

Copy link
Copy Markdown
Contributor Author

@nyxsky404 The CI failures on this PR are fixed.

Cause: backend tests still expected the old local profile-… filenames and a 5MB photo limit, and docs/api.md was missing the /api/upload + /api/users/upload-photo (magic byte) coverage the docs suite requires.

Fix: aligned backend/tests/uploadPhoto.test.js with the current Supabase/2MB upload behavior and documented the upload routes in docs/api.md.

The required test check is green now. (Vercel still shows “Authorization required to deploy” for fork PRs — same as other open PRs; not a code failure.)

@durdana3105 Ready for review and merge when you are.

@nyxsky404 nyxsky404 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good: push dispatch and send-push now honor category inApp prefs before delivery; Settings UI matches the push-only path.

CI test is green after the upload test/docs alignment commit. LGTM on the feature change — needs a maintainer approval/merge (@durdana3105).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/controllers/cronController.js`:
- Around line 135-143: Update the notification state handling in the cron
controller: in the prefsError rollback path, inspect the result of the
notifications update that clears push_claimed_at and handle or report rollback
failures instead of ignoring them. In the opt-out path, check the push_sent_at
update result and ensure failures do not mark the notification as processed or
allow it to be reclaimed for repeated evaluation.

In `@backend/tests/uploadPhoto.test.js`:
- Around line 169-171: Update the upload test around storageUploadMock to assert
that its first call received a path containing TEST_USER_ID followed by the
expected filename or path structure. Keep the existing fileUrl and
upload-invocation assertions, but make the user-scoping verification depend
directly on the .upload() argument rather than the generated public URL.

In `@docs/api.md`:
- Line 165: Update the profile-photo validation bullet to claim only that the
image type is checked against the allowed MIME types; do not claim magic-byte
content matching unless the corresponding comparison is added in the user upload
route.

In `@src/pages/Settings.tsx`:
- Around line 154-157: Update the ToggleSwitch component to expose its checked
state with aria-pressed={checked}, and add category-specific accessible labels
at each Settings call site for messages/Push, sessions, and friend requests.
Ensure each label identifies the notification category and Push channel while
preserving the existing checked and onChange behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f38b810-148d-438e-b9ec-4cb3e06bec88

📥 Commits

Reviewing files that changed from the base of the PR and between 8c4d292 and f4542cc.

📒 Files selected for processing (8)
  • backend/controllers/cronController.js
  • backend/controllers/notificationController.js
  • backend/tests/dispatchPushNotifications.test.js
  • backend/tests/notificationPreferences.test.js
  • backend/tests/uploadPhoto.test.js
  • backend/utils/notificationPreferences.js
  • docs/api.md
  • src/pages/Settings.tsx

Comment on lines +135 to +143
if (prefsError) {
const notificationIds = notifications.map((n) => n.id);
if (notificationIds.length > 0) {
await supabase
.from("notifications")
.update({ push_claimed_at: null })
.in("id", notificationIds);
}
return res.status(500).json({ error: prefsError.message });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle failures when changing preference-related notification state.

If the profile query fails, check the claim rollback result. A rollback failure leaves claimed notifications unavailable until the claim TTL expires.

If the opt-out push_sent_at update fails, do not report the notification as processed. The row can otherwise be reclaimed and repeatedly evaluated.

Proposed fix
-        await supabase
+        const { error: rollbackError } = await supabase
           .from("notifications")
           .update({ push_claimed_at: null })
           .in("id", notificationIds);
+        if (rollbackError) {
+          return res.status(500).json({
+            error: `Preference fetch failed, and rollback failed: ${rollbackError.message}`,
+          });
+        }

-        await supabase
+        const { error: skipError } = await supabase
           .from("notifications")
           .update({ push_sent_at: new Date().toISOString() })
           .eq("id", notification.id);
+        if (skipError) throw skipError;

Also applies to: 165-168

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/cronController.js` around lines 135 - 143, Update the
notification state handling in the cron controller: in the prefsError rollback
path, inspect the result of the notifications update that clears push_claimed_at
and handle or report rollback failures instead of ignoring them. In the opt-out
path, check the push_sent_at update result and ensure failures do not mark the
notification as processed or allow it to be reclaimed for repeated evaluation.

Comment on lines +169 to +171
// Supabase storage path is scoped to the authenticated user's ID
expect(res.body.fileUrl).toContain(`${TEST_USER_ID}/`);
expect(storageUploadMock).toHaveBeenCalled();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Assert the storage path directly.

toHaveBeenCalled() proves only that an upload occurred. The URL assertion proves user scoping only if the getPublicUrl mock derives the URL from the upload path. Assert the first .upload() argument directly.

Suggested assertion
     expect(res.body.fileUrl).toContain(`${TEST_USER_ID}/`);
     expect(storageUploadMock).toHaveBeenCalled();
+    expect(storageUploadMock.mock.calls[0][0]).toMatch(
+      new RegExp(`^${TEST_USER_ID}/`),
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Supabase storage path is scoped to the authenticated user's ID
expect(res.body.fileUrl).toContain(`${TEST_USER_ID}/`);
expect(storageUploadMock).toHaveBeenCalled();
// Supabase storage path is scoped to the authenticated user's ID
expect(res.body.fileUrl).toContain(`${TEST_USER_ID}/`);
expect(storageUploadMock).toHaveBeenCalled();
expect(storageUploadMock.mock.calls[0][0]).toMatch(
new RegExp(`^${TEST_USER_ID}/`),
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/uploadPhoto.test.js` around lines 169 - 171, Update the upload
test around storageUploadMock to assert that its first call received a path
containing TEST_USER_ID followed by the expected filename or path structure.
Keep the existing fileUrl and upload-invocation assertions, but make the
user-scoping verification depend directly on the .upload() argument rather than
the generated public URL.

Comment thread docs/api.md
**Validation**:
- 2MB size limit
- Strict image MIME allow-list
- Magic byte verification that file content matches the declared image type

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Correct the profile-photo MIME validation claim.

backend/routes/users.js checks the declared MIME and detected MIME against the allow-list separately. It does not verify that both values match. Therefore, an allowed JPEG declaration with PNG content is accepted.

Update this bullet to describe verification of an allowed image type, or add an explicit comparison in the route.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api.md` at line 165, Update the profile-photo validation bullet to claim
only that the image type is checked against the allowed MIME types; do not claim
magic-byte content matching unless the corresponding comparison is added in the
user upload route.

Comment thread src/pages/Settings.tsx
Comment on lines 154 to 157
<ToggleSwitch
checked={preferences.messages.inApp}
onChange={() => handleToggle("messages", "inApp")}
onChange={() => handleToggle("messages")}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Give each Push toggle an accessible name and state.

ToggleSwitch renders a button without text, aria-label, or aria-pressed. Screen-reader users cannot identify the notification category or determine whether Push is enabled.

Pass a category-specific label at each call site. Add aria-pressed={checked} to ToggleSwitch.

Proposed fix
 <ToggleSwitch
   checked={preferences.messages.inApp}
   onChange={() => handleToggle("messages")}
+  label="Push notifications for new messages"
 />

-const ToggleSwitch = ({ checked, onChange }: { checked: boolean; onChange: () => void }) => {
+const ToggleSwitch = ({ checked, onChange, label }: {
+  checked: boolean;
+  onChange: () => void;
+  label: string;
+}) => {
   return (
     <button
       type="button"
       onClick={onChange}
+      aria-label={label}
+      aria-pressed={checked}

Apply equivalent labels for sessions and friend requests.

Also applies to: 167-170, 180-182

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/Settings.tsx` around lines 154 - 157, Update the ToggleSwitch
component to expose its checked state with aria-pressed={checked}, and add
category-specific accessible labels at each Settings call site for
messages/Push, sessions, and friend requests. Ensure each label identifies the
notification category and Push channel while preserving the existing checked and
onChange behavior.

@nyxsky404

Copy link
Copy Markdown
Contributor Author

Hi @durdana3105 — gentle nudge.

This PR is ready for review from my side. I've rebased/kept it current where possible. If CI is red due to unrelated upstream/main issues or deploy previews, the code change itself should still be reviewable.

Please review and merge when you can — and add gssoc:approved (+ difficulty/type) if appropriate so it can count for GSSoC.

Thank you!

@durdana3105
durdana3105 merged commit e288bc7 into durdana3105:main Aug 10, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Notification delivery ignores saved category preferences

2 participants