Skip to content

fix(security): harden OTP generation, sanitize(), and verifyApiKey project select#106

Open
prathamtagad wants to merge 5 commits intogeturbackend:mainfrom
prathamtagad:fix/security-otp-sanitize-siteurl
Open

fix(security): harden OTP generation, sanitize(), and verifyApiKey project select#106
prathamtagad wants to merge 5 commits intogeturbackend:mainfrom
prathamtagad:fix/security-otp-sanitize-siteurl

Conversation

@prathamtagad
Copy link
Copy Markdown

@prathamtagad prathamtagad commented Apr 14, 2026

Summary

Fixes three security and functional issues found in the public-api.

1. OTP generated with Math.random() — Cryptographically Weak

Files: apps/public-api/src/controllers/userAuth.controller.js
Lines: 970, 995, 1306, 1344

Math.random() is not a CSPRNG and its output is predictable. Replaced
all four OTP generation sites with crypto.randomInt(100000, 1000000),
which uses Node's secure random source. This matches the pattern already
in use in apps/dashboard-api/src/controllers/auth.controller.js.

2. sanitize() only strips top-level $-prefixed keys — NoSQL Injection Risk

File: packages/common/src/utils/input.validation.js

The previous implementation only removed $-operator keys at the top
level. A payload like { "profile": { "$gt": "" } } would pass through
unchanged, enabling NoSQL injection via nested update/query paths.

The function is now recursive: it walks plain objects and arrays,
stripping $-prefixed keys at every depth.

3. Missing siteUrl in verifyApiKey select — Social Auth Callback Breaks

File: apps/public-api/src/middlewares/verifyApiKey.js

siteUrl and authProviders were not included in the Mongoose
.select() projection. As a result, the project object cached and
forwarded to downstream controllers was missing these fields.
getFrontendCallbackBaseUrl() would always fall back to localhost:5173,
breaking social auth redirects in production.

Both fields are now included in the select clause.

Testing

  • All 90 existing public-api tests pass (npm test -- --runInBand).

Summary by CodeRabbit

  • Bug Fixes

    • Fixed missing HTTP 404 status codes in error responses
    • Enhanced graceful shutdown to properly close Redis connections
    • Improved debug logging cleanup
  • New Features

    • Added role-based security checks and confirmation requirements for destructive file operations
    • Enhanced OTP generation with stronger cryptographic methods
  • Improvements

    • Standardized API response formats with consistent success messages and data structures
    • Improved input validation with recursive sanitization for nested data structures

…lect

- Replace Math.random() with crypto.randomInt() for OTP generation in
  userAuth.controller.js (lines 970, 995, 1306, 1344). Math.random() is
  not cryptographically secure; crypto.randomInt uses Node's CSPRNG,
  consistent with how dashboard-api generates OTPs in auth.controller.js.

- Make sanitize() in input.validation.js recursive so nested himBHsprefixed
  keys (e.g. { profile: { "$gt": "" } }) are stripped at every depth,
  closing a NoSQL injection vector through nested update payloads.

- Add siteUrl and authProviders to the Project.findOne().select() clause
  in verifyApiKey.js. Without these fields the cached project object is
  missing data required by getFrontendCallbackBaseUrl(), causing social
  auth callbacks to always redirect to localhost:5173 in production.

All 90 public-api tests pass.
Copilot AI review requested due to automatic review settings April 14, 2026 06:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

Warning

Rate limit exceeded

@prathamtagad has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 57 minutes and 41 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 57 minutes and 41 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e5a86752-1bae-4003-a8c0-285800c05a43

📥 Commits

Reviewing files that changed from the base of the PR and between e2ecf11 and 5a0378e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • apps/dashboard-api/src/app.js
  • apps/public-api/src/controllers/data.controller.js
📝 Walkthrough

Walkthrough

This PR consolidates 15 file changes across authentication, storage, data, and infrastructure layers. Updates include: replacing Math.random with cryptographically secure OTP generation, standardizing API response shapes with wrapped success/data/message structure, normalizing email handling in verification flows, enhancing input validation with recursive array sanitization, securing storage deletion with role and confirmation checks, adding Redis shutdown handling, and removing unused middleware.

Changes

