107 lines
3.7 KiB
Text
107 lines
3.7 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
|
||
|
|
```
|
||
|
|
|
||
|
|
**CRITICAL — known bug:** `getUserFromRequest` currently has a development-mode fallback that returns a hardcoded admin user when no Bearer token is present. Until that's fixed, callers MUST also assert `req.headers.authorization` exists when the route is sensitive (admin operations, deletes). See `AGENTS.md` Gotcha #2.
|
||
|
|
|
||
|
|
## 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 });
|
||
|
|
```
|
||
|
|
|
||
|
|
## Dev/test endpoints
|
||
|
|
|
||
|
|
`pages/api/simple.js`, `pages/api/test-auth.js`, `pages/api/test-db.js`, `pages/api/setup-database.js` — these are dev-only endpoints currently shipped to prod. Don't add more. Existing ones should be deleted or admin-gated before public launch.
|