Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through
1629afb) on the docs side. Code already on main; this PR is docs only.
Updates:
AGENTS.md
- §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic
admin, login/register rate limit)
- §3 conventions point at lib/auth-secret.js + lib/rate-limit.js
- §4 gotchas #2/#3/#5 converted to "Resolved" notes in place
(NOT renumbered, to preserve cross-references)
- new #12 documents the KV_REST_API_* env-var convention
- §5 setup list adds the rate-limit env vars
- §6 testing rewritten for Vitest (16 unit tests, blocking CI gate)
.cursor/rules/auth-and-permissions.mdc
- canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js
- token model now 24h (was 7d) with fail-loud explanation
- server-side authorization patterns lead with null → 401 contract
.cursor/rules/api-routes.mdc
- removes the "CRITICAL — known bug" callout (resolved by Brief 2)
- adds a "Rate limiting" section with verbatim shape + env-var notes
- "Dev/test endpoints" → "Removed" historical note so future agents
searching for test-db understand why it's gone
.convoys/fix-auth-bypass.md (restored — was on convoy branch only)
- frontmatter → status: shipped
- new "Convoy outcome" section: briefs + commits + resolved gotchas,
R1-R12 risk walk, env-var-rename deviation record, queued follow-up
convoys, lessons learned
.convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch)
- audit-trail completeness; convoy plan references them by name
- brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_*
across init rules, smoke, pre-deploy checklist
- brief 4 has a new "Post-merge addendum" explaining the rename
.convoys/ship-readiness.md
- P0 #1, #2, #4 → RESOLVED with merge-commit citations
- P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers
(cors-tighten and add-rate-limiting convoys)
- each item gains an "As-shipped" line for self-containment
README.md
- Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep
- auth + rate-limit + testing bullets updated
- env-var template extended with KV_REST_API_*
- deleted dev-endpoints note added to the API list
- "Default Admin Account" section LEFT ALONE — drop-public-setup territory
Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass,
lint baseline unchanged (128/81/47).
Convoy: fix-auth-bypass / role-doc-writer (closeout)
Co-authored-by: Cursor <cursoragent@cursor.com>
139 lines
6 KiB
Text
139 lines
6 KiB
Text
---
|
|
description: Conventions for Next.js Pages-router API route handlers in tcg-vault
|
|
globs: pages/api/**/*.js
|
|
---
|
|
|
|
# API Route Conventions
|
|
|
|
Pages router handlers; `(req, res)` signature; Vercel serverless functions.
|
|
|
|
## Authentication & Authorization
|
|
|
|
Two paths are in use; both go through `lib/permission-middleware.js`.
|
|
|
|
```js
|
|
// 1. Generic auth — every protected route
|
|
import { getUserFromRequest } from '../../lib/permission-middleware';
|
|
|
|
export default async function handler(req, res) {
|
|
const user = await getUserFromRequest(req);
|
|
if (!user) return res.status(401).json({ error: 'Authentication required' });
|
|
// user = { userId, email, role }
|
|
// ...
|
|
}
|
|
```
|
|
|
|
```js
|
|
// 2. Collection-scoped auth — when the route operates on a specific collection
|
|
import { withCollectionPermission } from '../../lib/permission-middleware';
|
|
|
|
async function handler(req, res) {
|
|
// req.user and req.permission are populated by the wrapper
|
|
const { userId, role } = req.user;
|
|
}
|
|
|
|
export default withCollectionPermission('viewer')(handler);
|
|
// 'viewer' | 'editor' | 'owner' — checks owner OR is_public OR explicit collection_permissions row
|
|
```
|
|
|
|
`getUserFromRequest` returns `null` for any unauthenticated request (missing header, malformed token, wrong signature, expired token, unknown user id). The early `if (!user) return res.status(401)` pattern in the snippet above is the canonical guard for every authenticated route. (The pre-`fix-auth-bypass` synthetic-admin fallback for missing tokens has been removed — see `AGENTS.md` Gotcha #2 for the audit trail.)
|
|
|
|
## Request validation
|
|
|
|
No schema validator is installed (no zod / yup / valibot). Validate manually:
|
|
|
|
```js
|
|
const { name, game } = req.body || {};
|
|
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
return res.status(400).json({ error: 'name is required' });
|
|
}
|
|
if (!['mtg', 'pokemon', 'lorcana'].includes(game)) {
|
|
return res.status(400).json({ error: 'invalid game' });
|
|
}
|
|
```
|
|
|
|
When adding zod (planned in `.convoys/`), define schemas at the top of the file.
|
|
|
|
## Method gating
|
|
|
|
Reject unsupported methods explicitly — Next.js will otherwise call the handler for any method:
|
|
|
|
```js
|
|
if (req.method !== 'POST') {
|
|
return res.status(405).json({ error: 'Method not allowed' });
|
|
}
|
|
```
|
|
|
|
## Error handling
|
|
|
|
Wrap the handler body in `try/catch`. Never throw unhandled — leaks stack traces in the Vercel response.
|
|
|
|
```js
|
|
export default async function handler(req, res) {
|
|
try {
|
|
// ...
|
|
} catch (err) {
|
|
console.error('[POST /api/collections]', err);
|
|
return res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
```
|
|
|
|
## Database access
|
|
|
|
- **Prefer:** `import { sql } from '@vercel/postgres'` and tagged templates: `` await sql`SELECT … WHERE id = ${id}` ``.
|
|
- **Avoid:** `import { db } from '../../lib/database'`. Its `query(str, params)` API uses `sql.unsafe` after manual interpolation — SQL-injection vector. Slated for removal in a convoy.
|
|
- **Always select narrow columns** — don't `SELECT *` from `cards` (large `oracle_text`, `colors` JSONB).
|
|
|
|
## Response shape
|
|
|
|
- Success (GET / read): `res.status(200).json({ data: ... })` OR direct payload — codebase is inconsistent; match the surrounding route's existing shape.
|
|
- Created (POST): `res.status(201).json({ data: ... })`.
|
|
- Errors: `res.status(<4xx|5xx>).json({ error: string, details?: unknown })`.
|
|
|
|
## Activity logging
|
|
|
|
Any handler that mutates a collection must call `logCollectionActivity`:
|
|
|
|
```js
|
|
import { logCollectionActivity } from '../../lib/permission-middleware';
|
|
|
|
await logCollectionActivity(collectionId, userId, 'card_added', { cardId, quantity });
|
|
```
|
|
|
|
## Rate limiting
|
|
|
|
`/api/auth/login` and `/api/auth/register` are wrapped with a 5-attempt / 15-minute sliding window via `lib/rate-limit.js`. New endpoints on the public auth surface (or anywhere brute-force / credential-stuffing matters) should follow the same shape:
|
|
|
|
```js
|
|
import { checkAuthRateLimit } from '../../../lib/rate-limit.js';
|
|
|
|
export default async function handler(req, res) {
|
|
if (req.method !== 'POST') {
|
|
return res.status(405).json({ error: 'Method not allowed' });
|
|
}
|
|
|
|
const { allowed, reset } = await checkAuthRateLimit(req);
|
|
if (!allowed) {
|
|
res.setHeader('Retry-After', Math.ceil((reset - Date.now()) / 1000));
|
|
return res.status(429).json({ error: 'Too many attempts. Try again later.' });
|
|
}
|
|
|
|
try {
|
|
// ... handler body ...
|
|
} catch (err) {
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
Notes:
|
|
|
|
- Gate sits between the method check and the body. It MUST be inside the `try`/`catch` if you want Upstash errors to bubble — but `checkAuthRateLimit` already swallows them and fails-open, so the placement above is fine.
|
|
- Identifier is the first hop in `x-forwarded-for` (Vercel's edge); do NOT key off `req.body.email` (rotates) or `req.headers.authorization` (login is unauthenticated by design).
|
|
- Env vars are `KV_REST_API_URL` + `KV_REST_API_TOKEN` (auto-provisioned by Vercel's Upstash Marketplace integration). In prod, missing either var is a **fail-closed throw** on the first call — set them in Vercel project settings before merging anything that imports `lib/rate-limit.js`. In dev, the module warn-and-no-ops so local work isn't blocked.
|
|
- The current scope is just the two auth endpoints. Sweeping the rest of the API (`/api/users/search`, `/api/cards/import-*`, avatar upload) is the queued `add-rate-limiting` convoy — follow the same pattern there.
|
|
|
|
## Dev/test endpoints (removed)
|
|
|
|
The four endpoints `pages/api/simple.js`, `pages/api/test-auth.js`, `pages/api/test-db.js`, and `pages/api/setup-database.js` used to exist as unauthenticated dev / diagnostic routes. They were **deleted** by `fix-auth-bypass` Brief 3 (commit `fc0dd73`) and `.github/workflows/ci.yml`'s `forbidden-endpoints` job now fails the build if any of them are re-introduced, or if any new file matching `pages/api/test-*.js` is added. **Do not re-create these files.** If a future agent searches for `test-db` or `setup-database` and finds them missing, this section is the explanation — diagnostics belong outside the public API surface (a CLI script, an admin-gated route, or `npm run` task).
|