Cohort / File(s) Summary
Authentication & User Auth
apps/public-api/src/controllers/userAuth.controller.js
Updated OTP generation to use crypto.randomInt(100000, 1000000) instead of Math.random arithmetic; normalized email usage in verification flows with Redis keys and queue payloads; wrapped signup/login responses in {success: true, data: {...}, message} structure; changed signup error logging to console.error.
API Response Standardization
apps/public-api/src/controllers/storage.controller.js, apps/public-api/src/controllers/data.controller.js
Updated deleteFile, insertData, and getAllData to return standardized {success: true, data: {...}, message} response structure; added precondition checks to deleteAllFiles (require keyRole: 'secret' and confirm: 'DELETE_ALL_FILES').
Middleware & API Key Handling
apps/public-api/src/middlewares/verifyApiKey.js, apps/dashboard-api/src/middlewares/authMiddleware.js, apps/dashboard-api/src/middlewares/loadProjectForAdmin.js
Added authProviders field to Project query projection in verifyApiKey; removed debug logging in authMiddleware; removed entire loadProjectForAdmin middleware.
Input Validation & Sanitization
packages/common/src/utils/input.validation.js
Enhanced sanitize function to recursively handle arrays and nested objects, excluding keys starting with $.
Infrastructure & Graceful Shutdown
apps/public-api/src/app.js, apps/dashboard-api/src/app.js, apps/public-api/src/middlewares/api_usage.js
Added persistent worker references (webhookWorkerRef, authEmailWorkerRef); extended graceful shutdown to close Redis connections and BullMQ workers; fixed 404 handler to explicitly set HTTP status 404; consolidated rate limiter validate options.
External Resource Management
apps/dashboard-api/src/controllers/project.controller.js
Updated deleteExternalDbConfig to parse projectId from params instead of body; added project cache refresh via deleteProjectById and setProjectById after config removal.
Utilities & Configuration
packages/common/src/utils/encryption.js, packages/common/src/utils/GC.js, packages/common/src/queues/webhookQueue.js, packages/common/src/config/redis.js, apps/public-api/src/utils/mailLimit.js, package.json
Moved ENCRYPTION_KEY reading from module level to function runtime; moved "Garbage Collecting" log to function execution; added worker persistence with idempotency check in webhookQueue; removed debug logs from redis config; added project parameter to getMonthlyMailLimit; removed path dependency from root package.json.
Test Updates
apps/public-api/src/__tests__/storage.controller.test.js, apps/public-api/src/__tests__/userAuth.refresh.test.js
Updated deleteFile assertions to expect wrapped response format; added keyRole and confirmation fields to deleteAllFiles tests; updated login and refreshToken assertions to expect standardized response shapes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

  • Cryptographically Weak #105: Cryptographically weak OTP generation — this PR replaces Math.random-based OTP arithmetic with crypto.randomInt(100000, 1000000) across signup, resendVerificationOtp, and requestPasswordReset endpoints.

Possibly related PRs

Suggested reviewers

  • yash-pouranik

Poem

🐰 Hopping through the code with cryptographic flair,
OTPs now secure, no randomness in the air!
Responses wrapped neat in success and data's care,
Workers persist, Redis quits with style to spare,
Tests pass with glee—standardization everywhere! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately describes three key security-focused fixes: cryptographically secure OTP generation, recursive sanitization for NoSQL injection prevention, and API key verification project field 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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 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 and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Hardens parts of the public API by improving OTP generation security, strengthening request sanitization against NoSQL operator injection, and expanding the Project projection used during API key verification.

Changes:

  • Replaced OTP generation sites using Math.random() with crypto.randomInt() in userAuth.controller.js.
  • Updated sanitize() to recursively strip $-prefixed keys across nested objects/arrays.
  • Added siteUrl and authProviders to the Project fields selected in verifyApiKey middleware.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
packages/common/src/utils/input.validation.js Makes sanitize() recursive to remove $ keys at all depths.
apps/public-api/src/middlewares/verifyApiKey.js Extends Project .select() projection to include siteUrl and authProviders.
apps/public-api/src/controllers/userAuth.controller.js Switches OTP generation to use Node crypto CSPRNG.

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

Comment on lines +48 to +49
siteUrl
authProviders
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Adding siteUrl/authProviders to this middleware’s Project projection may not resolve the social-auth redirect issue described in the PR: the social flow re-fetches the project via getSocialProviderConfig() (apps/public-api/src/controllers/userAuth.controller.js around the selectClause construction) and that select does not include siteUrl, which is what getFrontendCallbackBaseUrl() reads. Consider including siteUrl in the getSocialProviderConfig() projection (or reusing req.project there) so the callback URL is built from the actual project setting.

