Skip to content

fix(experiences): scope idempotency dedup to the author - #1803

Open
ionfwsrijan wants to merge 1 commit into
Canopus-Labs:mainfrom
ionfwsrijan:fix/1795-interview-experience-idempotency-scope
Open

fix(experiences): scope idempotency dedup to the author#1803
ionfwsrijan wants to merge 1 commit into
Canopus-Labs:mainfrom
ionfwsrijan:fix/1795-interview-experience-idempotency-scope

Conversation

@ionfwsrijan

@ionfwsrijan ionfwsrijan commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Problem

POST /api/interview-experiences deduplicates submissions by idempotencyKey alone, with no scope to the submitting user or client key. Anyone who re-uses a key gets the first submitter's full submission back (the unscoped findOne({ idempotencyKey }) plus the same flaw in the 11000 unique-index race branch). Because the model also enforces a global unique index on idempotencyKey, a different user re-using the same key can never persist their own fresh submission — their content is silently lost.

Fix

  • Scoped the dedup lookup to the author in both places: userId when authenticated, clientKey for anonymous submissions.
  • Replaced the global unique index on idempotencyKey with author-scoped partial unique indexes (userId + idempotencyKey and clientKey + idempotencyKey), so a different author re-using the same key gets their own new submission instead of someone else's data.

Files changed

  • backend/controllers/interviewExperienceController.js — author-scoped findOne filters in the pre-create check and the 11000 race branch.
  • backend/models/InterviewExperience.js — author-scoped partial unique indexes.
  • backend/tests/interviewExperienceController.unit.test.js — updated existing assertion and added coverage for authenticated scoping, cross-author key reuse, and the race branch.

Testing

  • Ran cd backend && npm test: all interview experience tests pass (16/16). The only failing tests in the full suite are the pre-existing jobCache.boundedKeys failures that also fail on clean origin/main and are unrelated to this change.

Closes #1795

Summary

  • Scope idempotency deduplication by author for POST /api/interview-experiences.
  • Match authenticated submissions by userId.
  • Match anonymous submissions by clientKey.
  • Apply author-scoped matching during initial lookup and duplicate-key race recovery.
  • Replace the global unique index with author-scoped partial unique indexes.
  • Add tests for authenticated scoping, cross-author key reuse, and race recovery.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Interview-experience idempotency now scopes duplicate detection to the authenticated user or anonymous client key. MongoDB indexes match these scopes. Unit tests cover key reuse and duplicate-key race recovery.

Changes

Interview idempotency scope

Layer / File(s) Summary
Author-scoped uniqueness indexes
backend/models/InterviewExperience.js
The model replaces the global unique index with partial compound indexes for userId and clientKey.
Author-scoped duplicate lookup
backend/controllers/interviewExperienceController.js
The controller selects the authenticated user ID or anonymous client key and applies that scope to both duplicate lookups.
Scoped deduplication validation
backend/tests/interviewExperienceController.unit.test.js
Tests cover authenticated deduplication, anonymous key reuse, client-key matching, and duplicate-key race recovery.

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

Possibly related PRs

Suggested reviewers: nyxsky404

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: scoping interview-experience idempotency deduplication by author.
Linked Issues check ✅ Passed The changes satisfy issue #1795 by scoping initial and race-recovery deduplication to userId or clientKey.
Out of Scope Changes check ✅ Passed The controller, model indexes, and tests directly support author-scoped idempotency deduplication and contain no unrelated changes.
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

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

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

Actionable comments posted: 2

