Skip to content

Notifications: explicit mark-read, drop side effect from GET - #226

Merged
martsokha merged 1 commit into
mainfrom
fix/notifications-mark-read
Aug 12, 2026
Merged

Notifications: explicit mark-read, drop side effect from GET#226
martsokha merged 1 commit into
mainfrom
fix/notifications-mark-read

Conversation

@martsokha

Copy link
Copy Markdown
Member

Problem

GET /notifications/ marked all notifications as read as a side effect (list_notifications called mark_all_...as_read after listing). A GET must be safe/idempotent — this one mutated state:

  • reading the list cleared the unread badge; any cache / prefetch / retry / preview-bot did too;
  • no way to view notifications without clearing them (polling a dropdown wiped the badge each poll);
  • the GET /unread/ count raced against GET /notifications/ zeroing it.

Change

Make the list a pure read and add explicit write endpoints:

Method Path Behavior
GET /notifications/ list, read-only
GET /notifications/unread/ { unreadCount } (unchanged)
POST /notifications/read/ mark all read → { markedRead: n }
POST /notifications/{notificationId}/read/ mark one read → 204 (404 if not the caller's / missing)

Security

  • Removed the unused, un-scoped find_account_notification_by_id (looked up by id only — a latent IDOR if ever used for per-item writes).
  • The new mark_account_notification_as_read(account_id, id) scopes by account_id in the WHERE clause: another account's id (or a missing one) updates 0 rows → 404, with no read-then-write race and no way to probe whether an id exists.

Behavior change

Clients that relied on "GET clears unread" must now call POST /notifications/read/ explicitly. No DB migration (all columns/queries existed; added one query).

Testing

Full gate green (check / fmt / clippy / doc / test).

🤖 Generated with Claude Code

GET /notifications/ marked all notifications read as a side effect, so a
GET mutated state — reading the list cleared the unread badge, and any
cache/prefetch/retry did too, with no way to just view without clearing.

Make the read pure and add explicit write endpoints:

- GET  /notifications/                        list, read-only
- POST /notifications/read/                   mark all read -> { markedRead }
- POST /notifications/{notificationId}/read/  mark one read -> 204 (404 if not
                                              the caller's / missing)

Security: remove the unused, un-scoped find_account_notification_by_id
(latent IDOR) and add mark_account_notification_as_read(account_id, id)
that scopes by account_id in the WHERE clause, so another account's id
updates no rows and returns 404 without revealing whether it exists.

Behavior change: clients that relied on "GET clears unread" must now call
POST /notifications/read/. No migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@martsokha martsokha added bug something isn't working as intended feat request for or implementation of a new feature server API handlers, middleware, auth postgres ORM, models, queries, migrations security security fixes and vulnerability patches labels Aug 12, 2026
@martsokha martsokha self-assigned this Aug 12, 2026
@martsokha
martsokha merged commit d48c612 into main Aug 12, 2026
9 checks passed
@martsokha
martsokha deleted the fix/notifications-mark-read branch August 12, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug something isn't working as intended feat request for or implementation of a new feature postgres ORM, models, queries, migrations security security fixes and vulnerability patches server API handlers, middleware, auth

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant