--- description: Auth model + permission model for tcg-vault (JWT + collection roles) globs: pages/api/**/*.js,lib/*.js,components/*.js,pages/*.js --- # Auth + permissions There are three parallel client-side auth implementations and one server-side helper. New code should use the canonical set listed below; don't proliferate variants. ## Canonical surface (use these) | Concern | Module | | --- | --- | | Server: extract user from request | `lib/permission-middleware.js::getUserFromRequest` | | Server: gate a collection route | `lib/permission-middleware.js::withCollectionPermission` | | Server: log collection mutation | `lib/permission-middleware.js::logCollectionActivity` | | Server: token / password primitives | `pages/api/auth-utils.js` (`generateToken`, `verifyToken`, `hashPassword`, `verifyPassword`) | | Client: hook | `lib/use-auth.js::useAuth` | | Client: route protection | `components/ProtectedRoute.js` | | Client: admin route protection | `components/AdminProtected.js` | ## Legacy (do not extend) - `lib/auth-context.js::AuthProvider` + `useAuth` — older context. Still wired in `pages/_app.js`; left in place for compatibility. Don't add new consumers. - `lib/admin-auth.js::AdminProvider` + `useAdmin` + `useIsAdmin` — parallel admin context. Same story. A convoy is planned to collapse these three into one provider + one hook. ## Token model - JWT in localStorage under the key `auth_token`. - Signed with `JWT_SECRET` (HS256), 7-day expiry, payload `{ userId, email, role }`. - Sent on every authenticated fetch as `Authorization: Bearer `. - Verified server-side with `jsonwebtoken.verify(token, JWT_SECRET)`. **JWT_SECRET MUST be set in the deploy environment.** Seven files default it to a string literal if unset; that defeats signing. ## Roles Two role surfaces are in play: 1. **User role** — `users.role` column, values `'user'` or `'admin'`. Admin gates `/admin/*` pages and admin-only API endpoints. 2. **Collection role** — `collection_permissions.role` (`viewer` / `editor` / `owner`) + `collections.is_public` (anonymous viewer access). Resolved by `checkCollectionPermission` in priority order: owner → public-viewer → explicit row. When introducing a new permission tier, update both `checkRolePermission`'s hierarchy AND every gate that reads `is_public`. ## Authentication state on the client `useAuth()` returns `{ user, loading, login, logout, refresh }`. `user === null` means logged out; `loading === true` means token verification in flight. Always render against `loading === false` before deciding to redirect. ## Server-side authorization patterns - **Owner-only** (delete, settings): inside the handler, `if (user.userId !== resource.user_id) return res.status(403)`. - **Editor-or-owner**: use `withCollectionPermission('editor')`. - **Public read**: use `withCollectionPermission('viewer')` — handles `is_public` and explicit-permission case. - **Admin-only**: check `user.role === 'admin'` directly; consider extracting `withAdmin()` if a third call site appears.