Skip to content

fix(presence): require participation + serialize first-editor check (#197) - #202

Merged
henrique221 merged 5 commits into
mainfrom
fix/197-presence-route-authz
Jun 30, 2026
Merged

fix(presence): require participation + serialize first-editor check (#197)#202
henrique221 merged 5 commits into
mainfrom
fix/197-presence-route-authz

Conversation

@henrique221

@henrique221 henrique221 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Two #197 presence fixes. (The remaining #197 item — QA that the 2nd-editor warning fires for a real 2nd user — needs a running 2-user environment.)

1. Authz: require chapter-assignment participation (IDOR fix)

The POST/DELETE /chapter-assignments/{chapterAssignmentId}/presence routes were gated only by authenticateUser + requirePermission(CONTENT_UPDATE), with no record-level check — so any translator could register presence on, and read concurrent-editor info for, any chapterAssignmentId, leaking who's editing what across assignments.

  • Add requireChapterAssignmentAccess(CHAPTER_ASSIGNMENT_ACTIONS.IS_PARTICIPANT) to both routes, matching the sibling editor-state routes (404 to non-participants).
  • Document the resulting 403/404 responses.

2. Serialize the first-editor check (race fix)

upsertAndQueryFirstEditor pruned, upserted, and selected the first editor in one READ COMMITTED transaction with no per-chapter serialization. Two near-simultaneous first registrations could each miss the other's uncommitted row and both conclude they're first → the 2nd user briefly saw no warning (self-correcting on the next 30s heartbeat). Only the both-fresh case was affected; startedAt is fixed per user, so the common "one already present" case was already correct.

  • Take a transaction-scoped pg_advisory_xact_lock(chapterAssignmentId) at the top of the transaction so same-chapter registrations run one at a time. (FOR UPDATE wouldn't fix the both-fresh case — there are no existing rows to lock — but the advisory lock does.) Uncontended cost is negligible.

The issue deferred #2 to "if QA shows flakiness." It's a real but rare, self-correcting edge case — happy to drop this commit and confirm via QA first if you'd prefer; the authz fix (#1) stands on its own.

Test plan

  • npm run typecheck
  • npm run lint
  • npm run test — 80/80 pass
  • Manual: a non-participant translator gets 404 from the /presence routes
  • Manual (needs 2-user env): 2nd editor on the same chapter sees the warning, even on simultaneous open

Summary by CodeRabbit

  • Security Updates
    • Strengthened access control for chapter assignment presence operations. Users can now only register or remove presence for chapter assignments where they are designated participants.
  • Bug Fixes
    • Improved reliability of concurrent presence registrations to ensure consistent “first editor” selection under near-simultaneous requests.
  • Documentation
    • Updated API documentation for presence registration and removal to explicitly reflect additional possible error responses.

…outes

The POST/DELETE /chapter-assignments/{id}/presence routes were gated only
by authenticateUser + requirePermission(CONTENT_UPDATE), so any translator
could register presence on — and read concurrent-editor info for — any
chapterAssignmentId, leaking editor identity across assignments.

Add requireChapterAssignmentAccess(IS_PARTICIPANT) to both routes, matching
the editor-state routes, and document the resulting 403/404 responses (the
403 from requirePermission was previously undocumented too).

Part of #197 (the presence-route authz item; the 2nd-editor-warning QA and
first-editor-race items need a running 2-user environment).
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The presence routes add requireChapterAssignmentAccess(CHAPTER_ASSIGNMENT_ACTIONS.IS_PARTICIPANT) to POST and DELETE chapter-assignment presence actions, and the repository adds a transaction-scoped advisory lock during presence upserts.

Changes

Chapter Assignment Presence

Layer / File(s) Summary
Route authorization and response docs
src/domains/chapter-assignments/presence/chapter-assignments-presence.route.ts
Adds chapter-assignment-scoped access checks to the POST and DELETE presence routes, and reorganizes their OpenAPI response blocks to document additional error statuses.
Advisory lock in presence upsert
src/domains/chapter-assignments/presence/chapter-assignments-presence.repository.ts
Adds sql import support, executes pg_advisory_xact_lock(chapterAssignmentId::bigint) in upsertAndQueryFirstEditor, and adds a secondary userId sort key when selecting the first editor.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4
✅ 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 two main changes: participation authorization and serialized first-editor selection.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/197-presence-route-authz

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.

@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: 1

🤖 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
`@src/domains/chapter-assignments/presence/chapter-assignments-presence.route.ts`:
- Around line 33-52: The responses object in the route is missing documentation
for the 400 BAD_REQUEST response that the requireChapterAssignmentAccess
middleware can return when the chapter assignment ID parameter is invalid or
missing. Add a new entry to the responses object using
HttpStatusCodes.BAD_REQUEST as the key, with jsonContent wrapping
createMessageObjectSchema to document the error response with an appropriate
message describing the invalid parameter condition.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ba9b7977-e5aa-4dba-bbcb-2382c00d939c

📥 Commits

Reviewing files that changed from the base of the PR and between bff9972 and 2bd87c0.

📒 Files selected for processing (1)
  • src/domains/chapter-assignments/presence/chapter-assignments-presence.route.ts

…ry lock

upsertAndQueryFirstEditor pruned, upserted, and selected the first editor
in one READ COMMITTED transaction without serializing per chapter. Two
near-simultaneous first registrations could each miss the other's
uncommitted row and both conclude they are first, so the second user
briefly saw no concurrent-editor warning (self-correcting on the next
heartbeat). startedAt is fixed per user, so the common 'one already
present' case was already correct — only the both-fresh race was affected.

Take a transaction-scoped pg_advisory_xact_lock keyed on chapterAssignmentId
at the top of the transaction so registrations for the same chapter run
strictly one at a time. Uncontended acquisition is negligible; contention
only occurs on simultaneous same-chapter opens.

Part of #197 (the first-editor-race item; the 2nd-editor-warning QA still
needs a running 2-user environment to confirm).
@henrique221 henrique221 changed the title fix(presence): require chapter-assignment participation on presence routes fix(presence): require participation + serialize first-editor check (#197) Jun 25, 2026

@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: 1

🤖 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
`@src/domains/chapter-assignments/presence/chapter-assignments-presence.repository.ts`:
- Around line 19-24: The first-editor lookup in
chapter-assignments-presence.repository.ts is non-deterministic when multiple
rows share the same startedAt value. Update the ordering used by the
first-editor selection query so it includes a stable secondary sort key, such as
userId, alongside startedAt in the relevant repository method (the one that
reads active_chapter_editors and determines firstEditor). This will make tie
cases deterministic without changing the existing advisory-lock flow or insert
logic.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2fed6d46-bb7b-443f-9cee-b9bcef851271

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd87c0 and 8972c3f.

📒 Files selected for processing (1)
  • src/domains/chapter-assignments/presence/chapter-assignments-presence.repository.ts

@henrique221 henrique221 added the bug Something isn't working label Jun 25, 2026
@henrique221 henrique221 self-assigned this Jun 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses two items from #197 in the chapter-assignment presence feature: tightening record-level authorization on the presence routes to prevent IDOR-style access, and eliminating a rare race in determining the “first editor” during near-simultaneous presence registrations.

Changes:

  • Added requireChapterAssignmentAccess(IS_PARTICIPANT) to the presence POST/DELETE routes to ensure only chapter participants can register/remove presence and observe concurrent-editor info.
  • Documented the new 403/404 responses in the OpenAPI route definitions for presence endpoints.
  • Serialized “first editor” determination by taking a transaction-scoped PostgreSQL advisory lock per chapterAssignmentId inside upsertAndQueryFirstEditor.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/domains/chapter-assignments/presence/chapter-assignments-presence.route.ts Adds participant-level access control middleware and documents 403/404 outcomes for presence routes.
src/domains/chapter-assignments/presence/chapter-assignments-presence.repository.ts Adds a per-chapter transaction-scoped advisory lock to prevent race conditions in first-editor selection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…deterministic

Add BAD_REQUEST to both presence routes' OpenAPI responses since
requireChapterAssignmentAccess runs before the zod validators in
@hono/zod-openapi and can return 400 for an invalid chapterAssignmentId.
Add userId as a stable secondary sort key to the first-editor lookup so
startedAt ties resolve deterministically, complementing the advisory lock.

Refs: #202
@henrique221
henrique221 enabled auto-merge (squash) June 29, 2026 20:27

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/domains/chapter-assignments/presence/chapter-assignments-presence.route.ts (1)

36-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mirror the positive-integer guard in src/domains/chapter-assignments/chapter-assignment-auth.middleware.ts.

createRoute middleware runs before the request.params schema here, so -1 and 1.5 can reach the lookup path and return 404 instead of the documented 400 on both presence routes.

Proposed fix in `chapter-assignment-auth.middleware.ts`
 const chapterAssignmentId = Number(c.req.param(paramName));
-if (!chapterAssignmentId || Number.isNaN(chapterAssignmentId)) {
-  return c.json({ message: 'Missing chapter assignment ID' }, HttpStatusCodes.BAD_REQUEST);
+if (!Number.isInteger(chapterAssignmentId) || chapterAssignmentId <= 0) {
+  return c.json({ message: 'Invalid chapter assignment ID' }, HttpStatusCodes.BAD_REQUEST);
 }
🤖 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/domains/chapter-assignments/presence/chapter-assignments-presence.route.ts`
around lines 36 - 44, The presence routes are allowing non-positive or
non-integer chapter assignment IDs to reach the lookup path because the
middleware in chapter-assignment-auth.middleware.ts does not mirror the
positive-integer guard before createRoute request param validation. Update the
chapter assignment ID check in chapter-assignment-auth.middleware.ts so it
rejects values like -1 and 1.5 early with the documented 400 behavior, and make
sure the same guard is applied consistently through the route flow used by the
chapter-assignments-presence.route.ts endpoints.
🤖 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.

Outside diff comments:
In
`@src/domains/chapter-assignments/presence/chapter-assignments-presence.route.ts`:
- Around line 36-44: The presence routes are allowing non-positive or
non-integer chapter assignment IDs to reach the lookup path because the
middleware in chapter-assignment-auth.middleware.ts does not mirror the
positive-integer guard before createRoute request param validation. Update the
chapter assignment ID check in chapter-assignment-auth.middleware.ts so it
rejects values like -1 and 1.5 early with the documented 400 behavior, and make
sure the same guard is applied consistently through the route flow used by the
chapter-assignments-presence.route.ts endpoints.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 12343fb4-e466-4fbc-9ebc-2e2c8072dd0c

📥 Commits

Reviewing files that changed from the base of the PR and between 8972c3f and 8ef7b91.

📒 Files selected for processing (2)
  • src/domains/chapter-assignments/presence/chapter-assignments-presence.repository.ts
  • src/domains/chapter-assignments/presence/chapter-assignments-presence.route.ts

@henrique221
henrique221 merged commit f2ca17e into main Jun 30, 2026
2 checks passed
@github-actions
github-actions Bot deleted the fix/197-presence-route-authz branch June 30, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants