Skip to content

Userauth part3 backend routes#36

Open
parthdude07 wants to merge 6 commits into
genesis-kb:userauthfrom
parthdude07:userauth-part3-backend-routes
Open

Userauth part3 backend routes#36
parthdude07 wants to merge 6 commits into
genesis-kb:userauthfrom
parthdude07:userauth-part3-backend-routes

Conversation

@parthdude07

@parthdude07 parthdude07 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary by cubic

Adds JWT-based email/password auth to the backend and introduces local note‑taking with a transcript whiteboard. Updates the UI to create, tag, and pin notes, and wires notes into highlights and transcript pages.

  • New Features

    • /api/v1/auth: POST /register, POST /login, GET /me (protected)
    • JWT middleware: requireAuth, optionalAuth
    • Auth service: registerUser, loginUser, getUserById with bcrypt hashing and JWTs
    • Config: JWT_SECRET, JWT_EXPIRES_IN, bcryptRounds added to .env.example and prod validation
    • API docs now list auth endpoints
    • Notes & Whiteboard: local notes (useNotes, NoteCard, NoteEditor, NotesSearchBar, TranscriptNotes) with tags/colors/pin, search/filter/sort, storage + migration; interactive whiteboard (TranscriptWhiteboard, ConceptNode, useWhiteboard) for concept mapping
    • UI wiring: TranscriptDetail adds Notes/Whiteboard tabs; Library adds Notes tab; HighlightToolbar can add notes; highlights now support color and isUnderline; TranscriptChat can consume a pending prompt
    • Dependencies: jsonwebtoken, bcryptjs, @xyflow/react
  • Migration

    • Run migration backend/supabase/migrations/002_auth_users.sql to create the users table and trigger
    • Set JWT_SECRET (32+ chars) and optionally JWT_EXPIRES_IN in your environment, then restart the server

Written for commit e938151. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7591b370-5b8c-4a4f-bfe2-addcaec55341

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

17 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/src/services/authService.js">

<violation number="1" location="backend/src/services/authService.js:23">
P2: Registration has a TOCTOU (time-of-check-time-of-use) race condition that can produce a 500 error under concurrent requests for the same email. The check-then-insert pattern checks `SELECT ... WHERE email = $1`, then inserts. When two registrations arrive near-simultaneously for the same email, both checks can pass, the second INSERT hits the UNIQUE constraint and throws a PostgreSQL error (code 23505) that bubbles up as an unhandled 500 to the caller. The controller's `catch` block checks for `err.statusCode === 409`, but the raw pg error has no `statusCode` — so it falls through and becomes a 500 instead of a clean 409.</violation>

<violation number="2" location="backend/src/services/authService.js:36">
P1: Passwords longer than 72 UTF-8 bytes are accepted but bcryptjs silently ignores the suffix, so different passwords sharing the first 72 bytes authenticate as the same credential. Reject `bcrypt.truncates(password)` during both registration and login, or use a password-hashing scheme without this limit.</violation>

<violation number="3" location="backend/src/services/authService.js:43">
P2: Malformed or unsanitized display names can cause registration to fail with a 500 or store forbidden input because optional chaining only handles nullish values, not non-strings. Validate the field as a bounded string and apply the repository's sanitization before calling the service.</violation>

<violation number="4" location="backend/src/services/authService.js:49">
P2: Registration and login email addresses are written verbatim to application logs, exposing user identifiers in every configured transport. Log a non-identifying event or pass a redacted value instead.</violation>

<violation number="5" location="backend/src/services/authService.js:70">
P2: Unknown-email login attempts return without bcrypt work, while known-email attempts do not, allowing attackers to infer registered accounts from latency. Compare against a fixed dummy bcrypt hash when no user is found so both paths perform equivalent password work.</violation>
</file>

<file name="backend/src/middleware/auth.js">

<violation number="1" location="backend/src/middleware/auth.js:25">
P3: JWT parsing, verification, and user mapping are duplicated across `requireAuth` and `optionalAuth`, so authentication-policy fixes can drift between required and optional routes. A shared verifier that returns a normalized user, with each middleware applying its own failure policy, would centralize this logic.</violation>

<violation number="2" location="backend/src/middleware/auth.js:41">
P3: This optional-authentication path is currently dead and has no exercised behavior. Removing it until a route needs guest/user handling, or adding a consumer and tests, would avoid maintaining an unverified authentication path.</violation>
</file>

<file name="backend/.env.example">

<violation number="1" location="backend/.env.example:30">
P1: Production can run with a publicly known JWT signing key when this example is copied unchanged, making authentication tokens forgeable. Leaving the sample empty would make production fail the existing required-variable check until a real random secret is supplied.</violation>
</file>

