Skip to content

 feat(rls): add owner-write RLS flow and move dashboard RLS config t…#51

Merged
yash-pouranik merged 5 commits into
mainfrom
rls/v1-write-on-ur-own-doc
Mar 28, 2026
Merged

 feat(rls): add owner-write RLS flow and move dashboard RLS config t…#51
yash-pouranik merged 5 commits into
mainfrom
rls/v1-write-on-ur-own-doc

Conversation

@yash-pouranik
Copy link
Copy Markdown
Collaborator

@yash-pouranik yash-pouranik commented Mar 27, 2026

  • Explore relevant files
  • Fix resolvePublicAuthContext.js: short-circuit for secret key to skip unnecessary JWT work
  • Fix authorizeWriteOperation.js: guard req.authUser null crash when requireAuthForWrite: false
  • Fix project.controller.js getSingleProject: use getDefaultRlsForCollection for schema-derived defaults
  • Fix project.controller.js toggleAuth: use getDefaultRlsForCollection for schema-derived defaults
  • Fix project.controller.js updateCollectionRls: restrict _id ownerField to users collection only
  • Fix Database.jsx RLS defaults: derive ownerField from schema (userId > ownerId > first key)
  • Fix Database.jsx rlsOwnerFieldOptions: generate options from schema keys only (not hardcoded)
  • Fix Database.jsx RLS dialog: add role="dialog", aria-modal, aria-labelledby for accessibility
  • Fix getVerificationField in both userAuth.controller.js files: return null instead of hardcoded 'emailVerified' fallback; guard both call sites (buildAuthUserPayload skips field, verifyEmail returns 500 with clear error)
  • Run CodeQL - no issues found

⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

…o modal

 - add collection-level RLS config support in project model/api
 - add public-api auth-context + write authorization middleware
 - allow PATCH in public data routes
 - replace inline RLS controls with compact RLS button + settings dialog
 - clarify ownerField help text in English
 - update README/docs for RLS publishable write behavior and PATCH endpoint
 - fix users default RLS ownerField consistency to _id
Copilot AI review requested due to automatic review settings March 27, 2026 21:03
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ur-backend-web-dashboard Ready Ready Preview, Comment Mar 28, 2026 8:47am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 27, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • apps/web-dashboard/dist/assets/index-CystpbmR.js is excluded by !**/dist/**
  • apps/web-dashboard/dist/index.html is excluded by !**/dist/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 73ad39e5-e3e6-44fc-b13f-5833e233b1a9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces Row Level Security (RLS) functionality across the platform. It adds per-collection RLS configuration with owner-based access control, new middleware for JWT-based user context resolution and write authorization, refactors user model creation to be schema-flexible, and provides dashboard UI for RLS management.

Changes

Cohort / File(s) Summary
Core RLS Data Model
packages/common/src/models/Project.js
Extended collectionSchema with RLS configuration fields: enabled, mode (owner-write-only), ownerField, and requireAuthForWrite.
Dashboard API RLS Management
apps/dashboard-api/src/controllers/project.controller.js, apps/dashboard-api/src/routes/projects.js
Added getDefaultRlsForCollection helper and updateCollectionRls endpoint to manage RLS settings per collection. Updated getSingleProject, createCollection, and toggleAuth to ensure collections include default RLS configuration.
Public API Write Authorization
apps/public-api/src/middlewares/resolvePublicAuthContext.js, apps/public-api/src/middlewares/authorizeWriteOperation.js, apps/public-api/src/routes/data.js
Introduced two new middleware: resolvePublicAuthContext extracts JWT from Authorization header; authorizeWriteOperation enforces RLS-based owner validation for POST/PUT/PATCH/DELETE. Replaced requireSecretKey on write routes with new auth chain. Added PATCH route for partial updates.
User Model Schema Flexibility
apps/dashboard-api/src/controllers/userAuth.controller.js, apps/public-api/src/controllers/userAuth.controller.js
Refactored user creation and email verification to be dynamic based on model schema via helpers buildAuthUserPayload, getVerificationField, and hasRequiredField. Supports schema-defined user fields beyond hardcoded emailVerified.
Web Dashboard RLS UI
apps/web-dashboard/src/pages/Database.jsx
Added RLS configuration UI with toggle for enabling RLS, dropdown for owner field selection, and backend integration via PATCH endpoint. Normalizes collections with default RLS on load.
Documentation
README.md, docs/api-reference.md, docs/database.md, docs/getting-started.md, docs/rls-implementation-roadmap.md
Updated API documentation to reflect publishable-key write behavior with RLS and Bearer tokens. Added comprehensive RLS implementation roadmap from V1 (current) through V5, clarifying authentication requirements and owner-based access control.

Sequence Diagram

sequenceDiagram
    participant Client
    participant resolvePublicAuthContext
    participant authorizeWriteOperation
    participant ProjectModel
    participant UserModel
    participant Database

    Client->>ProjectModel: PATCH /api/data/posts (publishable key + Bearer JWT)
    ProjectModel->>resolvePublicAuthContext: Extract JWT from Authorization header
    resolvePublicAuthContext->>resolvePublicAuthContext: Verify JWT against jwtSecret
    resolvePublicAuthContext->>ProjectModel: Populate req.authUser with userId
    
    ProjectModel->>authorizeWriteOperation: Validate write operation with RLS
    alt RLS Enabled for Collection
        authorizeWriteOperation->>authorizeWriteOperation: Fetch collection RLS config
        alt ownerField in Payload and Matches authUserId
            authorizeWriteOperation->>Database: Proceed with update
            Database->>Client: 200 Success
        else ownerField Mismatch
            authorizeWriteOperation->>Client: 403 Forbidden (ownership violation)
        end
    else RLS Disabled
        authorizeWriteOperation->>Client: 403 Forbidden (publishable key cannot write)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hops through the database with JWT in paw,
Row-level security without a single flaw,
Owners guard their fields with bearer-token cheer,
Publishable keys now know their frontier!
Middleware middleware, dancing in the stack,
Never let unauthorized writes sneak back! 🔐

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding owner-write RLS flow and updating dashboard RLS configuration, which are the primary objectives of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rls/v1-write-on-ur-own-doc

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

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements Row Level Security (RLS) for collections, enabling secure write operations via publishable keys when a valid user JWT is provided. The changes include new backend middlewares for JWT verification and write authorization, a new controller for RLS management, and a dashboard UI for configuration. Feedback identifies a critical potential crash in the authorization middleware if authentication is bypassed while RLS is enabled, along with several instances of code duplication and redundant logic in the project controllers and frontend state initialization that should be refactored for better maintainability.

Comment on lines +27 to +39
if (rls.requireAuthForWrite && !req.authUser?.userId) {
return res.status(401).json({
error: 'Authentication required',
message: 'Provide a valid user Bearer token for write operations.'
});
}

if ((rls.mode || 'owner-write-only') !== 'owner-write-only') {
return res.status(403).json({ error: 'Unsupported RLS mode' });
}

const ownerField = rls.ownerField || 'userId';
const authUserId = String(req.authUser.userId);
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.

critical

This block of code contains a potential server crash. If rls.enabled is true but rls.requireAuthForWrite is false, and no auth token is provided, req.authUser will be null. The code then proceeds to line 39, where String(req.authUser.userId) will throw a TypeError and crash the process.

For an "owner-write-only" policy, the user's identity is essential. Therefore, authentication should be mandatory whenever RLS is enabled for writes.

I suggest refactoring this section to always require authentication when RLS is enabled. This resolves the crash and strengthens the security model.

Suggested change
if (rls.requireAuthForWrite && !req.authUser?.userId) {
return res.status(401).json({
error: 'Authentication required',
message: 'Provide a valid user Bearer token for write operations.'
});
}
if ((rls.mode || 'owner-write-only') !== 'owner-write-only') {
return res.status(403).json({ error: 'Unsupported RLS mode' });
}
const ownerField = rls.ownerField || 'userId';
const authUserId = String(req.authUser.userId);
if (!req.authUser?.userId) {
return res.status(401).json({
error: 'Authentication required',
message: 'Provide a valid user Bearer token for write operations on this RLS-protected collection.'
});
}
if ((rls.mode || 'owner-write-only') !== 'owner-write-only') {
return res.status(403).json({ error: 'Unsupported RLS mode' });
}
const ownerField = rls.ownerField || 'userId';
const authUserId = String(req.authUser.userId);