🤖 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/interviewExperienceController.js`:
- Line 104: The duplicate-key recovery must reuse the validated, trimmed
idempotency key and author filter instead of the raw request value. In
backend/controllers/interviewExperienceController.js:104, retain those
normalized values from the initial validation and pass them to both lookup paths
in the duplicate-key branch. In
backend/tests/interviewExperienceController.unit.test.js:230-263, add a
whitespace-padded duplicate-key race case and assert both lookups use the
trimmed key.

In `@backend/models/InterviewExperience.js`:
- Around line 101-117: Make authenticated and anonymous ownership scopes
mutually exclusive: in backend/models/InterviewExperience.js lines 101-117,
restrict the anonymous clientKey/idempotencyKey unique index to documents with
userId null; in backend/controllers/interviewExperienceController.js lines
79-82, strip caller-supplied clientKey for authenticated requests, strip
caller-supplied userId for anonymous requests, and limit anonymous lookups to
ownerless records; in backend/tests/interviewExperienceController.unit.test.js
lines 207-228, add coverage for an authenticated submission followed by an
anonymous submission reusing the same clientKey and idempotencyKey.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ec2fbd5e-daeb-42a6-8885-7474b261dc88

📥 Commits

Reviewing files that changed from the base of the PR and between 7d3f898 and a6469ed.

📒 Files selected for processing (3)
  • backend/controllers/interviewExperienceController.js
  • backend/models/InterviewExperience.js
  • backend/tests/interviewExperienceController.unit.test.js

try {
const existing = await InterviewExperience.findOne({
idempotencyKey: req.body.idempotencyKey,
...buildAuthorFilter(req, req.body.clientKey),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the normalized idempotency key in duplicate-key recovery.

Line 57 stores the trimmed key, but line 104 queries with the raw request value. If two concurrent requests submit " submit-key-abc12345 ", the losing request cannot find the winning record and returns HTTP 500.

  • backend/controllers/interviewExperienceController.js#L104-L104: retain the validated, trimmed idempotency key and author filter for use in the duplicate-key branch.
  • backend/tests/interviewExperienceController.unit.test.js#L230-L263: add a duplicate-key race test with surrounding whitespace and assert that both lookups use the trimmed key.
🧰 Tools
🪛 ast-grep (0.45.1)

[error] 101-104: Untrusted HTTP request data (req.body / req.query / req.params) flows into a MongoDB/Mongoose query, enabling NoSQL injection — an attacker can supply objects like {"$gt":""} or {"$where":"..."} to bypass filters or run arbitrary JavaScript. Never pass raw request data as a query object or operator value; validate and cast each field to its expected primitive type (e.g. with a schema validator), or whitelist allowed operators before querying.
Context: InterviewExperience.findOne({
idempotencyKey: req.body.idempotencyKey,
...buildAuthorFilter(req, req.body.clientKey),
})
Note: [CWE-943] Improper Neutralization of Special Elements in Data Query Logic.

(nosql-injection-mongo-request-javascript)

📍 Affects 2 files
  • backend/controllers/interviewExperienceController.js#L104-L104 (this comment)
  • backend/tests/interviewExperienceController.unit.test.js#L230-L263
🤖 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/interviewExperienceController.js` at line 104, The
duplicate-key recovery must reuse the validated, trimmed idempotency key and
author filter instead of the raw request value. In
backend/controllers/interviewExperienceController.js:104, retain those
normalized values from the initial validation and pass them to both lookup paths
in the duplicate-key branch. In
backend/tests/interviewExperienceController.unit.test.js:230-263, add a
whitespace-padded duplicate-key race case and assert both lookups use the
trimmed key.

Comment on lines 101 to +117
interviewExperienceSchema.index(
{ idempotencyKey: 1 },
{ userId: 1, idempotencyKey: 1 },
{
unique: true,
partialFilterExpression: {
userId: { $type: "objectId" },
idempotencyKey: { $type: "string", $gt: "" },
},
},
);

interviewExperienceSchema.index(
{ clientKey: 1, idempotencyKey: 1 },
{
unique: true,
partialFilterExpression: {
clientKey: { $type: "string", $gt: "" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Make authenticated and anonymous ownership scopes mutually exclusive.

An authenticated request can include a valid clientKey. The controller preserves that value. The document then matches both partial indexes and an anonymous lookup with the same clientKey and idempotencyKey can return the authenticated submission. A second authenticated user can also fail the clientKey unique index despite a different userId.

  • backend/models/InterviewExperience.js#L101-L117: apply the anonymous unique index only to ownerless documents, such as documents where userId is null.
  • backend/controllers/interviewExperienceController.js#L79-L82: remove caller-supplied clientKey for authenticated submissions, remove caller-supplied userId for anonymous submissions, and constrain anonymous lookups to anonymous records.
  • backend/tests/interviewExperienceController.unit.test.js#L207-L228: add coverage for an authenticated submission with clientKey followed by an anonymous submission using that same clientKey and idempotencyKey.
📍 Affects 3 files
  • backend/models/InterviewExperience.js#L101-L117 (this comment)
  • backend/controllers/interviewExperienceController.js#L79-L82
  • backend/tests/interviewExperienceController.unit.test.js#L207-L228
🤖 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/models/InterviewExperience.js` around lines 101 - 117, Make
authenticated and anonymous ownership scopes mutually exclusive: in
backend/models/InterviewExperience.js lines 101-117, restrict the anonymous
clientKey/idempotencyKey unique index to documents with userId null; in
backend/controllers/interviewExperienceController.js lines 79-82, strip
caller-supplied clientKey for authenticated requests, strip caller-supplied
userId for anonymous requests, and limit anonymous lookups to ownerless records;
in backend/tests/interviewExperienceController.unit.test.js lines 207-228, add
coverage for an authenticated submission followed by an anonymous submission
reusing the same clientKey and idempotencyKey.

@github-actions github-actions Bot added the merge ready PR is mergeable and has no conflicts label Aug 11, 2026
@KaranUnique

Copy link
Copy Markdown
Contributor

@ionfwsrijan Address coderabbit suggestions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge ready PR is mergeable and has no conflicts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Interview experience idempotencyKey dedup ignores the author - any re-use of a key returns the first submitter's full submission

2 participants