<file name="backend/src/config/index.js">

<violation number="1" location="backend/src/config/index.js:43">
P1: Production accepts the publicly known `.env.example` placeholder as a valid JWT secret, allowing anyone who follows the example without replacing it to forge authentication tokens. Secret validation should reject placeholders and enforce a sufficiently random secret before startup.</violation>

<violation number="2" location="backend/src/config/index.js:69">
P1: A reachable staging or other non-production deployment without `JWT_SECRET` will accept forged Bearer tokens because all tokens use the known `dev-secret-change-in-production` key. Removing the fixed fallback and requiring an injected secret for every deployed environment would prevent authentication bypass.</violation>
</file>

<file name="backend/supabase/migrations/002_auth_users.sql">

<violation number="1" location="backend/supabase/migrations/002_auth_users.sql:8">
P1: This creates a public-schema table containing password hashes without a database access boundary. If `public` is exposed through Supabase APIs, an anon/authenticated client can read the hashes; keeping `users` server-only with RLS and no public policies (or moving it to a private schema) prevents that exposure.</violation>

<violation number="2" location="backend/supabase/migrations/002_auth_users.sql:10">
P2: Email identity uniqueness is case-sensitive in this schema even though `authService` treats emails case-insensitively. A non-normalizing writer or future auth provider can create duplicate logical accounts; enforcing a unique index on `LOWER(email)` would preserve one account per email.</violation>

<violation number="3" location="backend/supabase/migrations/002_auth_users.sql:21">
P3: This index duplicates the index automatically created by the `UNIQUE` constraint on `email`, adding storage and write overhead without changing lookup behavior. Removing the redundant index keeps the schema lean.</violation>

<violation number="4" location="backend/supabase/migrations/002_auth_users.sql:22">
P2: OAuth identities are not constrained to one row per `(provider, provider_id)`; this index only speeds up duplicate lookups. Making it a unique partial index for non-null provider IDs would prevent duplicate external accounts while still allowing local users without a provider ID.</violation>

<violation number="5" location="backend/supabase/migrations/002_auth_users.sql:33">
P3: Reapplying this migration fails at the trigger even though the surrounding DDL is written to tolerate existing objects. Dropping this named trigger before recreating it, or guarding creation in a `DO` block, would make repair/redeploy runs consistent.</violation>
</file>

<file name="backend/src/controllers/authController.js">

<violation number="1" location="backend/src/controllers/authController.js:18">
P2: Auth controller validates inputs inline (regex, length checks) instead of using the express-validator middleware pattern that every other endpoint follows.

The project has a dedicated validation layer (`src/middleware/validation.js`) with reusable rulesets like `validationRules.search` that are applied as route middleware. The auth controller's inline validation duplicates this concern inside the handler, making the validation invisible from the route definition and harder to audit, reuse, or update.

Consider extracting auth validation rules into `validationRules.register` and `validationRules.login` in `validation.js`, then applying them as middleware on the auth routes — consistent with the rest of the codebase.</violation>

<violation number="2" location="backend/src/controllers/authController.js:39">
P2: Auth controller constructs `{ success: true, data: ... }` response manually instead of using the `sendSuccess`/`sendCreated` helper consistently used by every other controller in this codebase.

All three handlers (`register`, `login`, `me`) call `res.json({ success: true, data: ... })` or `res.status(201).json(...)` directly. The rest of the codebase (transcriptController, aiController, healthController) uses `sendSuccess(res, data)` and `sendCreated(res, data)` from `src/utils/responseHelper.js`, which guarantees a uniform response shape and reduces duplication.

Recommendation: Replace inline response construction with the shared helpers — `sendCreated(res, result)` for register (201), `sendSuccess(res, result)` for login, and `sendSuccess(res, { user })` for me.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/.env.example

# Authentication (JWT)
# Generate a strong secret: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_SECRET=your-jwt-secret-min-32-chars

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Production can run with a publicly known JWT signing key when this example is copied unchanged, making authentication tokens forgeable. Leaving the sample empty would make production fail the existing required-variable check until a real random secret is supplied.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/.env.example, line 30:

<comment>Production can run with a publicly known JWT signing key when this example is copied unchanged, making authentication tokens forgeable. Leaving the sample empty would make production fail the existing required-variable check until a real random secret is supplied.</comment>

<file context>
@@ -24,3 +24,8 @@ RATE_LIMIT_MAX_REQUESTS=100
+
+# Authentication (JWT)
+# Generate a strong secret: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
+JWT_SECRET=your-jwt-secret-min-32-chars
+JWT_EXPIRES_IN=7d
</file context>
Suggested change
JWT_SECRET=your-jwt-secret-min-32-chars
JWT_SECRET=