Comment on lines +35 to +36
} else if (keys.includes('userId')) {
ownerField = 'userId';
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.

medium

This else if block is redundant because ownerField is already initialized to 'userId'. You can safely remove these lines to simplify the code and improve readability.

Comment on lines 145 to +165
if (col.name === 'users' && col.model) {
return {
...col,
model: col.model.filter(m => m.key !== 'password')
model: col.model.filter(m => m.key !== 'password'),
rls: col.rls || {
enabled: false,
mode: 'owner-write-only',
ownerField: '_id',
requireAuthForWrite: true
}
};
}
return col;
return {
...col,
rls: col.rls || {
enabled: false,
mode: 'owner-write-only',
ownerField: 'userId',
requireAuthForWrite: true
}
};
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.

medium

There's significant code duplication here for setting default RLS configurations. This logic is also slightly different from the getDefaultRlsForCollection helper function you've added, which could lead to inconsistencies.

You can refactor this to remove duplication and reuse the helper function, making the code more maintainable.

                const sanitizedCol = { ...col };
                if (sanitizedCol.name === 'users' && sanitizedCol.model) {
                    sanitizedCol.model = sanitizedCol.model.filter(m => m.key !== 'password');
                }
                sanitizedCol.rls = sanitizedCol.rls || getDefaultRlsForCollection(sanitizedCol.name, sanitizedCol.model);
                return sanitizedCol;

Comment on lines 969 to +989
if (col.name === 'users' && col.model) {
return {
...col,
model: col.model.filter(m => m.key !== 'password')
model: col.model.filter(m => m.key !== 'password'),
rls: col.rls || {
enabled: false,
mode: 'owner-write-only',
ownerField: '_id',
requireAuthForWrite: true
}
};
}
return col;
return {
...col,
rls: col.rls || {
enabled: false,
mode: 'owner-write-only',
ownerField: 'userId',
requireAuthForWrite: true
}
};
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.

medium

Similar to getSingleProject, there's duplicated code here for setting default RLS configurations. Reusing the getDefaultRlsForCollection helper function would make this code more maintainable and consistent with other parts of the controller.

                const sanitizedCol = { ...col };
                if (sanitizedCol.name === 'users' && sanitizedCol.model) {
                    sanitizedCol.model = sanitizedCol.model.filter(m => m.key !== 'password');
                }
                sanitizedCol.rls = sanitizedCol.rls || getDefaultRlsForCollection(sanitizedCol.name, sanitizedCol.model);
                return sanitizedCol;

Comment on lines 98 to 124
const withRlsDefaults = (res.data.collections || []).map(c => ({
...c,
rls: c.rls || {
enabled: false,
mode: "owner-write-only",
ownerField: c.name === 'users' ? '_id' : 'userId',
requireAuthForWrite: true
}
}));

setProject(res.data);
setCollections(res.data.collections || []);
setCollections(withRlsDefaults);

const queryCollection = searchParams.get("collection");
if (queryCollection) {
const found = res.data.collections.find(
const found = withRlsDefaults.find(
(c) => c.name === queryCollection
);
if (found) setActiveCollection(found);
} else {
const filtered = res.data.collections.filter(c => c.name !== 'users');
const filtered = withRlsDefaults.filter(c => c.name !== 'users');
if (filtered.length > 0) {
setActiveCollection(filtered[0]);
} else if (res.data.collections.length > 0) {
setActiveCollection(res.data.collections[0]);
} else if (withRlsDefaults.length > 0) {
setActiveCollection(withRlsDefaults[0]);
}
}
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.

medium

This client-side logic to add default RLS settings to collections is redundant. The backend API (/api/projects/:projectId) already ensures that every collection has an rls object with appropriate defaults.

Relying on the backend as the single source of truth is safer and more maintainable. If the backend's default logic changes (e.g., to prefer ownerId over userId), the frontend would become inconsistent. Please remove this mapping and rely on the data provided by the API.

        const collections = res.data.collections || [];

        setProject(res.data);
        setCollections(collections);

        const queryCollection = searchParams.get("collection");
        if (queryCollection) {
          const found = collections.find(
            (c) => c.name === queryCollection
          );
          if (found) setActiveCollection(found);
        } else {
          const filtered = collections.filter(c => c.name !== 'users');
          if (filtered.length > 0) {
            setActiveCollection(filtered[0]);
          } else if (collections.length > 0) {
            setActiveCollection(collections[0]);
          }
        }

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

Completes the RLS V1 rollout by introducing collection-level RLS configuration in the project model, enforcing owner-write authorization for publishable-key writes in public-api, and adding dashboard UX + docs updates for configuring and understanding RLS.

Changes:

  • Added per-collection RLS config (enabled, mode, ownerField, requireAuthForWrite) and a dashboard API endpoint to update it.
  • Replaced public-api secret-key-only write gating with an RLS-aware middleware flow and added PATCH support.
  • Updated the web dashboard Database page UX to configure RLS via a modal, plus documentation updates for the new behavior/endpoints.

Reviewed changes

Copilot reviewed 14 out of 16 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/common/src/models/Project.js Persists collection-level rls configuration in the Project schema.
apps/dashboard-api/src/controllers/project.controller.js Injects RLS defaults into project responses; adds endpoint to update a collection’s RLS settings.
apps/dashboard-api/src/routes/projects.js Exposes the new PATCH /collections/:collectionName/rls route.
apps/public-api/src/routes/data.js Routes write ops through new auth-context + RLS authorization middleware; adds PATCH.
apps/public-api/src/middlewares/resolvePublicAuthContext.js Resolves req.authUser from Authorization: Bearer <token>.
apps/public-api/src/middlewares/authorizeWriteOperation.js Enforces owner-write-only policy for publishable-key writes.
apps/web-dashboard/src/pages/Database.jsx Adds RLS button + settings modal; PATCHes dashboard API to save per-collection RLS config.
apps/web-dashboard/dist/index.html Updates built asset hash reference.
apps/public-api/src/controllers/userAuth.controller.js Refactors user payload creation + verification-field handling for signup/admin flows.
apps/dashboard-api/src/controllers/userAuth.controller.js Mirrors the user payload + verification-field refactor for dashboard-api.
docs/getting-started.md Updates publishable key guidance to reflect RLS-enabled writes.
docs/database.md Documents RLS write requirements and new PATCH endpoint section.
docs/api-reference.md Adds PATCH endpoint + clarifies 403 semantics for RLS.
docs/rls-implementation-roadmap.md Adds a detailed RLS roadmap/reference doc.
README.md Adds a brief note about RLS-enabled publishable-key writes.

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

Comment on lines +217 to +221
const rlsOwnerFieldOptions = (() => {
if (!activeCollection) return [];
const modelKeys = (activeCollection.model || []).map((f) => f.key).filter(Boolean);
return [...new Set(["_id", ...modelKeys, "userId", "ownerId"])];
})();
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

rlsOwnerFieldOptions always includes _id, userId, and ownerId even when those fields aren’t present in the collection schema. Since the backend validates ownerField must exist in the schema (except _id), this UI can offer choices that are guaranteed to fail on save. Consider generating options from the schema keys only (and include _id only for the users collection), so users can’t select an invalid owner field.

Copilot uses AI. Check for mistakes.
Comment on lines +729 to +733
<div className="rls-dialog slide-up" onClick={(e) => e.stopPropagation()}>
<div className="rls-dialog-header">
<div className="rls-dialog-title-wrap">
<Shield size={18} />
<h3>RLS Settings</h3>
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The RLS dialog markup is missing basic dialog accessibility semantics (e.g., role="dialog", aria-modal="true", and an aria-labelledby pointing to the title). Without this, screen readers may not announce it as a modal and focus isn’t constrained. Add appropriate ARIA attributes and ensure focus is moved into the dialog on open and restored on close.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
const authHeader = req.header('Authorization');
req.authUser = null;

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

resolvePublicAuthContext runs JWT verification whenever an Authorization header is present, even for secret-key requests where RLS is bypassed. To avoid unnecessary JWT work and potential confusion, consider short-circuiting when req.keyRole === 'secret' (or only resolving req.authUser when the key role is publishable).

Suggested change
const authHeader = req.header('Authorization');
req.authUser = null;
req.authUser = null;
if (req.keyRole === 'secret') {
return next();
}
const authHeader = req.header('Authorization');

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +40
const ownerField = rls.ownerField || 'userId';
const authUserId = String(req.authUser.userId);
const method = String(req.method || '').toUpperCase();
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

authorizeWriteOperation computes authUserId = String(req.authUser.userId) unconditionally. If a collection is configured with rls.requireAuthForWrite: false, req.authUser may be null and this will throw, resulting in a 500 instead of a controlled 401/403. Either enforce auth for owner-write-only mode regardless of requireAuthForWrite, or guard this path so anonymous writes don’t crash the middleware.

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +163
rls: col.rls || {
enabled: false,
mode: 'owner-write-only',
ownerField: 'userId',
requireAuthForWrite: true
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

getSingleProject injects a default rls.ownerField: 'userId' for non-users collections regardless of the collection schema. If the schema doesn’t contain userId (e.g., it uses ownerId), the dashboard will show an invalid default and updateCollectionRls will reject saves with “ownerField not found in collection schema”. Use getDefaultRlsForCollection(col.name, col.model) (already defined) or derive the default from col.model here so the default ownerField is always schema-valid.

Copilot uses AI. Check for mistakes.
Comment on lines +983 to +987
rls: col.rls || {
enabled: false,
mode: 'owner-write-only',
ownerField: 'userId',
requireAuthForWrite: true
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Same issue as getSingleProject: toggleAuth returns rls.ownerField: 'userId' as the default for all non-users collections, even when that field isn’t present in the collection schema. This can surface an invalid default in the dashboard and cause the RLS save call to fail. Reuse getDefaultRlsForCollection(col.name, col.model) (or schema-derived defaults) when populating col.rls here.

Copilot uses AI. Check for mistakes.

const modelKeys = (collection.model || []).map(f => f.key);
const nextOwnerField = ownerField || collection?.rls?.ownerField || 'userId';

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

updateCollectionRls allows ownerField: '_id' for any collection (it bypasses the schema-key check). For non-users collections, selecting _id will effectively block inserts (public-api middleware explicitly denies POST when ownerField === '_id') and likely make updates/deletes impossible unless document _id equals the user id. Consider restricting _id to the users collection (or returning a clear 400 explaining why _id isn’t a valid ownerField for general collections).

Suggested change
// Restrict use of '_id' as ownerField to the 'users' collection only.
if (nextOwnerField === '_id' && collection.name !== 'users') {
return res.status(400).json({
error: "Invalid owner field",
message: "ownerField '_id' is only allowed for the 'users' collection"
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +104
rls: c.rls || {
enabled: false,
mode: "owner-write-only",
ownerField: c.name === 'users' ? '_id' : 'userId',
requireAuthForWrite: true
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Dashboard-side RLS defaults hardcode ownerField to 'userId' for every non-users collection when c.rls is missing. If the collection schema doesn’t include userId, this default is invalid and will cause the save request to fail. Derive the default ownerField from c.model (e.g., prefer userId, else ownerId, else force the user to choose) so the initial value is always valid for the schema.

Copilot uses AI. Check for mistakes.
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: 6

🧹 Nitpick comments (9)
apps/web-dashboard/src/pages/Database.jsx (4)

768-774: Minor: Raw backticks in JSX render as literal text.

The help text uses backticks that render as literal characters rather than styled code. Consider using <code> elements for better presentation.

📝 Suggested fix
 <p className="rls-help-text">
-  `userId` means the field in each document that stores the logged-in user's id.
-  Example: `posts.userId` or `orders.ownerId`. For the users collection, `_id` is usually the correct owner field.
+  <code>userId</code> means the field in each document that stores the logged-in user's id.
+  Example: <code>posts.userId</code> or <code>orders.ownerId</code>. For the users collection, <code>_id</code> is usually the correct owner field.
 </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web-dashboard/src/pages/Database.jsx` around lines 768 - 774, The help
text in Database.jsx (the JSX paragraph elements with className "rls-help-text")
contains raw backticks that render literally; replace inline backticks with
semantic <code> elements around identifiers like userId, posts.userId,
orders.ownerId and _id, and similarly wrap Authorization: Bearer
&lt;user_jwt&gt; in a <code> element so the text renders as code/monospace
(update the two <p className="rls-help-text"> blocks accordingly).

173-181: Fallback logic differs from initial load defaults.

The effect derives fallbackOwner by checking modelKeys.includes('userId') then 'ownerId', but the initial collection mapping at lines 98-106 always defaults to 'userId' for non-users collections. This inconsistency could cause the UI to show different values depending on whether the data came from initial load vs. subsequent activeCollection changes.

♻️ Suggested approach

Extract the fallback derivation into a shared helper to ensure consistency:

const deriveDefaultOwnerField = (collectionName, modelKeys) => {
  if (collectionName === 'users') return '_id';
  if (modelKeys.includes('userId')) return 'userId';
  if (modelKeys.includes('ownerId')) return 'ownerId';
  return 'userId';
};

Then use this helper in both the initial load mapping and the effect.

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

In `@apps/web-dashboard/src/pages/Database.jsx` around lines 173 - 181, The
fallback owner-field logic is inconsistent between the initial collection
mapping and the useEffect; extract the logic into a single helper (e.g.,
deriveDefaultOwnerField(collectionName, modelKeys)) that returns '_id' for
collectionName === 'users', 'userId' if modelKeys includes 'userId', 'ownerId'
if modelKeys includes 'ownerId', otherwise 'userId', then replace the inline
fallback computation in the useEffect and the initial collection mapping to call
deriveDefaultOwnerField and use its result when calling setRlsOwnerField or when
building the initial collection objects (referencing activeCollection,
setRlsOwnerField, and the initial mapping code path).

98-106: Potential inconsistency in default ownerField derivation between frontend and backend.

The frontend defaults ownerField to 'userId' for non-users collections without checking if the schema contains userId or ownerId. However, the backend helper getDefaultRlsForCollection (in project.controller.js lines 28-47) checks if schema keys include 'userId' first, then 'ownerId', then falls back to 'userId'.

This could lead to a mismatch where the frontend shows 'userId' but the backend persisted 'ownerId' if that was present in the schema. Consider aligning the logic or relying solely on the server response.

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

In `@apps/web-dashboard/src/pages/Database.jsx` around lines 98 - 106, The
frontend's withRlsDefaults logic sets ownerField to 'userId' unconditionally for
non-'users' collections which can diverge from the backend
getDefaultRlsForCollection behavior; update withRlsDefaults to mirror the
backend: inspect the collection schema (e.g., c.schema or c.fields) and set
ownerField to 'userId' if schema contains 'userId', otherwise 'ownerId' if
present, otherwise fallback to 'userId', or alternatively stop deriving and use
the server-provided RLS defaults directly from the response to ensure
consistency with getDefaultRlsForCollection in project.controller.js.

727-794: Missing keyboard trap and focus management for modal accessibility.

The RLS dialog lacks proper focus management. When opened, focus should move to the dialog, and pressing Escape should close it. Currently, only clicking the overlay or buttons closes it.

♿ Suggested accessibility improvements
+import { useEffect, useRef } from "react";

// Inside the component, add:
+const rlsDialogRef = useRef(null);
+
+useEffect(() => {
+  if (isRlsDialogOpen && rlsDialogRef.current) {
+    rlsDialogRef.current.focus();
+  }
+}, [isRlsDialogOpen]);

// In the dialog div:
-<div className="rls-dialog slide-up" onClick={(e) => e.stopPropagation()}>
+<div
+  ref={rlsDialogRef}
+  className="rls-dialog slide-up"
+  onClick={(e) => e.stopPropagation()}
+  onKeyDown={(e) => { if (e.key === 'Escape') setIsRlsDialogOpen(false); }}
+  tabIndex={-1}
+  role="dialog"
+  aria-modal="true"
+  aria-labelledby="rls-dialog-title"
+>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web-dashboard/src/pages/Database.jsx` around lines 727 - 794, The RLS
modal (rendered when isRlsDialogOpen) needs proper focus management and Escape
handling: when isRlsDialogOpen transitions true, move focus into the dialog (use
a ref for the dialog container and call focus on mount), trap tab focus inside
the dialog (ensure first/last tabbable wrap or use a small focus-trap utility)
and restore focus to the element that opened the dialog on close via
setIsRlsDialogOpen(false); also add a keydown handler on the dialog that listens
for Escape to call setIsRlsDialogOpen(false) and prevent propagation, and ensure
all interactive elements (Close button, Cancel, Save RLS) are reachable by
keyboard; use the existing dialog container element (class "rls-dialog") and
state functions (isRlsDialogOpen, setIsRlsDialogOpen, handleSaveRls) to
implement these changes.
docs/rls-implementation-roadmap.md (1)

180-182: Minor: Add period after "etc" for American English style.

📝 Fix
- Support column-level restrictions:
- - Publishable writes cannot modify protected fields (`role`, `plan`, `isAdmin`, etc).
+ - Publishable writes cannot modify protected fields (`role`, `plan`, `isAdmin`, etc.).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/rls-implementation-roadmap.md` around lines 180 - 182, Update the
markdown bullet "Support column-level restrictions" so the phrase "`role`,
`plan`, `isAdmin`, etc" ends with a period; locate the line containing that
bullet (the text "Publishable writes cannot modify protected fields (`role`,
`plan`, `isAdmin`, etc).") and ensure there is a period after "etc" to match
American English style.
apps/public-api/src/middlewares/authorizeWriteOperation.js (2)

72-74: Performance consideration: Document fetch on every write.

Each PUT/PATCH/DELETE requires a database query to verify ownership. For high-frequency updates, this adds latency.

Consider documenting this as a known V1 limitation in the roadmap. Future versions could explore caching strategies or optimistic checks for frequently updated documents.

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

In `@apps/public-api/src/middlewares/authorizeWriteOperation.js` around lines 72 -
74, The middleware currently performs a
Model.findById(id).select(ownerField).lean() on every write (via getConnection
and getCompiledModel), which adds latency for high-frequency PUT/PATCH/DELETE
operations; add a short note in the project roadmap/docs (and a TODO comment in
authorizeWriteOperation.js near the
getConnection/getCompiledModel/Model.findById usage) marking this as a known V1
limitation and propose future mitigations (caching owner checks, optimistic
concurrency, or batched ownership validation) so reviewers know this is
intentional and slated for improvement.

85-87: Edge case: ownerField immutability check doesn't apply to DELETE.

The check for ownerField tampering in req.body is inside the PUT/PATCH/DELETE block, but DELETE requests typically don't have a body. While not a bug (the condition just won't trigger for DELETE), consider restructuring for clarity:

♻️ Clearer structure
 if (method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
     // ... validation and doc fetch ...

-    if (ownerField === '_id') {
+    const isOwner = ownerField === '_id'
+        ? String(doc._id) === authUserId
+        : String(doc[ownerField]) === authUserId;
+
+    if (!isOwner) {
+        return res.status(403).json({ error: 'RLS owner mismatch', message: 'You can only modify your own document.' });
+    }
+
+    // Only check ownerField immutability on updates with body
+    if ((method === 'PUT' || method === 'PATCH') && ownerField !== '_id') {
+        if (req.body && Object.prototype.hasOwnProperty.call(req.body, ownerField) && String(req.body[ownerField]) !== String(doc[ownerField])) {
+            return res.status(403).json({ error: 'Owner field immutable', message: `${ownerField} cannot be changed under RLS.` });
+        }
-        if (String(doc._id) !== authUserId) {
-            return res.status(403).json({ error: 'RLS owner mismatch', message: 'You can only modify your own document.' });
-        }
-    } else {
-        if (req.body && Object.prototype.hasOwnProperty.call(req.body, ownerField) && String(req.body[ownerField]) !== String(doc[ownerField])) {
-            return res.status(403).json({ error: 'Owner field immutable', message: `${ownerField} cannot be changed under RLS.` });
-        }
-
-        if (doc[ownerField] === undefined || doc[ownerField] === null || String(doc[ownerField]) !== authUserId) {
-            return res.status(403).json({ error: 'RLS owner mismatch', message: 'You can only modify your own document.' });
-        }
     }

     return next();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/middlewares/authorizeWriteOperation.js` around lines 85 -
87, The ownerField immutability check currently lives inside the
PUT/PATCH/DELETE branch and should be applied only for methods that can carry a
body; update authorizeWriteOperation so the ownerField tampering check (the
condition using req.body, ownerField and doc[ownerField]) runs only for PUT and
PATCH (or when req.method !== 'DELETE'), or alternatively gate it explicitly by
checking req.method === 'PUT' || req.method === 'PATCH' before evaluating
req.body; this clarifies intent and avoids implying DELETE should be subject to
a body-based check while keeping the same protection for updates.
apps/dashboard-api/src/controllers/project.controller.js (2)

968-990: Same code duplication in toggleAuth response sanitization.

The toggleAuth function also has inline RLS defaults rather than using the helper function. Apply the same refactor as suggested for getSingleProject.

🤖 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 968 -
990, The toggleAuth response still duplicates inline RLS/defaults and model
sanitization; replace that block inside toggleAuth with the same helper used by
getSingleProject (the project collections sanitization helper used in
getSingleProject) so you don't recreate the default rls object or the users
model password filter inline. Specifically, refactor the mapping inside
toggleAuth to call the shared helper (the function getSingleProject uses for
applying default rls and stripping user password fields) and ensure toggleAuth
returns the sanitized projectObj rather than building RLS/model defaults
in-place.

143-166: Code duplication: RLS defaults are inline here instead of using the helper.

The getSingleProject function constructs RLS defaults inline rather than using getDefaultRlsForCollection. This creates maintenance burden and potential inconsistency if defaults change.

♻️ Suggested refactor
 if (projectObj.collections && Array.isArray(projectObj.collections)) {
     projectObj.collections = projectObj.collections.map(col => {
+        const defaultRls = getDefaultRlsForCollection(col.name, col.model);
         if (col.name === 'users' && col.model) {
             return {
                 ...col,
                 model: col.model.filter(m => m.key !== 'password'),
-                rls: col.rls || {
-                    enabled: false,
-                    mode: 'owner-write-only',
-                    ownerField: '_id',
-                    requireAuthForWrite: true
-                }
+                rls: col.rls || defaultRls
             };
         }
-        return {
-            ...col,
-            rls: col.rls || {
-                enabled: false,
-                mode: 'owner-write-only',
-                ownerField: 'userId',
-                requireAuthForWrite: true
-            }
-        };
+        return { ...col, rls: col.rls || defaultRls };
     });
 }
🤖 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 143 -
166, In getSingleProject, remove the duplicated inline RLS default objects
inside the projectObj.collections mapping and instead call the existing helper
getDefaultRlsForCollection to supply defaults; specifically, in the mapping for
each col replace rls: col.rls || { ...inline... } with rls: col.rls ||
getDefaultRlsForCollection(col.name), and for the special-case when col.name ===
'users' keep the model filtering (col.model.filter(m => m.key !== 'password'))
but still set rls via getDefaultRlsForCollection('users') (or merge/override
only specific fields if needed) so all RLS defaults come from the single helper.
🤖 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/public-api/src/controllers/userAuth.controller.js`:
- Around line 46-48: The fallback username generation in the payload branch
using payload.name || email.split('@')[0] can produce values outside the API's
3-50 character rule and isn't propagated to the value returned by
createAdminUser(); update the logic so the generated username is normalized to
the same validation rules (trim, enforce min length 3 and max length 50, and
sanitize/remove invalid chars per the API's username policy), set
payload.username to that canonicalized value, and ensure createAdminUser()
returns this same canonical username variable (not the original raw value) so
stored data and the response body remain consistent with the profile endpoint
checks; look for hasRequiredField, payload.username usage, and the
createAdminUser() return path to apply the fix.
- Around line 17-23: getVerificationField currently defaults to 'emailVerified'
even when usersColConfig.model (the declared field list) doesn't include any
supported verification key; change getVerificationField to return null (or
undefined) instead of 'emailVerified' when no matching keys are found so callers
don't act on an undeclared field. Update the function (getVerificationField) to
compute modelKeys from usersColConfig.model and if none of 'emailVerified',
'isVerified', or 'isverified' are present return null/undefined; then audit
callers (signup/admin-create/verifyEmail) to handle a null/undefined
verificationField gracefully (e.g., skip verification logic or surface an
explicit error).

In `@apps/public-api/src/middlewares/authorizeWriteOperation.js`:
- Around line 27-39: The code currently assumes req.authUser exists when setting
authUserId which can throw if no token is provided; modify the assignment of
authUserId in authorizeWriteOperation middleware to safely read req.authUser
(use optional chaining or a conditional) so authUserId becomes undefined/null
when req.authUser is missing, and ensure downstream logic that uses ownerField
and authUserId (rls.ownerField, authUserId) handles the absent user case (e.g.,
treat as anonymous or reject based on rls.requireAuthForWrite). Specifically,
update the use of req.authUser.userId to a guarded read (req.authUser?.userId)
and adjust any subsequent checks that assume authUserId is always a string.

In `@apps/public-api/src/middlewares/resolvePublicAuthContext.js`:
- Around line 4-12: Authorization header check currently requires 'Bearer ' with
exact casing; update the middleware to accept the scheme case-insensitively by
matching the header against a case-insensitive pattern (e.g., use a regex with
the i flag) to extract the token. Locate the authHeader handling in
resolvePublicAuthContext.js (variables/authHeader, token) and replace the
startsWith/slice logic with a case-insensitive match like /^\s*Bearer\s+(.+)$/i,
then set req.authUser processing to use the captured token only when the regex
matches; otherwise call next().

In `@docs/database.md`:
- Around line 18-24: Update the wording to make clear that the owner check under
RLS applies only to publishable-key writes: state that when using a publishable
key (x-api-key: pk_live_...) plus an Authorization: Bearer <user_jwt>, writes
are restricted to documents owned by the authenticated user based on the
configured ownerField; explicitly note that requests authenticated with a secret
key remain administrative and are not subject to the ownerField restriction so
secret-key behavior is preserved.

In `@packages/common/src/models/Project.js`:
- Around line 29-37: The rls.ownerField value is stored as an arbitrary string
which can alter query semantics; update the Project model to validate and
normalize ownerField before persistence by enforcing a safe field-name pattern
(e.g., allow only alphanumeric/underscore, no operator characters) and rejecting
or sanitizing invalid values, and implement logic in the Project schema
save/update path (e.g., a pre-save/pre-validate hook on the Project model) to
map the special-case collection ownerField value to '_id' where appropriate
(apply the users collection override at persistence time rather than only in
response shaping) so stored ownerField is always a validated, normalized
identifier used safely when building ownership filters.

---

Nitpick comments:
In `@apps/dashboard-api/src/controllers/project.controller.js`:
- Around line 968-990: The toggleAuth response still duplicates inline
RLS/defaults and model sanitization; replace that block inside toggleAuth with
the same helper used by getSingleProject (the project collections sanitization
helper used in getSingleProject) so you don't recreate the default rls object or
the users model password filter inline. Specifically, refactor the mapping
inside toggleAuth to call the shared helper (the function getSingleProject uses
for applying default rls and stripping user password fields) and ensure
toggleAuth returns the sanitized projectObj rather than building RLS/model
defaults in-place.
- Around line 143-166: In getSingleProject, remove the duplicated inline RLS
default objects inside the projectObj.collections mapping and instead call the
existing helper getDefaultRlsForCollection to supply defaults; specifically, in
the mapping for each col replace rls: col.rls || { ...inline... } with rls:
col.rls || getDefaultRlsForCollection(col.name), and for the special-case when
col.name === 'users' keep the model filtering (col.model.filter(m => m.key !==
'password')) but still set rls via getDefaultRlsForCollection('users') (or
merge/override only specific fields if needed) so all RLS defaults come from the
single helper.

In `@apps/public-api/src/middlewares/authorizeWriteOperation.js`:
- Around line 72-74: The middleware currently performs a
Model.findById(id).select(ownerField).lean() on every write (via getConnection
and getCompiledModel), which adds latency for high-frequency PUT/PATCH/DELETE
operations; add a short note in the project roadmap/docs (and a TODO comment in
authorizeWriteOperation.js near the
getConnection/getCompiledModel/Model.findById usage) marking this as a known V1
limitation and propose future mitigations (caching owner checks, optimistic
concurrency, or batched ownership validation) so reviewers know this is
intentional and slated for improvement.
- Around line 85-87: The ownerField immutability check currently lives inside
the PUT/PATCH/DELETE branch and should be applied only for methods that can
carry a body; update authorizeWriteOperation so the ownerField tampering check
(the condition using req.body, ownerField and doc[ownerField]) runs only for PUT
and PATCH (or when req.method !== 'DELETE'), or alternatively gate it explicitly
by checking req.method === 'PUT' || req.method === 'PATCH' before evaluating
req.body; this clarifies intent and avoids implying DELETE should be subject to
a body-based check while keeping the same protection for updates.

In `@apps/web-dashboard/src/pages/Database.jsx`:
- Around line 768-774: The help text in Database.jsx (the JSX paragraph elements
with className "rls-help-text") contains raw backticks that render literally;
replace inline backticks with semantic <code> elements around identifiers like
userId, posts.userId, orders.ownerId and _id, and similarly wrap Authorization:
Bearer &lt;user_jwt&gt; in a <code> element so the text renders as
code/monospace (update the two <p className="rls-help-text"> blocks
accordingly).
- Around line 173-181: The fallback owner-field logic is inconsistent between
the initial collection mapping and the useEffect; extract the logic into a
single helper (e.g., deriveDefaultOwnerField(collectionName, modelKeys)) that
returns '_id' for collectionName === 'users', 'userId' if modelKeys includes
'userId', 'ownerId' if modelKeys includes 'ownerId', otherwise 'userId', then
replace the inline fallback computation in the useEffect and the initial
collection mapping to call deriveDefaultOwnerField and use its result when
calling setRlsOwnerField or when building the initial collection objects
(referencing activeCollection, setRlsOwnerField, and the initial mapping code
path).
- Around line 98-106: The frontend's withRlsDefaults logic sets ownerField to
'userId' unconditionally for non-'users' collections which can diverge from the
backend getDefaultRlsForCollection behavior; update withRlsDefaults to mirror
the backend: inspect the collection schema (e.g., c.schema or c.fields) and set
ownerField to 'userId' if schema contains 'userId', otherwise 'ownerId' if
present, otherwise fallback to 'userId', or alternatively stop deriving and use
the server-provided RLS defaults directly from the response to ensure
consistency with getDefaultRlsForCollection in project.controller.js.
- Around line 727-794: The RLS modal (rendered when isRlsDialogOpen) needs
proper focus management and Escape handling: when isRlsDialogOpen transitions
true, move focus into the dialog (use a ref for the dialog container and call
focus on mount), trap tab focus inside the dialog (ensure first/last tabbable
wrap or use a small focus-trap utility) and restore focus to the element that
opened the dialog on close via setIsRlsDialogOpen(false); also add a keydown
handler on the dialog that listens for Escape to call setIsRlsDialogOpen(false)
and prevent propagation, and ensure all interactive elements (Close button,
Cancel, Save RLS) are reachable by keyboard; use the existing dialog container
element (class "rls-dialog") and state functions (isRlsDialogOpen,
setIsRlsDialogOpen, handleSaveRls) to implement these changes.

In `@docs/rls-implementation-roadmap.md`:
- Around line 180-182: Update the markdown bullet "Support column-level
restrictions" so the phrase "`role`, `plan`, `isAdmin`, etc" ends with a period;
locate the line containing that bullet (the text "Publishable writes cannot
modify protected fields (`role`, `plan`, `isAdmin`, etc).") and ensure there is
a period after "etc" to match American English style.
🪄 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: CHILL

Plan: Pro

Run ID: c3487b1d-b602-4f99-83c5-a4461cf62b35

📥 Commits

Reviewing files that changed from the base of the PR and between 712dbbe and c7723f4.

⛔ Files ignored due to path filters (2)
  • apps/web-dashboard/dist/assets/index-Bu_-Dcm1.js is excluded by !**/dist/**
  • apps/web-dashboard/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (14)
  • README.md
  • apps/dashboard-api/src/controllers/project.controller.js
  • apps/dashboard-api/src/controllers/userAuth.controller.js
  • apps/dashboard-api/src/routes/projects.js
  • apps/public-api/src/controllers/userAuth.controller.js
  • apps/public-api/src/middlewares/authorizeWriteOperation.js
  • apps/public-api/src/middlewares/resolvePublicAuthContext.js
  • apps/public-api/src/routes/data.js
  • apps/web-dashboard/src/pages/Database.jsx
  • docs/api-reference.md
  • docs/database.md
  • docs/getting-started.md
  • docs/rls-implementation-roadmap.md
  • packages/common/src/models/Project.js

Comment on lines +17 to +23
const getVerificationField = (usersColConfig) => {
const modelKeys = (usersColConfig?.model || []).map((f) => f?.key);
if (modelKeys.includes('emailVerified')) return 'emailVerified';
if (modelKeys.includes('isVerified')) return 'isVerified';
if (modelKeys.includes('isverified')) return 'isverified';
return 'emailVerified';
};
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

Don't fall back to an undeclared verification field.

packages/common/src/models/Project.js, Lines 26-39 makes usersColConfig.model the declared field list. If none of the supported keys exist, returning 'emailVerified' here lets signup/admin-create/verifyEmail target a path the collection never defined; on strict models that can turn email verification into a false-positive success.

Proposed fix
 const getVerificationField = (usersColConfig) => {
-    const modelKeys = (usersColConfig?.model || []).map((f) => f?.key);
-    if (modelKeys.includes('emailVerified')) return 'emailVerified';
-    if (modelKeys.includes('isVerified')) return 'isVerified';
-    if (modelKeys.includes('isverified')) return 'isverified';
-    return 'emailVerified';
+    const verificationField = (usersColConfig?.model || []).find(
+        (f) =>
+            ['emailVerified', 'isVerified', 'isverified'].includes(f?.key) &&
+            f?.type === 'Boolean'
+    );
+    if (!verificationField) {
+        throw new Error("Users collection must define a boolean verification field.");
+    }
+    return verificationField.key;
 };
📝 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 getVerificationField = (usersColConfig) => {
const modelKeys = (usersColConfig?.model || []).map((f) => f?.key);
if (modelKeys.includes('emailVerified')) return 'emailVerified';
if (modelKeys.includes('isVerified')) return 'isVerified';
if (modelKeys.includes('isverified')) return 'isverified';
return 'emailVerified';
};
const getVerificationField = (usersColConfig) => {
const verificationField = (usersColConfig?.model || []).find(
(f) =>
['emailVerified', 'isVerified', 'isverified'].includes(f?.key) &&
f?.type === 'Boolean'
);
if (!verificationField) {
throw new Error("Users collection must define a boolean verification field.");
}
return verificationField.key;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/controllers/userAuth.controller.js` around lines 17 - 23,
getVerificationField currently defaults to 'emailVerified' even when
usersColConfig.model (the declared field list) doesn't include any supported
verification key; change getVerificationField to return null (or undefined)
instead of 'emailVerified' when no matching keys are found so callers don't act
on an undeclared field. Update the function (getVerificationField) to compute
modelKeys from usersColConfig.model and if none of 'emailVerified',
'isVerified', or 'isverified' are present return null/undefined; then audit
callers (signup/admin-create/verifyEmail) to handle a null/undefined
verificationField gracefully (e.g., skip verification logic or surface an
explicit error).

Comment on lines +46 to +48
if (hasRequiredField(usersColConfig, 'username') && (payload.username === undefined || payload.username === null || payload.username === '')) {
payload.username = payload.name || email.split('@')[0];
}
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

Keep generated usernames consistent with the rest of the API.

This fallback can produce values outside the 3-50 character rule enforced in Lines 389-393 (a@... -> a), and createAdminUser() still returns the raw username variable on Line 220 instead of the generated one. That leaves you with stored data the profile endpoint would later reject and a response body that can disagree with the document.

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

In `@apps/public-api/src/controllers/userAuth.controller.js` around lines 46 - 48,
The fallback username generation in the payload branch using payload.name ||
email.split('@')[0] can produce values outside the API's 3-50 character rule and
isn't propagated to the value returned by createAdminUser(); update the logic so
the generated username is normalized to the same validation rules (trim, enforce
min length 3 and max length 50, and sanitize/remove invalid chars per the API's
username policy), set payload.username to that canonicalized value, and ensure
createAdminUser() returns this same canonical username variable (not the
original raw value) so stored data and the response body remain consistent with
the profile endpoint checks; look for hasRequiredField, payload.username usage,
and the createAdminUser() return path to apply the fix.

Comment on lines +27 to +39
if (rls.requireAuthForWrite && !req.authUser?.userId) {
return res.status(401).json({
error: 'Authentication required',
message: 'Provide a valid user Bearer token for write operations.'
});
}

if ((rls.mode || 'owner-write-only') !== 'owner-write-only') {
return res.status(403).json({ error: 'Unsupported RLS mode' });
}

const ownerField = rls.ownerField || 'userId';
const authUserId = String(req.authUser.userId);
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 | 🔴 Critical

Critical: Potential crash when requireAuthForWrite is false but RLS is enabled.

If rls.requireAuthForWrite is false, the auth check on line 27 is skipped, but line 39 still accesses req.authUser.userId unconditionally. This will throw a TypeError if no Bearer token was provided.

🔧 Proposed fix
 if (rls.requireAuthForWrite && !req.authUser?.userId) {
     return res.status(401).json({
         error: 'Authentication required',
         message: 'Provide a valid user Bearer token for write operations.'
     });
 }

+// Even if requireAuthForWrite is false, owner-write-only mode needs a userId
+if (!req.authUser?.userId) {
+    return res.status(401).json({
+        error: 'Authentication required',
+        message: 'Owner-write-only mode requires a valid user Bearer token.'
+    });
+}
+
 if ((rls.mode || 'owner-write-only') !== 'owner-write-only') {
     return res.status(403).json({ error: 'Unsupported RLS mode' });
 }

 const ownerField = rls.ownerField || 'userId';
 const authUserId = String(req.authUser.userId);

Alternatively, if the intent is to allow anonymous writes when requireAuthForWrite: false, then the entire owner-write-only logic should be conditional, which would be a design change.

📝 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
if (rls.requireAuthForWrite && !req.authUser?.userId) {
return res.status(401).json({
error: 'Authentication required',
message: 'Provide a valid user Bearer token for write operations.'
});
}
if ((rls.mode || 'owner-write-only') !== 'owner-write-only') {
return res.status(403).json({ error: 'Unsupported RLS mode' });
}
const ownerField = rls.ownerField || 'userId';
const authUserId = String(req.authUser.userId);
if (rls.requireAuthForWrite && !req.authUser?.userId) {
return res.status(401).json({
error: 'Authentication required',
message: 'Provide a valid user Bearer token for write operations.'
});
}
// Even if requireAuthForWrite is false, owner-write-only mode needs a userId
if (!req.authUser?.userId) {
return res.status(401).json({
error: 'Authentication required',
message: 'Owner-write-only mode requires a valid user Bearer token.'
});
}
if ((rls.mode || 'owner-write-only') !== 'owner-write-only') {
return res.status(403).json({ error: 'Unsupported RLS mode' });
}
const ownerField = rls.ownerField || 'userId';
const authUserId = String(req.authUser.userId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/middlewares/authorizeWriteOperation.js` around lines 27 -
39, The code currently assumes req.authUser exists when setting authUserId which
can throw if no token is provided; modify the assignment of authUserId in
authorizeWriteOperation middleware to safely read req.authUser (use optional
chaining or a conditional) so authUserId becomes undefined/null when
req.authUser is missing, and ensure downstream logic that uses ownerField and
authUserId (rls.ownerField, authUserId) handles the absent user case (e.g.,
treat as anonymous or reject based on rls.requireAuthForWrite). Specifically,
update the use of req.authUser.userId to a guarded read (req.authUser?.userId)
and adjust any subsequent checks that assume authUserId is always a string.

Comment on lines +4 to +12
const authHeader = req.header('Authorization');
req.authUser = null;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next();
}

const token = authHeader.slice(7).trim();
if (!token) return next();
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

🧩 Analysis chain

🏁 Script executed:

cat -n apps/public-api/src/middlewares/resolvePublicAuthContext.js | head -30

Repository: yash-pouranik/urBackend

Length of output: 834


🌐 Web query:

HTTP Authorization header scheme case sensitivity RFC

💡 Result:

The HTTP Authorization header scheme (auth-scheme) is case-insensitive according to the relevant RFCs. RFC 7235 Section 2.1 (HTTP/1.1 Authentication) explicitly states: "It uses a case-insensitive token as a means to identify the authentication scheme". This is reaffirmed in RFC 9110 (HTTP Semantics, June 2022), Section 11.1: "It uses a case-insensitive token to identify the authentication scheme: auth-scheme = token". Parameter names within authentication are also matched case-insensitively. HTTP header field names themselves are case-insensitive per RFC 7230/9112. This applies to both WWW-Authenticate challenges and Authorization credentials. Specific schemes like Basic (RFC 7617) and Bearer note case-insensitive matching for scheme and parameters. Despite this, some implementations incorrectly treat it as case-sensitive, leading to interoperability issues.

Citations:


Accept case-insensitive Bearer schemes.

Authorization schemes are case-insensitive per RFC 7235/9110, so bearer <token> or other case variations currently fall through as anonymous and can break otherwise valid authentication flows depending on the client.

♻️ Proposed fix
 module.exports = (req, res, next) => {
     const authHeader = req.header('Authorization');
     req.authUser = null;
 
-    if (!authHeader || !authHeader.startsWith('Bearer ')) {
+    if (!authHeader) {
         return next();
     }
 
-    const token = authHeader.slice(7).trim();
-    if (!token) return next();
+    const [scheme, token = ''] = authHeader.trim().split(/\s+/, 2);
+    if (!/^Bearer$/i.test(scheme) || !token) {
+        return next();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/middlewares/resolvePublicAuthContext.js` around lines 4 -
12, Authorization header check currently requires 'Bearer ' with exact casing;
update the middleware to accept the scheme case-insensitively by matching the
header against a case-insensitive pattern (e.g., use a regex with the i flag) to
extract the token. Locate the authHeader handling in resolvePublicAuthContext.js
(variables/authHeader, token) and replace the startsWith/slice logic with a
case-insensitive match like /^\s*Bearer\s+(.+)$/i, then set req.authUser
processing to use the captured token only when the regex matches; otherwise call
next().

Comment thread docs/database.md
Comment on lines +18 to +24
If you enable **RLS (owner-write-only)** for a collection from the dashboard, publishable-key writes are also allowed but must include a valid user token:

- `x-api-key: pk_live_...`
- `Authorization: Bearer <user_jwt>`

Under RLS, writes are permitted only for documents owned by the authenticated user (based on configured `ownerField`).

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

Scope the owner check to the publishable-key exception path.

Lines 18-24 currently read as if all writes become owner-restricted once RLS is enabled, which conflicts with the PR goal of preserving secret-key admin behavior. Please clarify that this owner check applies to publishable-key writes, while secret-key writes remain administrative.

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

In `@docs/database.md` around lines 18 - 24, Update the wording to make clear that
the owner check under RLS applies only to publishable-key writes: state that
when using a publishable key (x-api-key: pk_live_...) plus an Authorization:
Bearer <user_jwt>, writes are restricted to documents owned by the authenticated
user based on the configured ownerField; explicitly note that requests
authenticated with a secret key remain administrative and are not subject to the
ownerField restriction so secret-key behavior is preserved.

Comment on lines +29 to +37
rls: {
enabled: { type: Boolean, default: false },
mode: {
type: String,
enum: ['owner-write-only'],
default: 'owner-write-only'
},
ownerField: { type: String, default: 'userId' },
requireAuthForWrite: { type: Boolean, default: true }
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

Normalize and validate ownerField before storing it.

ownerField is part of the RLS security boundary, because it is later reused to build the ownership filter. Leaving it as an arbitrary string here means operator-like keys can change query semantics, and the generic 'userId' default still doesn't cover the users collection special case called out in this PR. Please enforce a safe field name and apply the _id override at persistence time instead of only in response shaping.

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

In `@packages/common/src/models/Project.js` around lines 29 - 37, The
rls.ownerField value is stored as an arbitrary string which can alter query
semantics; update the Project model to validate and normalize ownerField before
persistence by enforcing a safe field-name pattern (e.g., allow only
alphanumeric/underscore, no operator characters) and rejecting or sanitizing
invalid values, and implement logic in the Project schema save/update path
(e.g., a pre-save/pre-validate hook on the Project model) to map the
special-case collection ownerField value to '_id' where appropriate (apply the
users collection override at persistence time rather than only in response
shaping) so stored ownerField is always a validated, normalized identifier used
safely when building ownership filters.

@yash-pouranik
Copy link
Copy Markdown
Collaborator Author

@copilot apply changes based on the comments in this thread

…rd, accessibility

Agent-Logs-Url: https://github.com/yash-pouranik/urBackend/sessions/b3c82297-16e2-48e7-9af8-ba7992227aa8

Co-authored-by: yash-pouranik <172860064+yash-pouranik@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 28, 2026

@copilot apply changes based on the comments in this thread

Applied all review feedback in commit 864904e:

  • resolvePublicAuthContext.js: Short-circuits immediately when req.keyRole === 'secret' to avoid unnecessary JWT verification.
  • authorizeWriteOperation.js: Added a null-guard for req.authUser before accessing .userId, so a missing/unauthenticated user always gets a clean 401 instead of a 500 crash.
  • project.controller.js (getSingleProject & toggleAuth): Both now use getDefaultRlsForCollection(col.name, col.model) for the fallback RLS config instead of hardcoding ownerField: 'userId', ensuring the default is always schema-valid.
  • project.controller.js (updateCollectionRls): Added a check to reject ownerField: '_id' for any collection other than users with a clear 400 error.
  • Database.jsx RLS defaults: ownerField is now derived from the collection schema (prefers userId, then ownerId, then the first schema key) rather than hardcoding 'userId'.
  • Database.jsx rlsOwnerFieldOptions: Now only shows fields actually present in the collection schema (_id is included only for the users collection), preventing selection of invalid owner fields.
  • Database.jsx RLS dialog: Added role="dialog", aria-modal="true", and aria-labelledby="rls-dialog-title" for screen reader accessibility.

@yash-pouranik
Copy link
Copy Markdown
Collaborator Author

@coderabbitai now please test all updates may i merge?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me examine the PR review comments and the current state of the code before giving you a merge recommendation.


🏁 Script executed:

#!/bin/bash
# Fetch PR review comments
gh pr view 51 --repo yash-pouranik/urBackend --json reviews,comments,reviewThreads 2>/dev/null | jq '.reviews[] | {state, body: .body[:500]}' 2>/dev/null || echo "Fallback"

gh api repos/yash-pouranik/urBackend/pulls/51/reviews 2>/dev/null | jq '.[] | {id, state, body: .body[:600]}'

Length of output: 2014


🏁 Script executed:

#!/bin/bash
# Fetch review comments (inline)
gh api repos/yash-pouranik/urBackend/pulls/51/comments 2>/dev/null | jq '.[] | {path, line, body: .body[:600], user: .user.login}'

Length of output: 12355


Let me check the current state of the critical files to see if the flagged issues were addressed.


🏁 Script executed:

#!/bin/bash
# Read the current state of the critical middleware file
cat -n apps/public-api/src/middlewares/authorizeWriteOperation.js

echo "---"
cat -n apps/public-api/src/middlewares/resolvePublicAuthContext.js

echo "---"
# Check userAuth controller for the verification field fix
cat -n apps/public-api/src/controllers/userAuth.controller.js | head -80

echo "---"
# Check project controller for the ownerField default fix
cat -n apps/dashboard-api/src/controllers/project.controller.js | grep -n "userId\|ownerField\|getDefaultRls" | head -40

Length of output: 10824


🏁 Script executed:

#!/bin/bash
# Check Database.jsx for accessibility fixes and ownerField options
grep -n "role=\|aria-\|rlsOwnerFieldOptions\|ownerField\|rls\." apps/web-dashboard/src/pages/Database.jsx | head -60

echo "=== DASHBOARD USERAUTH ==="
cat -n apps/dashboard-api/src/controllers/userAuth.controller.js | grep -n "getVerificationField\|emailVerified\|isVerified" | head -20

echo "=== CHECK getVerificationField in DASHBOARD ==="
grep -n "getVerificationField\|emailVerified" apps/dashboard-api/src/controllers/userAuth.controller.js | head -20

echo "=== PROJECT MODEL ownerField ==="
cat -n packages/common/src/models/Project.js | grep -n "ownerField\|validate\|match\|required" | head -20

Length of output: 1879


🏁 Script executed:

#!/bin/bash
# Check the full rlsOwnerFieldOptions implementation
sed -n '95,125p' apps/web-dashboard/src/pages/Database.jsx
echo "---"
sed -n '228,250p' apps/web-dashboard/src/pages/Database.jsx

Length of output: 1894


@yash-pouranik Here's my merge readiness assessment after checking the current state of all flagged files:


✅ Issues That Were Fixed

Issue Severity Status
Crash in authorizeWriteOperation.js: req.authUser.userId accessed when authUser is null 🔴 Critical ✅ Fixed — unconditional !req.authUser?.userId guard added at Line 40
resolvePublicAuthContext runs JWT work for secret-key requests 🟡 Minor ✅ Fixed — if (req.keyRole === 'secret') return next() added
RLS dialog missing role, aria-modal, aria-labelledby 🟡 Minor ✅ Fixed — all ARIA attributes present
rlsOwnerFieldOptions offered fields not in schema 🟡 Minor ✅ Fixed — now derived from schema keys; _id only for users
getSingleProject / toggleAuth hardcoded ownerField: 'userId' default 🟠 Major ✅ Fixed — now uses getDefaultRlsForCollection() helper
updateCollectionRls allowed _id as ownerField for non-users collections 🟡 Minor ✅ Fixed — restricted to users collection only
Dashboard-side ownerField defaults not schema-derived 🟡 Minor ✅ Fixed — derives from schema keys with priority fallback

⚠️ Remaining Open Issues

1. 🟠 Major — getVerificationField fallback in both userAuth controllers

Both apps/public-api/src/controllers/userAuth.controller.js and apps/dashboard-api/src/controllers/userAuth.controller.js still have:

// Line 22 in both files
return 'emailVerified'; // ← fallback when field isn't in schema

If a project's users collection defines none of emailVerified, isVerified, or isverified, this silently writes to a phantom field — users would appear unverified forever, or a stale field in the DB is set with no effect. The fix (throw or return null and guard the caller) was flagged as Major in the review and is still outstanding:

const getVerificationField = (usersColConfig) => {
    const modelKeys = (usersColConfig?.model || []).map((f) => f?.key);
    if (modelKeys.includes('emailVerified')) return 'emailVerified';
    if (modelKeys.includes('isVerified')) return 'isVerified';
    if (modelKeys.includes('isverified')) return 'isverified';
-   return 'emailVerified'; // ← dangerous fallback
+   return null; // caller must handle null
};

2. 🟡 Minor — Bearer scheme check is case-sensitive

In resolvePublicAuthContext.js Line 12, authHeader.startsWith('Bearer ') will miss bearer <token> or BEARER <token>. Per RFC 7235, auth-scheme is case-insensitive:

- if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ if (!authHeader || !/^bearer\s/i.test(authHeader)) {

3. 🟡 Minor — Auto-generated username can be shorter than 3 chars

In buildAuthUserPayload, the fallback email.split('@')[0] (e.g., a@b.com"a") can produce a 1-character username that the validation rules at Lines 389–393 would reject in other contexts.


Merge Recommendation

Not yet ready to merge due to the getVerificationField fallback issue (item 1 above). It's a silent data-correctness bug — no crash, but email verification would silently fail for non-standard schema configurations, which could block all new user signups if email verification is enabled. The fix is a one-liner in two files.

Items 2 and 3 are low risk and can be addressed in a follow-up if you prefer to keep this PR focused. Would you like me to open a follow-up issue for those?

@yash-pouranik
Copy link
Copy Markdown
Collaborator Author

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain
Let me examine the PR review comments and the current state of the code before giving you a merge recommendation.

🏁 Script executed:

#!/bin/bash
# Fetch PR review comments
gh pr view 51 --repo yash-pouranik/urBackend --json reviews,comments,reviewThreads 2>/dev/null | jq '.reviews[] | {state, body: .body[:500]}' 2>/dev/null || echo "Fallback"

gh api repos/yash-pouranik/urBackend/pulls/51/reviews 2>/dev/null | jq '.[] | {id, state, body: .body[:600]}'

Length of output: 2014

🏁 Script executed:

#!/bin/bash
# Fetch review comments (inline)
gh api repos/yash-pouranik/urBackend/pulls/51/comments 2>/dev/null | jq '.[] | {path, line, body: .body[:600], user: .user.login}'

Length of output: 12355

Let me check the current state of the critical files to see if the flagged issues were addressed.

🏁 Script executed:

#!/bin/bash
# Read the current state of the critical middleware file
cat -n apps/public-api/src/middlewares/authorizeWriteOperation.js

echo "---"
cat -n apps/public-api/src/middlewares/resolvePublicAuthContext.js

echo "---"
# Check userAuth controller for the verification field fix
cat -n apps/public-api/src/controllers/userAuth.controller.js | head -80

echo "---"
# Check project controller for the ownerField default fix
cat -n apps/dashboard-api/src/controllers/project.controller.js | grep -n "userId\|ownerField\|getDefaultRls" | head -40

Length of output: 10824

🏁 Script executed:

#!/bin/bash
# Check Database.jsx for accessibility fixes and ownerField options
grep -n "role=\|aria-\|rlsOwnerFieldOptions\|ownerField\|rls\." apps/web-dashboard/src/pages/Database.jsx | head -60

echo "=== DASHBOARD USERAUTH ==="
cat -n apps/dashboard-api/src/controllers/userAuth.controller.js | grep -n "getVerificationField\|emailVerified\|isVerified" | head -20

echo "=== CHECK getVerificationField in DASHBOARD ==="
grep -n "getVerificationField\|emailVerified" apps/dashboard-api/src/controllers/userAuth.controller.js | head -20

echo "=== PROJECT MODEL ownerField ==="
cat -n packages/common/src/models/Project.js | grep -n "ownerField\|validate\|match\|required" | head -20

Length of output: 1879

🏁 Script executed:

#!/bin/bash
# Check the full rlsOwnerFieldOptions implementation
sed -n '95,125p' apps/web-dashboard/src/pages/Database.jsx
echo "---"
sed -n '228,250p' apps/web-dashboard/src/pages/Database.jsx

Length of output: 1894

@yash-pouranik Here's my merge readiness assessment after checking the current state of all flagged files:

✅ Issues That Were Fixed

Issue Severity Status
Crash in authorizeWriteOperation.js: req.authUser.userId accessed when authUser is null 🔴 Critical ✅ Fixed — unconditional !req.authUser?.userId guard added at Line 40
resolvePublicAuthContext runs JWT work for secret-key requests 🟡 Minor ✅ Fixed — if (req.keyRole === 'secret') return next() added
RLS dialog missing role, aria-modal, aria-labelledby 🟡 Minor ✅ Fixed — all ARIA attributes present
rlsOwnerFieldOptions offered fields not in schema 🟡 Minor ✅ Fixed — now derived from schema keys; _id only for users
getSingleProject / toggleAuth hardcoded ownerField: 'userId' default 🟠 Major ✅ Fixed — now uses getDefaultRlsForCollection() helper
updateCollectionRls allowed _id as ownerField for non-users collections 🟡 Minor ✅ Fixed — restricted to users collection only
Dashboard-side ownerField defaults not schema-derived 🟡 Minor ✅ Fixed — derives from schema keys with priority fallback

⚠️ Remaining Open Issues

1. 🟠 Major — getVerificationField fallback in both userAuth controllers

Both apps/public-api/src/controllers/userAuth.controller.js and apps/dashboard-api/src/controllers/userAuth.controller.js still have:

// Line 22 in both files
return 'emailVerified'; // ← fallback when field isn't in schema

If a project's users collection defines none of emailVerified, isVerified, or isverified, this silently writes to a phantom field — users would appear unverified forever, or a stale field in the DB is set with no effect. The fix (throw or return null and guard the caller) was flagged as Major in the review and is still outstanding:

const getVerificationField = (usersColConfig) => {
    const modelKeys = (usersColConfig?.model || []).map((f) => f?.key);
    if (modelKeys.includes('emailVerified')) return 'emailVerified';
    if (modelKeys.includes('isVerified')) return 'isVerified';
    if (modelKeys.includes('isverified')) return 'isverified';
-   return 'emailVerified'; // ← dangerous fallback
+   return null; // caller must handle null
};

2. 🟡 Minor — Bearer scheme check is case-sensitive

In resolvePublicAuthContext.js Line 12, authHeader.startsWith('Bearer ') will miss bearer <token> or BEARER <token>. Per RFC 7235, auth-scheme is case-insensitive:

- if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ if (!authHeader || !/^bearer\s/i.test(authHeader)) {

3. 🟡 Minor — Auto-generated username can be shorter than 3 chars

In buildAuthUserPayload, the fallback email.split('@')[0] (e.g., a@b.com"a") can produce a 1-character username that the validation rules at Lines 389–393 would reject in other contexts.

Merge Recommendation

Not yet ready to merge due to the getVerificationField fallback issue (item 1 above). It's a silent data-correctness bug — no crash, but email verification would silently fail for non-standard schema configurations, which could block all new user signups if email verification is enabled. The fix is a one-liner in two files.

Items 2 and 3 are low risk and can be addressed in a follow-up if you prefer to keep this PR focused. Would you like me to open a follow-up issue for those?

@coderabbitai, yes create a followup issue for item 2 and 3, till then I fix 1

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Sure, @yash-pouranik! Creating both follow-up issues now.


🏁 Script executed:

#!/bin/bash
# Create issue for item 2: Case-insensitive Bearer scheme check
gh issue create \
  --repo yash-pouranik/urBackend \
  --title "fix: case-insensitive Bearer scheme check in resolvePublicAuthContext" \
  --body "## Summary

In \`apps/public-api/src/middlewares/resolvePublicAuthContext.js\` (Line 12), the \`Authorization\` header check uses \`authHeader.startsWith('Bearer ')\`, which is case-sensitive and will silently ignore valid tokens sent with \`bearer <token>\` or \`BEARER <token>\`.

Per [RFC 7235 §2.1](https://datatracker.ietf.org/doc/html/rfc7235#section-2.1), the auth-scheme token is case-insensitive.

## Suggested Fix

\`\`\`js
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ if (!authHeader || !/^bearer\s/i.test(authHeader)) {
\`\`\`

And extract the token accordingly:
\`\`\`js
- const token = authHeader.split(' ')[1];
+ const token = authHeader.replace(/^bearer\s+/i, '');
\`\`\`

## Context

Identified during review of PR `#51` (https://github.com/yash-pouranik/urBackend/pull/51#issuecomment-0) as a minor issue deferred for a follow-up.

Requested by `@yash-pouranik`." \
  --assignee yash-pouranik

echo "---"

# Create issue for item 3: Short auto-generated username
gh issue create \
  --repo yash-pouranik/urBackend \
  --title "fix: auto-generated username from email prefix can be shorter than 3 chars" \
  --body "## Summary

In both \`apps/public-api/src/controllers/userAuth.controller.js\` and \`apps/dashboard-api/src/controllers/userAuth.controller.js\`, the \`buildAuthUserPayload\` helper falls back to \`email.split('@')[0]\` to generate a username when the field is required but not provided.

This can produce a username as short as 1 character (e.g., \`a@b.com\` → \`\"a\"\`), which may fail validation rules elsewhere in the codebase that enforce a minimum length (e.g., Lines 389–393 in the controller).

## Suggested Fix

Pad the auto-generated username to meet the minimum length requirement:

\`\`\`js
const generatedUsername = email.split('@')[0];
const username = generatedUsername.length >= 3
    ? generatedUsername
    : generatedUsername.padEnd(3, '0');
\`\`\`

Or alternatively, enforce a minimum length check and return a \`400\` error asking the client to supply a valid username.

## Context

Identified during review of PR `#51` (https://github.com/yash-pouranik/urBackend/pull/51#issuecomment-0) as a minor issue deferred for a follow-up.

Requested by `@yash-pouranik`." \
  --assignee yash-pouranik

Length of output: 109


@yash-pouranik Both follow-up issues have been created and assigned to you:

Once you've fixed item 1 (getVerificationField returning null instead of the phantom 'emailVerified' fallback in both userAuth controllers), this PR should be good to merge. Let me know when you're ready for a final check! 🐇

@yash-pouranik
Copy link
Copy Markdown
Collaborator Author

@copilot apply changes based on the comment in #51 (review)

work on the last comment by @coderabbitai and the first one only, i mentioned

'
⚠️ Remaining Open Issues

  1. 🟠 Major — getVerificationField fallback in both userAuth controllers
    Both apps/public-api/src/controllers/userAuth.controller.js and apps/dashboard-api/src/controllers/userAuth.controller.js still have:

// Line 22 in both files
return 'emailVerified'; // ← fallback when field isn't in schema
If a project's users collection defines none of emailVerified, isVerified, or isverified, this silently writes to a phantom field — users would appear unverified forever, or a stale field in the DB is set with no effect. The fix (throw or return null and guard the caller) was flagged as Major in the review and is still outstanding:

const getVerificationField = (usersColConfig) => {
const modelKeys = (usersColConfig?.model || []).map((f) => f?.key);
if (modelKeys.includes('emailVerified')) return 'emailVerified';
if (modelKeys.includes('isVerified')) return 'isVerified';
if (modelKeys.includes('isverified')) return 'isverified';

  • return 'emailVerified'; // ← dangerous fallback
  • return null; // caller must handle null
    };
    '

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Task: Fix getVerificationField null fallback in both userAuth controllers

  • Change return 'emailVerified' to return null in getVerificationField in both files
  • Guard every call site that uses the return value so that null causes an early 400/500 response or skips the update cleanly
  • Both files have an identical change pattern

🏁 Script executed:

#!/bin/bash
cat -n apps/public-api/src/controllers/userAuth.controller.js
echo "==========================="
cat -n apps/dashboard-api/src/controllers/userAuth.controller.js

Length of output: 45665


🏁 Script executed:

cat -n apps/public-api/src/controllers/userAuth.controller.js

Length of output: 23176


🏁 Script executed:

cat -n apps/dashboard-api/src/controllers/userAuth.controller.js

Length of output: 22460


@yash-pouranik Here are the exact changes needed for both files. The diff is identical for apps/public-api/src/controllers/userAuth.controller.js and apps/dashboard-api/src/controllers/userAuth.controller.js.

Task: Fix getVerificationField phantom-field fallback

  • Fix getVerificationField to return null instead of the phantom fallback 'emailVerified'
  • Guard buildAuthUserPayload — skip setting the verification field when schema has none
  • Guard verifyEmail — return 500 early when the verification field is not found in schema

Summary of what each change does:

Location Change Why
getVerificationField fallback return null Stops returning a phantom field name for schemas that define none of the known verification keys
buildAuthUserPayload Skip payload[verificationField] assignment when null Prevents writing payload['null'] = false/true during signup/admin-create
verifyEmail Return 500 when verificationField is null Prevents silently writing { $set: { null: true } } to the database; gives a clear error instead

Failed to handle agent chat message. Please try again.

…ema field

Agent-Logs-Url: https://github.com/yash-pouranik/urBackend/sessions/8ffa16aa-260a-441d-a417-eb2734631188

Co-authored-by: yash-pouranik <172860064+yash-pouranik@users.noreply.github.com>
@yash-pouranik yash-pouranik temporarily deployed to rls/v1-write-on-ur-own-doc - urBackend-frankfrut PR #51 March 28, 2026 08:47 — with Render Destroyed
Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 28, 2026

@copilot apply changes based on the comment in #51 (review)

work on the last comment by @coderabbitai and the first one only, i me...

Fixed in commit 7fd6ace. getVerificationField now returns null instead of the hardcoded 'emailVerified' fallback in both apps/public-api/src/controllers/userAuth.controller.js and apps/dashboard-api/src/controllers/userAuth.controller.js. Both call sites are guarded:

  • buildAuthUserPayload — skips setting the verification field entirely when null, so no phantom field is written to new user documents.
  • verifyEmail handler — returns a clear 500 error instead of silently updating a non-existent field.

@yash-pouranik
Copy link
Copy Markdown
Collaborator Author

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain
Let me examine the PR review comments and the current state of the code before giving you a merge recommendation.

🏁 Script executed:

#!/bin/bash
# Fetch PR review comments
gh pr view 51 --repo yash-pouranik/urBackend --json reviews,comments,reviewThreads 2>/dev/null | jq '.reviews[] | {state, body: .body[:500]}' 2>/dev/null || echo "Fallback"

gh api repos/yash-pouranik/urBackend/pulls/51/reviews 2>/dev/null | jq '.[] | {id, state, body: .body[:600]}'

Length of output: 2014

🏁 Script executed:

#!/bin/bash
# Fetch review comments (inline)
gh api repos/yash-pouranik/urBackend/pulls/51/comments 2>/dev/null | jq '.[] | {path, line, body: .body[:600], user: .user.login}'

Length of output: 12355

Let me check the current state of the critical files to see if the flagged issues were addressed.

🏁 Script executed:

#!/bin/bash
# Read the current state of the critical middleware file
cat -n apps/public-api/src/middlewares/authorizeWriteOperation.js

echo "---"
cat -n apps/public-api/src/middlewares/resolvePublicAuthContext.js

echo "---"
# Check userAuth controller for the verification field fix
cat -n apps/public-api/src/controllers/userAuth.controller.js | head -80

echo "---"
# Check project controller for the ownerField default fix
cat -n apps/dashboard-api/src/controllers/project.controller.js | grep -n "userId\|ownerField\|getDefaultRls" | head -40

Length of output: 10824

🏁 Script executed:

#!/bin/bash
# Check Database.jsx for accessibility fixes and ownerField options
grep -n "role=\|aria-\|rlsOwnerFieldOptions\|ownerField\|rls\." apps/web-dashboard/src/pages/Database.jsx | head -60

echo "=== DASHBOARD USERAUTH ==="
cat -n apps/dashboard-api/src/controllers/userAuth.controller.js | grep -n "getVerificationField\|emailVerified\|isVerified" | head -20

echo "=== CHECK getVerificationField in DASHBOARD ==="
grep -n "getVerificationField\|emailVerified" apps/dashboard-api/src/controllers/userAuth.controller.js | head -20

echo "=== PROJECT MODEL ownerField ==="
cat -n packages/common/src/models/Project.js | grep -n "ownerField\|validate\|match\|required" | head -20

Length of output: 1879

🏁 Script executed:

#!/bin/bash
# Check the full rlsOwnerFieldOptions implementation
sed -n '95,125p' apps/web-dashboard/src/pages/Database.jsx
echo "---"
sed -n '228,250p' apps/web-dashboard/src/pages/Database.jsx

Length of output: 1894

@yash-pouranik Here's my merge readiness assessment after checking the current state of all flagged files:

✅ Issues That Were Fixed

Issue Severity Status
Crash in authorizeWriteOperation.js: req.authUser.userId accessed when authUser is null 🔴 Critical ✅ Fixed — unconditional !req.authUser?.userId guard added at Line 40
resolvePublicAuthContext runs JWT work for secret-key requests 🟡 Minor ✅ Fixed — if (req.keyRole === 'secret') return next() added
RLS dialog missing role, aria-modal, aria-labelledby 🟡 Minor ✅ Fixed — all ARIA attributes present
rlsOwnerFieldOptions offered fields not in schema 🟡 Minor ✅ Fixed — now derived from schema keys; _id only for users
getSingleProject / toggleAuth hardcoded ownerField: 'userId' default 🟠 Major ✅ Fixed — now uses getDefaultRlsForCollection() helper
updateCollectionRls allowed _id as ownerField for non-users collections 🟡 Minor ✅ Fixed — restricted to users collection only
Dashboard-side ownerField defaults not schema-derived 🟡 Minor ✅ Fixed — derives from schema keys with priority fallback

⚠️ Remaining Open Issues

1. 🟠 Major — getVerificationField fallback in both userAuth controllers

Both apps/public-api/src/controllers/userAuth.controller.js and apps/dashboard-api/src/controllers/userAuth.controller.js still have:

// Line 22 in both files
return 'emailVerified'; // ← fallback when field isn't in schema

If a project's users collection defines none of emailVerified, isVerified, or isverified, this silently writes to a phantom field — users would appear unverified forever, or a stale field in the DB is set with no effect. The fix (throw or return null and guard the caller) was flagged as Major in the review and is still outstanding:

const getVerificationField = (usersColConfig) => {
    const modelKeys = (usersColConfig?.model || []).map((f) => f?.key);
    if (modelKeys.includes('emailVerified')) return 'emailVerified';
    if (modelKeys.includes('isVerified')) return 'isVerified';
    if (modelKeys.includes('isverified')) return 'isverified';
-   return 'emailVerified'; // ← dangerous fallback
+   return null; // caller must handle null
};

2. 🟡 Minor — Bearer scheme check is case-sensitive

In resolvePublicAuthContext.js Line 12, authHeader.startsWith('Bearer ') will miss bearer <token> or BEARER <token>. Per RFC 7235, auth-scheme is case-insensitive:

- if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ if (!authHeader || !/^bearer\s/i.test(authHeader)) {

3. 🟡 Minor — Auto-generated username can be shorter than 3 chars

In buildAuthUserPayload, the fallback email.split('@')[0] (e.g., a@b.com"a") can produce a 1-character username that the validation rules at Lines 389–393 would reject in other contexts.

Merge Recommendation

Not yet ready to merge due to the getVerificationField fallback issue (item 1 above). It's a silent data-correctness bug — no crash, but email verification would silently fail for non-standard schema configurations, which could block all new user signups if email verification is enabled. The fix is a one-liner in two files.

Items 2 and 3 are low risk and can be addressed in a follow-up if you prefer to keep this PR focused. Would you like me to open a follow-up issue for those?

@coderabbitai check if the issue one is solved? and i can merge?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PUBLIC API userAuth.controller.js - getVerificationField & call sites ==="
grep -n "getVerificationField\|emailVerified\|verificationField\|return null\|return 'email" apps/public-api/src/controllers/userAuth.controller.js | head -40

echo ""
echo "=== DASHBOARD API userAuth.controller.js - getVerificationField & call sites ==="
grep -n "getVerificationField\|emailVerified\|verificationField\|return null\|return 'email" apps/dashboard-api/src/controllers/userAuth.controller.js | head -40

echo ""
echo "=== PUBLIC API - full getVerificationField function ==="
sed -n '15,50p' apps/public-api/src/controllers/userAuth.controller.js

echo ""
echo "=== DASHBOARD API - full getVerificationField function ==="
sed -n '15,50p' apps/dashboard-api/src/controllers/userAuth.controller.js

Length of output: 3850


@yash-pouranik Issue 1 is fully resolved in both files ✅. Here's what was confirmed in the current code:

Change public-api dashboard-api
getVerificationField returns null (Line 22)
buildAuthUserPayload guards with if (verificationField !== null)
verifyEmail guards with if (!verificationField) before DB write

All previously flagged issues are now addressed:

  • 🔴 Critical: req.authUser null-crash in authorizeWriteOperation.js — fixed
  • 🟠 Major: getVerificationField phantom fallback in both userAuth controllers — fixed
  • 🟠 Major: Hardcoded ownerField: 'userId' defaults — fixed
  • 🟡 Minor issues: All fixed

✅ This PR is ready to merge. The two remaining minor issues (Bearer case-sensitivity #53, short username #54) are tracked as follow-up issues.

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