57 lines
3 KiB
Text
57 lines
3 KiB
Text
|
|
---
|
||
|
|
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 <token>`.
|
||
|
|
- 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.
|