// Validate critical environment variables
const requiredVars = ['DATABASE_URL'];
const requiredVars = ['DATABASE_URL', 'JWT_SECRET'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Production accepts the publicly known .env.example placeholder as a valid JWT secret, allowing anyone who follows the example without replacing it to forge authentication tokens. Secret validation should reject placeholders and enforce a sufficiently random secret before startup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/src/config/index.js, line 43:

<comment>Production accepts the publicly known `.env.example` placeholder as a valid JWT secret, allowing anyone who follows the example without replacing it to forge authentication tokens. Secret validation should reject placeholders and enforce a sufficiently random secret before startup.</comment>

<file context>
@@ -40,7 +40,7 @@ const validateEnvVars = (requiredVars) => {
 
 // Validate critical environment variables
-const requiredVars = ['DATABASE_URL'];
+const requiredVars = ['DATABASE_URL', 'JWT_SECRET'];
 
 // Only validate in production, allow fallbacks in development
</file context>


// Authentication (JWT)
auth: {
jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: A reachable staging or other non-production deployment without JWT_SECRET will accept forged Bearer tokens because all tokens use the known dev-secret-change-in-production key. Removing the fixed fallback and requiring an injected secret for every deployed environment would prevent authentication bypass.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/src/config/index.js, line 69:

<comment>A reachable staging or other non-production deployment without `JWT_SECRET` will accept forged Bearer tokens because all tokens use the known `dev-secret-change-in-production` key. Removing the fixed fallback and requiring an injected secret for every deployed environment would prevent authentication bypass.</comment>

<file context>
@@ -64,6 +64,13 @@ const config = {
 
+  // Authentication (JWT)
+  auth: {
+    jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production',
+    jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
+    bcryptRounds: 12,
</file context>

}

// Hash password
const hashedPassword = await bcrypt.hash(password, config.auth.bcryptRounds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Passwords longer than 72 UTF-8 bytes are accepted but bcryptjs silently ignores the suffix, so different passwords sharing the first 72 bytes authenticate as the same credential. Reject bcrypt.truncates(password) during both registration and login, or use a password-hashing scheme without this limit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/src/services/authService.js, line 36:

<comment>Passwords longer than 72 UTF-8 bytes are accepted but bcryptjs silently ignores the suffix, so different passwords sharing the first 72 bytes authenticate as the same credential. Reject `bcrypt.truncates(password)` during both registration and login, or use a password-hashing scheme without this limit.</comment>

<file context>
@@ -0,0 +1,139 @@
+  }
+
+  // Hash password
+  const hashedPassword = await bcrypt.hash(password, config.auth.bcryptRounds);
+
+  // Insert user
</file context>

-- ============================================================

-- Users table
CREATE TABLE IF NOT EXISTS users (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: This creates a public-schema table containing password hashes without a database access boundary. If public is exposed through Supabase APIs, an anon/authenticated client can read the hashes; keeping users server-only with RLS and no public policies (or moving it to a private schema) prevents that exposure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/supabase/migrations/002_auth_users.sql, line 8:

<comment>This creates a public-schema table containing password hashes without a database access boundary. If `public` is exposed through Supabase APIs, an anon/authenticated client can read the hashes; keeping `users` server-only with RLS and no public policies (or moving it to a private schema) prevents that exposure.</comment>

<file context>
@@ -0,0 +1,36 @@
+-- ============================================================
+
+-- Users table
+CREATE TABLE IF NOT EXISTS users (
+    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+    email       TEXT UNIQUE NOT NULL,
</file context>

*/
export const registerUser = async (email, password, name) => {
// Check if user already exists
const existing = await query(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Registration has a TOCTOU (time-of-check-time-of-use) race condition that can produce a 500 error under concurrent requests for the same email. The check-then-insert pattern checks SELECT ... WHERE email = $1, then inserts. When two registrations arrive near-simultaneously for the same email, both checks can pass, the second INSERT hits the UNIQUE constraint and throws a PostgreSQL error (code 23505) that bubbles up as an unhandled 500 to the caller. The controller's catch block checks for err.statusCode === 409, but the raw pg error has no statusCode — so it falls through and becomes a 500 instead of a clean 409.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/src/services/authService.js, line 23:

<comment>Registration has a TOCTOU (time-of-check-time-of-use) race condition that can produce a 500 error under concurrent requests for the same email. The check-then-insert pattern checks `SELECT ... WHERE email = $1`, then inserts. When two registrations arrive near-simultaneously for the same email, both checks can pass, the second INSERT hits the UNIQUE constraint and throws a PostgreSQL error (code 23505) that bubbles up as an unhandled 500 to the caller. The controller's `catch` block checks for `err.statusCode === 409`, but the raw pg error has no `statusCode` — so it falls through and becomes a 500 instead of a clean 409.</comment>

<file context>
@@ -0,0 +1,139 @@
+ */
+export const registerUser = async (email, password, name) => {
+  // Check if user already exists
+  const existing = await query(
+    'SELECT id FROM users WHERE email = $1',
+    [email.toLowerCase().trim()]
</file context>

* Useful for routes that work for both guests and logged-in users.
* Sets req.user = null when no token or invalid token.
*/
export const optionalAuth = (req, _res, next) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This optional-authentication path is currently dead and has no exercised behavior. Removing it until a route needs guest/user handling, or adding a consumer and tests, would avoid maintaining an unverified authentication path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/src/middleware/auth.js, line 41:

<comment>This optional-authentication path is currently dead and has no exercised behavior. Removing it until a route needs guest/user handling, or adding a consumer and tests, would avoid maintaining an unverified authentication path.</comment>

<file context>
@@ -0,0 +1,59 @@
+ * Useful for routes that work for both guests and logged-in users.
+ * Sets req.user = null when no token or invalid token.
+ */
+export const optionalAuth = (req, _res, next) => {
+  const authHeader = req.headers.authorization;
+
</file context>

const token = authHeader.split(' ')[1];

try {
const decoded = jwt.verify(token, config.auth.jwtSecret);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: JWT parsing, verification, and user mapping are duplicated across requireAuth and optionalAuth, so authentication-policy fixes can drift between required and optional routes. A shared verifier that returns a normalized user, with each middleware applying its own failure policy, would centralize this logic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/src/middleware/auth.js, line 25:

<comment>JWT parsing, verification, and user mapping are duplicated across `requireAuth` and `optionalAuth`, so authentication-policy fixes can drift between required and optional routes. A shared verifier that returns a normalized user, with each middleware applying its own failure policy, would centralize this logic.</comment>

<file context>
@@ -0,0 +1,59 @@
+  const token = authHeader.split(' ')[1];
+
+  try {
+    const decoded = jwt.verify(token, config.auth.jwtSecret);
+    req.user = { id: decoded.sub, email: decoded.email };
+    next();
</file context>

);

-- Indexes
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This index duplicates the index automatically created by the UNIQUE constraint on email, adding storage and write overhead without changing lookup behavior. Removing the redundant index keeps the schema lean.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/supabase/migrations/002_auth_users.sql, line 21:

<comment>This index duplicates the index automatically created by the `UNIQUE` constraint on `email`, adding storage and write overhead without changing lookup behavior. Removing the redundant index keeps the schema lean.</comment>

<file context>
@@ -0,0 +1,36 @@
+);
+
+-- Indexes
+CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
+CREATE INDEX IF NOT EXISTS idx_users_provider ON users(provider, provider_id);
+
</file context>

END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_users_updated_at

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Reapplying this migration fails at the trigger even though the surrounding DDL is written to tolerate existing objects. Dropping this named trigger before recreating it, or guarding creation in a DO block, would make repair/redeploy runs consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/supabase/migrations/002_auth_users.sql, line 33:

<comment>Reapplying this migration fails at the trigger even though the surrounding DDL is written to tolerate existing objects. Dropping this named trigger before recreating it, or guarding creation in a `DO` block, would make repair/redeploy runs consistent.</comment>

<file context>
@@ -0,0 +1,36 @@
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trg_users_updated_at
+    BEFORE UPDATE ON users
+    FOR EACH ROW
</file context>

- Create 002_auth_users.sql: users table with email/password,
  provider columns for future GitHub OAuth, auto-update trigger
- Add auth config block (JWT_SECRET, JWT_EXPIRES_IN, bcryptRounds)
- Add JWT_SECRET to production required env vars
- Install jsonwebtoken and bcryptjs dependencies
- Update .env.example with auth variables
- auth.js middleware: requireAuth (hard 401) and optionalAuth (soft attach)
  for JWT verification from Authorization: Bearer header
- authService.js: registerUser, loginUser, getUserById with bcrypt
  hashing, email normalization, and sanitized user output
- authController.js: HTTP handlers with input validation (email format,
  password length 8–128), proper error code propagation
- Export query() from supabaseService.js for use by authService
- Create authRoutes.js: POST /register, POST /login, GET /me
- Mount auth routes at /api/v1/auth in route index
- Add auth endpoints to API documentation endpoint
- Export requireAuth and optionalAuth from middleware index
@parthdude07
parthdude07 force-pushed the userauth-part3-backend-routes branch from ed26223 to e938151 Compare July 21, 2026 09:53
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.

1 participant