Copilot uses AI. Check for mistakes.
Comment on lines +293 to +294
for (const key in obj) {
if (!key.startsWith("$")) {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

sanitize() now recursively copies keys into a fresh object, but it still allows special keys like __proto__, constructor, and prototype. If an attacker submits these keys in JSON payloads (e.g. in insert/update endpoints that call sanitize()), assigning them onto clean can mutate the object's prototype and lead to prototype-pollution style issues. Please explicitly block these keys (in addition to $-prefixed keys) and consider iterating only own-properties (e.g., Object.keys() / hasOwnProperty) when building clean.

Suggested change
for (const key in obj) {
if (!key.startsWith("$")) {
const blockedKeys = new Set(["__proto__", "constructor", "prototype"]);
for (const key of Object.keys(obj)) {
if (!key.startsWith("$") && !blockedKeys.has(key)) {

Copilot uses AI. Check for mistakes.
Comment on lines +970 to 972
const otp = crypto.randomInt(100000, 1000000).toString();
await redis.set(`project:${project._id}:otp:verification:${email}`, otp, 'EX', 300);
await setPublicOtpCooldown(project._id, email, 'verification');
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

OTP/cooldown Redis keys here are built with the raw email from the request, but verifyEmail later looks up the OTP using a normalized/lowercased email in the Redis key. If the user signs up with mixed-case or surrounding whitespace, the OTP will be stored under a different key and verification (and cooldown enforcement) can fail / be bypassed. Use the same normalized email value consistently for OTP storage and cooldown keys (and ideally for downstream email sending payloads too).

Copilot uses AI. Check for mistakes.
Comment on lines +995 to 996
const otp = crypto.randomInt(100000, 1000000).toString();

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

In this signup path, normalizedEmail is computed earlier but the OTP is later stored in Redis under a key that uses the raw email value (see the redis.set(...otp:verification:${email}...) a few lines below). This can break verification/cooldown when the input email contains uppercase or whitespace. Use the normalized email consistently for OTP storage and subsequent lookups.

Copilot uses AI. Check for mistakes.
Comment on lines 1306 to 1308
const otp = crypto.randomInt(100000, 1000000).toString();
await redis.set(`project:${project._id}:otp:verification:${email}`, otp, 'EX', 300);
await setPublicOtpCooldown(project._id, email, 'verification');
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Same issue as in signup: OTP/cooldown Redis keys use the raw email value, but the verification flow normalizes email before reading the OTP key. This can cause resend + verification to fail for mixed-case/whitespace emails and can allow cooldown bypass by changing email casing. Store OTP and set cooldown using the normalized email consistently.

Copilot uses AI. Check for mistakes.
- enforce correct api response formats
- add graceful shutdown for redis/bullmq
- fix email normalization on verify/resend
- remove dead code and noisy logs
- secure storage delete endpoints
@yash-pouranik yash-pouranik self-requested a review April 15, 2026 18:12
@yash-pouranik
Copy link
Copy Markdown
Collaborator

@prathamtagad
Thank you for the PR
Can you please Resolve Merge Conflicts??

@prathamtagad
Copy link
Copy Markdown
Author

@prathamtagad Thank you for the PR Can you please Resolve Merge Conflicts??

Done i've resolved the Conflicts ;)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
packages/common/src/utils/encryption.js (1)

27-42: ⚠️ Potential issue | 🟠 Major

Make decrypt() fail fast on missing key instead of silently returning null.

Line 27 currently allows missing-key misconfiguration to be swallowed by the broad try/catch and reported as generic decryption failure. That makes config errors look like tampering and obscures root cause.

🔧 Suggested fix
 function decrypt(encryptedData) {
     const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY;
+    if (!ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY missing in .env");
     try {
         const iv = Buffer.from(encryptedData.iv, "hex");
         const tag = Buffer.from(encryptedData.tag, "hex");
         const encryptedText = Buffer.from(encryptedData.encrypted, "hex");

         const decipher = crypto.createDecipheriv(algorithm, Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
         decipher.setAuthTag(tag);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/src/utils/encryption.js` around lines 27 - 42, The decrypt
function currently swallows a missing ENCRYPTION_KEY inside its try/catch; fix
by validating ENCRYPTION_KEY before the try block in function decrypt (i.e.,
check the ENCRYPTION_KEY constant) and if it's missing or falsy throw a clear
Error (e.g., "ENCRYPTION_KEY not set") so the misconfiguration fails fast
instead of being caught and reported as a generic decryption failure; ensure the
Buffer.from(ENCRYPTION_KEY, 'hex') call only happens after this check so you
don't hit a misleading catch path.
🧹 Nitpick comments (3)
apps/public-api/src/utils/mailLimit.js (1)

18-21: Use project or remove it to avoid a misleading helper contract.

getMonthlyMailLimit(project) accepts project but ignores it, while the call site now passes req.project. This suggests tier/project-aware limits that are not actually enforced yet. Either implement project-based limit resolution here or keep the old no-arg signature until that logic is ready.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/utils/mailLimit.js` around lines 18 - 21,
getMonthlyMailLimit currently accepts a project parameter but ignores it, which
is misleading because call sites pass req.project; either implement
project-aware logic here or remove the parameter. Update getMonthlyMailLimit to
inspect the passed project (e.g., project.tier or project.plan) and return
different limits instead of always MONTHLY_FREE_MAIL_LIMIT, or change its
signature to getMonthlyMailLimit() and update all callers (where req.project is
passed) to stop passing an argument so the helper contract matches behavior.
Ensure references to getMonthlyMailLimit and MONTHLY_FREE_MAIL_LIMIT are updated
consistently.
apps/public-api/src/__tests__/storage.controller.test.js (1)

288-288: LGTM, but consider adding tests for new validation paths.

The test correctly includes the new required fields (keyRole: 'secret', body: { confirm: 'DELETE_ALL_FILES' }).

However, there are no tests covering the new validation error paths added to deleteAllFiles:

  • keyRole !== 'secret' should return 403
  • Missing or incorrect confirm value should return 400
💡 Suggested additional test cases
test('returns 403 when keyRole is not secret', async () => {
    const req = { project: makeProject(), keyRole: 'publishable', body: { confirm: 'DELETE_ALL_FILES' } };
    const res = makeRes();

    await storageController.deleteAllFiles(req, res);

    expect(res.status).toHaveBeenCalledWith(403);
    expect(res.json).toHaveBeenCalledWith({ error: 'Access denied. Action requires a Secret Key.' });
});

test('returns 400 when confirm token is missing', async () => {
    const req = { project: makeProject(), keyRole: 'secret', body: {} };
    const res = makeRes();

    await storageController.deleteAllFiles(req, res);

    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ error: expect.stringContaining('DELETE_ALL_FILES') }));
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/__tests__/storage.controller.test.js` at line 288, Add
tests for the new validation branches in deleteAllFiles: one test that calls
storageController.deleteAllFiles with req.keyRole set to a non-'secret' value
(e.g., 'publishable') and asserts res.status was called with 403 and res.json
with { error: 'Access denied. Action requires a Secret Key.' }, and another test
that calls deleteAllFiles with keyRole:'secret' but missing or incorrect
req.body.confirm and asserts res.status was called with 400 and res.json
contains an error mentioning 'DELETE_ALL_FILES'; reuse helpers makeProject() and
makeRes() to construct req/res and keep test names descriptive.
apps/public-api/src/controllers/data.controller.js (1)

157-166: LGTM!

Response format properly standardized with explicit HTTP 200 status.

Note: Other endpoints in this file (getSingleDoc, updateSingleData, deleteSingleDoc) and error responses still use non-standardized formats. Consider standardizing them in a follow-up for consistency with the coding guideline that all API endpoints return { success: bool, data: {}, message: "" }.

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/controllers/data.controller.js` around lines 157 - 166,
Other endpoints in this controller (getSingleDoc, updateSingleData,
deleteSingleDoc) and their error handlers are not using the standardized
response shape; update each of those functions to return res.status(...).json({
success: boolean, data: {...} or null, message: "..." }) on success and
res.status(...).json({ success: false, data: {}, message: "<error message>" })
on errors, preserving appropriate HTTP status codes (e.g., 200, 404, 400, 500)
and keeping existing response payloads inside the data field so all endpoints
follow the same { success, data, message } contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/dashboard-api/src/app.js`:
- Around line 162-165: The code always prints "✅ Redis connection closed." even
if the redis client doesn't exist or quit() wasn't called; change this so the
success log is emitted only when redis && typeof redis.quit === 'function' and
after await redis.quit() completes (place the console.log inside that if block),
and wrap the await redis.quit() in a try/catch to log failures (use the same
redis variable and its quit method) so missing clients or errors don't produce a
misleading success message.

In `@apps/dashboard-api/src/controllers/project.controller.js`:
- Around line 512-513: The current flow calls deleteProjectById(projectId) then
setProjectById(projectId, project.toObject()) but only refreshes the
project:id:${projectId} cache; you must also evict any API-key based caches so
public lookups don't return stale config. Update the code around
deleteProjectById/setProjectById to call the cache invalidation utility for keys
like project:publishableKey:${project.publishableKey} and
project:secretKey:${project.secretKey} (or the equivalent cache helper you use)
before re-setting the project cache, ensuring both API-key and id-based entries
are cleared and then repopulated.
- Around line 496-500: Validate incoming projectId using
mongoose.Types.ObjectId.isValid before calling Project.findOne (replace the
current empty-string guard) and return a 400 with { success: false, data: {},
message: "Invalid project ID." } for invalid ids; on DB errors, do not return
err.message to the client—instead log the error internally and return { success:
false, data: {}, message: "Failed to fetch project." } with a 500 status; on
success return { success: true, data: { project }, message: "Project fetched
successfully." }. Apply the same ObjectId validation and response-format changes
to deleteExternalStorageConfig, and reference Project.findOne, projectId, and
deleteExternalStorageConfig when updating the code.

In `@apps/public-api/src/app.js`:
- Around line 137-143: initWebhookWorker currently creates a new Worker on each
call causing the startup worker to never be closed; modify initWebhookWorker (in
webhookQueue.js) to cache the created worker (e.g., a module-scoped variable
`worker`) and return it if already set (same pattern used by
initAuthEmailWorker), or alternatively capture and store the startup worker
reference where it is created and use that stored instance in the shutdown
sequence; ensure the shutdown code calls close() on the cached/stored worker
instance rather than creating a new one.

In `@apps/public-api/src/middlewares/blockUsersCollectionDataAccess.js`:
- Around line 2-4: The middleware currently compares collectionName === 'users'
which only catches lowercase; normalize the extracted collectionName (e.g., call
.toLowerCase() on the trimmed String(req.params?.collectionName || '') or
otherwise convert to lowercase) before performing the guard so any case variants
like "Users" or "USERS" are blocked; update the comparison that references
collectionName in this middleware to use the normalized/lowercased value.

---

Outside diff comments:
In `@packages/common/src/utils/encryption.js`:
- Around line 27-42: The decrypt function currently swallows a missing
ENCRYPTION_KEY inside its try/catch; fix by validating ENCRYPTION_KEY before the
try block in function decrypt (i.e., check the ENCRYPTION_KEY constant) and if
it's missing or falsy throw a clear Error (e.g., "ENCRYPTION_KEY not set") so
the misconfiguration fails fast instead of being caught and reported as a
generic decryption failure; ensure the Buffer.from(ENCRYPTION_KEY, 'hex') call
only happens after this check so you don't hit a misleading catch path.

---

Nitpick comments:
In `@apps/public-api/src/__tests__/storage.controller.test.js`:
- Line 288: Add tests for the new validation branches in deleteAllFiles: one
test that calls storageController.deleteAllFiles with req.keyRole set to a
non-'secret' value (e.g., 'publishable') and asserts res.status was called with
403 and res.json with { error: 'Access denied. Action requires a Secret Key.' },
and another test that calls deleteAllFiles with keyRole:'secret' but missing or
incorrect req.body.confirm and asserts res.status was called with 400 and
res.json contains an error mentioning 'DELETE_ALL_FILES'; reuse helpers
makeProject() and makeRes() to construct req/res and keep test names
descriptive.

In `@apps/public-api/src/controllers/data.controller.js`:
- Around line 157-166: Other endpoints in this controller (getSingleDoc,
updateSingleData, deleteSingleDoc) and their error handlers are not using the
standardized response shape; update each of those functions to return
res.status(...).json({ success: boolean, data: {...} or null, message: "..." })
on success and res.status(...).json({ success: false, data: {}, message: "<error
message>" }) on errors, preserving appropriate HTTP status codes (e.g., 200,
404, 400, 500) and keeping existing response payloads inside the data field so
all endpoints follow the same { success, data, message } contract.

In `@apps/public-api/src/utils/mailLimit.js`:
- Around line 18-21: getMonthlyMailLimit currently accepts a project parameter
but ignores it, which is misleading because call sites pass req.project; either
implement project-aware logic here or remove the parameter. Update
getMonthlyMailLimit to inspect the passed project (e.g., project.tier or
project.plan) and return different limits instead of always
MONTHLY_FREE_MAIL_LIMIT, or change its signature to getMonthlyMailLimit() and
update all callers (where req.project is passed) to stop passing an argument so
the helper contract matches behavior. Ensure references to getMonthlyMailLimit
and MONTHLY_FREE_MAIL_LIMIT are updated consistently.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 177611e2-fd96-4861-bd0b-7bd977a031c0

📥 Commits

Reviewing files that changed from the base of the PR and between a61dd3e and 1b24a73.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • apps/dashboard-api/src/app.js
  • apps/dashboard-api/src/controllers/project.controller.js
  • apps/dashboard-api/src/middlewares/authMiddleware.js
  • apps/dashboard-api/src/middlewares/loadProjectForAdmin.js
  • apps/public-api/src/__tests__/storage.controller.test.js
  • apps/public-api/src/__tests__/userAuth.refresh.test.js
  • apps/public-api/src/app.js
  • apps/public-api/src/controllers/data.controller.js
  • apps/public-api/src/controllers/storage.controller.js
  • apps/public-api/src/controllers/userAuth.controller.js
  • apps/public-api/src/middlewares/api_usage.js
  • apps/public-api/src/middlewares/blockUsersCollectionDataAccess.js
  • apps/public-api/src/middlewares/verifyApiKey.js
  • apps/public-api/src/utils/mailLimit.js
  • package.json
  • packages/common/src/config/redis.js
  • packages/common/src/utils/GC.js
  • packages/common/src/utils/encryption.js
💤 Files with no reviewable changes (4)
  • packages/common/src/config/redis.js
  • apps/dashboard-api/src/middlewares/authMiddleware.js
  • apps/dashboard-api/src/middlewares/loadProjectForAdmin.js
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • apps/public-api/src/middlewares/api_usage.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/public-api/src/controllers/userAuth.controller.js
  • apps/public-api/src/middlewares/verifyApiKey.js

Comment on lines +162 to +165
const { redis } = require('@urbackend/common');
if (redis && typeof redis.quit === 'function') await redis.quit();
console.log('✅ Redis connection closed.');

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.

⚠️ Potential issue | 🟡 Minor

Avoid unconditional Redis “closed” log.

At Line 164, success is logged even when redis is missing or quit is not called. This can mislead incident/debugging traces.

Suggested patch
                 const { redis } = require('@urbackend/common');
-                if (redis && typeof redis.quit === 'function') await redis.quit();
-                console.log('✅ Redis connection closed.');
+                if (redis && typeof redis.quit === 'function') {
+                    await redis.quit();
+                    console.log('✅ Redis connection closed.');
+                } else {
+                    console.log('ℹ️ Redis shutdown skipped (client unavailable).');
+                }
📝 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
const { redis } = require('@urbackend/common');
if (redis && typeof redis.quit === 'function') await redis.quit();
console.log('✅ Redis connection closed.');
const { redis } = require('@urbackend/common');
if (redis && typeof redis.quit === 'function') {
await redis.quit();
console.log('✅ Redis connection closed.');
} else {
console.log('ℹ️ Redis shutdown skipped (client unavailable).');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard-api/src/app.js` around lines 162 - 165, The code always prints
"✅ Redis connection closed." even if the redis client doesn't exist or quit()
wasn't called; change this so the success log is emitted only when redis &&
typeof redis.quit === 'function' and after await redis.quit() completes (place
the console.log inside that if block), and wrap the await redis.quit() in a
try/catch to log failures (use the same redis variable and its quit method) so
missing clients or errors don't produce a misleading success message.

Comment on lines +496 to 500
const projectId = String(req.params.projectId || '').trim();
if (!projectId) return res.status(400).json({ error: "Project ID is required." });

const project = await Project.findOne({
_id: { $eq: projectId },
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Controller and route references:"
rg -n -C4 '\bdeleteExternal(Db|Storage)Config\b' --type js --type ts

echo
echo "Any dashboard-api projectId/ObjectId validation:"
rg -n -C3 'ObjectId\.isValid|isValidObjectId|param\(["'"'"']projectId|projectId.*schema|projectId.*parse' --type js --type ts apps/dashboard-api

Repository: geturbackend/urBackend

Length of output: 4704


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== deleteExternalDbConfig full implementation ==="
sed -n '494,521p' apps/dashboard-api/src/controllers/project.controller.js

echo
echo "=== deleteExternalStorageConfig full implementation ==="
sed -n '523,550p' apps/dashboard-api/src/controllers/project.controller.js

echo
echo "=== Success response format in other project handlers ==="
rg -n -A2 'res\.status\(200\)\.json|res\.json\(\{.*success' apps/dashboard-api/src/controllers/project.controller.js | head -50

echo
echo "=== Cache invalidation in other mutation handlers ==="
rg -n -B2 -A2 'deleteProjectByApiKeyCache|setProjectById|deleteProjectById' apps/dashboard-api/src/controllers/project.controller.js

Repository: geturbackend/urBackend

Length of output: 5828


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if AppError is imported or defined ==="
head -30 apps/dashboard-api/src/controllers/project.controller.js | grep -i apperror || echo "(not found in imports)"

echo
echo "=== Check how other handlers validate ObjectId (broader search) ==="
rg -n 'ObjectId\.isValid|mongoose\.Types\.ObjectId\.isValid' apps/dashboard-api/src/controllers/ --type js

echo
echo "=== Check standard error response pattern in this codebase ==="
rg -n 'res\.status\(400\)\.json\|res\.status\(404\)\.json' apps/dashboard-api/src/controllers/project.controller.js | head -20

Repository: geturbackend/urBackend

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Error response patterns in webhook.controller.js ==="
grep -n "res\.status" apps/dashboard-api/src/controllers/webhook.controller.js | head -30

echo
echo "=== Sample success responses in webhook.controller.js ==="
rg -n 'res\.(json|status.*json)' apps/dashboard-api/src/controllers/webhook.controller.js -A1 | head -40

echo
echo "=== Check import statements at top of project.controller.js ==="
head -20 apps/dashboard-api/src/controllers/project.controller.js

echo
echo "=== Check if Mongoose import exists ==="
grep -n "require.*mongoose\|import.*mongoose" apps/dashboard-api/src/controllers/project.controller.js

Repository: geturbackend/urBackend

Length of output: 4093


Validate projectId as an ObjectId and fix response formats to match API guidelines.

This code has two issues:

  1. ObjectId validation missing: The guard only rejects empty strings. A malformed non-empty projectId can still reach Mongoose, trigger a CastError, and expose raw error details (res.status(500).json({ error: err.message })). Mongoose precedent exists in webhook.controller.js.

  2. Response format violations: Both success and error responses violate the guideline { success: bool, data: {}, message: "" }. Currently returns { message: "..." } on success and { error: err.message } on error, exposing MongoDB/library errors to clients.

🔧 Suggested fix
  module.exports.deleteExternalDbConfig = async (req, res) => {
  try {
    const projectId = String(req.params.projectId || "").trim();
+   if (!mongoose.Types.ObjectId.isValid(projectId)) {
+     return res.status(400).json({
+       success: false,
+       data: {},
+       message: "Invalid project ID.",
+     });
+   }
-   if (!projectId) return res.status(400).json({ error: "Project ID is required." });

    const project = await Project.findOne({
      _id: { $eq: projectId },
      owner: req.user._id,
    });
    if (!project)
      return res
        .status(404)
-       .json({ error: "Project not found or access denied." });
+       .json({ success: false, data: {}, message: "Project not found or access denied." });

    project.resources.db.isExternal = false;
    project.resources.db.config = null;
    await project.save();

    await deleteProjectById(projectId);
    await setProjectById(projectId, project.toObject());

    res
      .status(200)
-     .json({ message: "External configuration deleted successfully." });
+     .json({ success: true, data: {}, message: "External configuration deleted successfully." });
  } catch (err) {
-   res.status(500).json({ error: err.message });
+   res.status(500).json({ success: false, data: {}, message: "Internal server error." });
  }
};

Apply the same fixes to deleteExternalStorageConfig (lines 525–529).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard-api/src/controllers/project.controller.js` around lines 496 -
500, Validate incoming projectId using mongoose.Types.ObjectId.isValid before
calling Project.findOne (replace the current empty-string guard) and return a
400 with { success: false, data: {}, message: "Invalid project ID." } for
invalid ids; on DB errors, do not return err.message to the client—instead log
the error internally and return { success: false, data: {}, message: "Failed to
fetch project." } with a 500 status; on success return { success: true, data: {
project }, message: "Project fetched successfully." }. Apply the same ObjectId
validation and response-format changes to deleteExternalStorageConfig, and
reference Project.findOne, projectId, and deleteExternalStorageConfig when
updating the code.

Comment on lines +512 to +513
await deleteProjectById(projectId);
await setProjectById(projectId, project.toObject());
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.

⚠️ Potential issue | 🟠 Major

Evict API-key caches here too.

This only refreshes project:id:${projectId}. Public API lookups keyed by publishableKey/secretKey can keep serving the old external DB config until TTL expiry, even though the delete call already succeeded.

🔧 Suggested invalidation
     await project.save();

     await deleteProjectById(projectId);
     await setProjectById(projectId, project.toObject());
+    await deleteProjectByApiKeyCache(project.publishableKey);
+    await deleteProjectByApiKeyCache(project.secretKey);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard-api/src/controllers/project.controller.js` around lines 512 -
513, The current flow calls deleteProjectById(projectId) then
setProjectById(projectId, project.toObject()) but only refreshes the
project:id:${projectId} cache; you must also evict any API-key based caches so
public lookups don't return stale config. Update the code around
deleteProjectById/setProjectById to call the cache invalidation utility for keys
like project:publishableKey:${project.publishableKey} and
project:secretKey:${project.secretKey} (or the equivalent cache helper you use)
before re-setting the project cache, ensuring both API-key and id-based entries
are cleared and then repopulated.

Comment thread apps/public-api/src/app.js Outdated
Comment thread apps/public-api/src/middlewares/blockUsersCollectionDataAccess.js Outdated
@yash-pouranik
Copy link
Copy Markdown
Collaborator

@prathamtagad Bro coderabbit have added some comments :(

@prathamtagad
Copy link
Copy Markdown
Author

@yash-pouranik Aah! here we go again.

@yash-pouranik
Copy link
Copy Markdown
Collaborator

waiting!!! @prathamtagad

… Zod log noise

- Fix refresh-token rotation test: assert flat response shape matching
  the actual controller output (not the login envelope)
- Normalize collectionName to lowercase in blockUsersCollectionDataAccess
  to prevent mixed-case bypass (e.g. 'Users', 'USERS')
- Add singleton guard to initWebhookWorker() (matching authEmailWorker
  pattern) and store worker refs at startup for clean shutdown
- Move console.error below ZodError check in data.controller aggregate
  handler to suppress expected 400 validation noise in CI
…tize-siteurl

# Conflicts:
#	package-lock.json
@prathamtagad
Copy link
Copy Markdown
Author

waiting!!! @prathamtagad

Done Bro, also resolved the conflict ;)

})
.parse(req.body);
const { projectId } = parsedBody;
const projectId = String(req.params.projectId || '').trim();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

cool thank you for making this easy.

Comment on lines -1 to -19
// FUNCTION - LOAD PROJECT FOR ADMIN (MIDDLEWARE)
const Project = require('../models/Project');

module.exports = async (req, res, next) => {
try {
const { projectId } = req.params;
if (!projectId) return res.status(400).json({ error: "Project ID is required" });

const project = await Project.findOne({ _id: projectId, owner: req.user._id });
if (!project) {
return res.status(404).json({ error: "Project not found or access denied" });
}

req.project = project;
next();
} catch (err) {
res.status(500).json({ error: err.message });
}
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Have you deeted this whole code??? why??

Comment on lines 1026 to 1036
res.status(201).json({
message: "User registered successfully. Please verify your email.",
token: issuedTokens.accessToken,
accessToken: issuedTokens.accessToken,
expiresIn: issuedTokens.expiresIn,
...(shouldExposeRefreshToken(req) ? { refreshToken: issuedTokens.refreshToken } : {}),
userId: result._id
success: true,
data: {
token: issuedTokens.accessToken,
accessToken: issuedTokens.accessToken,
expiresIn: issuedTokens.expiresIn,
...(shouldExposeRefreshToken(req) ? { refreshToken: issuedTokens.refreshToken } : {}),
userId: result._id
},
message: "User registered successfully. Please verify your email."
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

you have updated the response type - but sdk is using the older response type I think, can u please check this as well?? other wise it will break signup calls from sdk.

Comment on lines 1078 to 1087
res.json({
token: issuedTokens.accessToken,
accessToken: issuedTokens.accessToken,
expiresIn: issuedTokens.expiresIn,
...(shouldExposeRefreshToken(req) ? { refreshToken: issuedTokens.refreshToken } : {})
success: true,
data: {
token: issuedTokens.accessToken,
accessToken: issuedTokens.accessToken,
expiresIn: issuedTokens.expiresIn,
...(shouldExposeRefreshToken(req) ? { refreshToken: issuedTokens.refreshToken } : {})
},
message: "Login successful"
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same sdk Qn for this?

allowedDomains
isAuthEnabled
siteUrl
authProviders
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why u have added auth providers??
can u pleae check @coderabbitai , whereever the auth provider os used is it fetching from db in that call or using the data from cache?

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.

🧩 Analysis chain

Let me search the codebase to trace all usages of authProviders and whether they rely on cached req.project data or make fresh DB calls.


🏁 Script executed:

#!/bin/bash
echo "=== All usages of authProviders ==="
rg -n "authProviders" --type=js -C3

echo ""
echo "=== getFrontendCallbackBaseUrl usages ==="
rg -n "getFrontendCallbackBaseUrl" --type=js -C5

echo ""
echo "=== Social auth / provider callback usages in public-api ==="
rg -n "req\.project\.(authProviders|siteUrl)" --type=js -C4

Length of output: 30178


@yash-pouranik — great question. Here's what the code actually does:

authProviders is NOT read from req.project anywhere. The grep for req.project.authProviders (and req.project.siteUrl) returns zero matches across the entire public-api.

Here's the actual flow for social OAuth:

  1. verifyApiKey middleware runs → fetches project (from cache or DB) → attaches to req.project
  2. The OAuth start handler calls getOAuthProviderConfig(projectId, provider) (lines 196–206 of userAuth.controller.js), which does its own fresh Project.findById().select().lean() DB call — completely bypassing req.project
  3. It has to do a fresh DB call because it needs the encrypted clientSecret fields (+authProviders.${provider}.clientSecret.encrypted/iv/tag) which are excluded from the default projection and also excluded from the verifyApiKey cache
// userAuth.controller.js – always fetches fresh, never uses req.project
const project = await Project.findById(projectId).select(selectClause).lean();
const providerConfig = project.authProviders?.[provider];

Conclusion: The authProviders field added to the verifyApiKey select projection is currently unused — nothing downstream reads req.project.authProviders. It gets cached unnecessarily. It can be safely removed without any functional impact.

A legitimate future optimization would be to check req.project.authProviders[provider].enabled as an early-exit guard before making the full DB call, but that code doesn't exist yet.


🧠 Learnings used
Learnt from: CR
Repo: geturbackend/urBackend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-11T08:21:56.441Z
Learning: Applies to **/*{[Aa]uth*,[Pp]rovider*}*.{js,ts,jsx,tsx} : Social auth flow rules: Provider callback URLs are backend-generated and read-only in the dashboard. Users only configure `enabled`, `clientId`, and `clientSecret`. Post-login redirect comes from `project.siteUrl`, not provider-specific redirect fields. Successful provider auth redirects to `<siteUrl>/auth/callback`.

Learnt from: CR
Repo: geturbackend/urBackend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-11T08:21:56.441Z
Learning: Applies to **/{utils,controllers}/**/*{[Rr]efresh,[Oo]auth,[Ss]ession,[Rr]edis}*.{js,ts} : Do not change Redis key patterns (`project:auth:refresh:session:{tokenId}`, `project:auth:oauth:state:{state}`, `project:mail:count:{projectId}:{YYYY-MM}`) — existing sessions will break.

Learnt from: CR
Repo: geturbackend/urBackend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-11T08:21:56.441Z
Learning: Applies to **/*{[Uu]serAuth,[Rr]efreshToken}*.{js,ts} : If changing public auth behavior, check `userAuth.controller.js`, `userAuth.js` routes, `refreshToken.js` utilities, and corresponding test files.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@prathamtagad remove that from verify api key

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines 6 to 8
function encrypt(plainText) {
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY;
if (!ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY missing in .env");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

very nice move hn.

@yash-pouranik
Copy link
Copy Markdown
Collaborator

@prathamtagad Sorry but 😶‍🌫️🥺

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants