---
convoy: fix-auth-bypass
brief_number: 2
depends_on: [1]
files:
  - lib/permission-middleware.js
  - pages/api/auth/verify.js
cross_brief_commitments:
  - brief: 1
    description: |
      Brief 1 already replaced the `JWT_SECRET` literal in both files with imports
      from `lib/auth-secret.js`. This brief preserves those imports and only
      removes the synthetic-admin fallback shapes.
  - brief: 5
    description: |
      Brief 5 (vitest) writes the unit tests that prove `getUserFromRequest`
      returns `null` for the four shapes (missing header, malformed token,
      expired token, valid token-but-no-user-row). The behavior is implemented
      here; the harness lands in Brief 5.
---

# Brief 2: Remove the synthetic-admin bypass

## Goal (1 sentence)

Make `lib/permission-middleware.js::getUserFromRequest` return `null` for any unauthenticated request, and make `pages/api/auth/verify.js` return 401 instead of fetching `admin@tcgvault.com` when no Bearer token is present.

## Files in scope (do not edit anything else)

- `lib/permission-middleware.js` — modified
- `pages/api/auth/verify.js` — modified

## Conventions to follow

- `.cursor/rules/auth-and-permissions.mdc` § "Server-side authorization patterns" — `if (!user) return res.status(401)` pattern. The 24 callers of `getUserFromRequest` already follow this; we just need to make the helper actually emit `null`.
- `.cursor/rules/api-routes.mdc` § "Error handling" — keep the `try/catch` wrapper in place; do not throw out of the handler.
- `.cursor/rules/no-go-zones.mdc` — do not touch any other file.

## Acceptance criteria

### `lib/permission-middleware.js::getUserFromRequest`

- [ ] **Delete lines 14-17** of the post-Brief-1 file (the `console.warn` and the synthetic admin return). Replace with a plain `return null`. Verbatim shape:

```js
// before (post-Brief-1, with literal already gone):
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  // For development, return user ID 1 if no token (should be removed in production)
  console.warn('⚠️  Development mode: Using fallback user authentication');
  return { userId: 1, email: 'admin@tcgvault.com', role: 'admin' };
}

// after:
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  return null;
}
```

- [ ] No `console.warn`. No comment-out. No env-gate (`NODE_ENV === 'development'`). The fallback is gone, period. If a developer needs an authenticated session locally, they log in.
- [ ] The rest of `getUserFromRequest` (token verify, DB lookup, error catch) is unchanged.
- [ ] The catch block at lines 38-41 stays:

```js
} catch (error) {
  console.error('Error getting user from request:', error);
  return null;
}
```

  This means JWT verification errors (expired, malformed, bad signature) AND DB errors all collapse to `null`. The 401 vs 500 distinction is left to callers (currently every caller treats `null` as 401, which is correct for an auth helper).

- [ ] No change to `checkCollectionPermission`, `withCollectionPermission`, `checkRolePermission`, or `logCollectionActivity`.

### `pages/api/auth/verify.js`

- [ ] **Delete lines 25-39** of the post-Brief-1 file (the `// For development, return admin user if no token provided` block and the `SELECT … WHERE email = 'admin@tcgvault.com'` query). Replace with an immediate 401:

```js
// before:
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  // For development, return admin user if no token provided
  // In production, this should return 401
  const result = await sql`
    SELECT id, email, role, created_at
    FROM users
    WHERE email = 'admin@tcgvault.com'
  `;

  if (result.rows.length > 0) {
    return res.status(200).json(result.rows[0]);
  } else {
    return res.status(401).json({ error: 'No admin user found' });
  }
}

// after:
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  return res.status(401).json({ error: 'Authentication required' });
}
```

- [ ] No env-gate. No comment-out.
- [ ] The rest of `verify.js` (CORS headers, OPTIONS preflight, method gate, JWT verify, DB lookup) is unchanged. CORS tightening is Brief 4's scope and only covers `login.js` + `register.js`, NOT `verify.js` (out of scope per the convoy).
- [ ] The error-message wording matches the existing convention: `{ error: 'Authentication required' }`. Do not invent a new shape.

### Caller spot-check (do this before opening the PR)

The convoy claims "30+ handlers depend on `getUserFromRequest`." Re-verify by running the following (results captured at architect time on 2026-05-23 — `master` revision `ebd4fd1`; if the count drifts, list the new files in the PR description):

- [ ] `rg "getUserFromRequest" pages/api --type js -l | wc -l` → 24 files (one of which is `permission-middleware.js`'s import-bookkeeping artifact, leave the count as-is).
- [ ] `rg "if \(!user\)" pages/api --type js -A 1` (with `-A 1`) — every match must be followed by `return res.status(401).json({ error: 'Authentication required' });` or a similar 401. If any caller has a different shape (e.g. `if (!user) return res.status(403)`, or `if (user) ...` inverted, or no null guard at all), **stop and re-architect**: that caller would need behavioral changes, and this convoy explicitly does not touch caller code.
- [ ] One file is known to use optional-chaining instead of an early 401 — `pages/api/collections/[identifier].js` uses `user?.userId` because it allows anonymous access to public collections. **This is intentional** and stays correct under the fix (when `user` is `null`, `user?.userId` is `undefined`, the public-collection branch still works). Do not "fix" it.

### Smoke (manual)

- [ ] `npm run dev`; with no `Authorization` header, hit `curl http://localhost:3000/api/user/profile` → expect HTTP 401 with body `{"error":"Authentication required"}`. (Pre-fix: returns the admin user's profile.)
- [ ] Same with `curl http://localhost:3000/api/auth/verify` → expect HTTP 401. (Pre-fix: returns admin user data.)
- [ ] Log in via the UI; observe the dashboard loads (the helper still works for valid tokens).
- [ ] Log out; observe the dashboard redirects to `/login` (the helper now correctly returns `null`).

### Out of scope

- [ ] No edits to any of the 24 callers — they already handle `null` correctly.
- [ ] No CORS changes (Brief 4).
- [ ] No rate-limit (Brief 4).
- [ ] No tests — Brief 5 ships them.
- [ ] No `AGENTS.md` / `.cursor/rules/*.mdc` updates — doc-writer pass.

## Rationale (≤3 sentences)

This is the convoy's actual security fix — removing the synthetic admin makes 24 currently-broken handlers correct in one ~6-line change. Folding `verify.js`'s parallel bug (the no-token branch fetches `admin@tcgvault.com` directly from the DB) into the same brief keeps "the auth helper returns null" and "the verify endpoint returns 401" coupled, since both have to land before any unauthenticated request can be safely served. Splitting them risks a deploy ordering where one is fixed and the other isn't — exactly the inconsistency that lets a P0 ship-blocker survive.
