@@ -44,7 +44,7 @@ generation, helmet + compression + express-rate-limit for hardening.
4444```
4545backend/
4646├── config/ db.js
47- ├── controllers/ auth, document, ai, flashcard, quiz, dashboard
47+ ├── controllers/ auth, document, ai, flashcard, quiz, dashboard, admin
4848├── middlewares/ auth, upload, error, rate limiter
4949├── models/ User, Document, Flashcard, Quiz, ChatHistory
5050├── routes/ one router per resource, mounted under /api/*
@@ -85,12 +85,14 @@ npm run dev # http://localhost:8000
8585PORT=8000
8686MONGO_URI=<your MongoDB connection string>
8787JWT_SECRET=<long random string>
88- JWT_EXPIRES_IN=7d
8988OPENAI_API_KEY=<your OpenAI API key>
9089OPENAI_MODEL=gpt-4o-mini
9190CLIENT_URL=http://localhost:5173
9291```
9392
93+ See ` backend/.env.example ` for the optional variables (access/refresh token
94+ lifetimes, account lockout thresholds, AI budget cap) and their defaults.
95+
9496### Frontend
9597
9698``` bash
@@ -126,7 +128,29 @@ Then open `http://localhost:5173`, register an account, and upload a PDF.
126128 be capped per user per month with ` MONTHLY_AI_BUDGET_USD ` (unset = no cap).
127129- Quiz answer keys are never sent to the client until a quiz is submitted.
128130 Grading happens on the server.
131+ - Login/register/refresh are rate limited to 20 requests per 15 minutes per
132+ IP, and an account locks itself out for 15 minutes after 5 consecutive
133+ wrong passwords — both blunt email enumeration and credential stuffing.
134+ Locked-out and nonexistent-user logins return the identical "Invalid email
135+ or password" response so neither leaks which emails are registered.
136+ - Auth uses a short-lived (15 min) access token returned in the response body
137+ plus a 7-day refresh token in an httpOnly cookie; ` POST /api/auth/refresh `
138+ rotates both. A stolen access token is only useful for minutes; the refresh
139+ token never touches JavaScript-readable storage. There's no server-side
140+ revocation list yet, so a compromised refresh token is still valid until it
141+ expires — full rotation-with-reuse-detection is still open (Phase 31).
129142- Uploaded files are stored on local disk under ` backend/uploads/ ` . For a
130- production deploy with an ephemeral filesystem, swap in S3 or Cloudinary.
143+ production deploy with an ephemeral filesystem, swap in S3 or Cloudinary —
144+ until then, a document whose file was wiped by a redeploy shows a "file no
145+ longer available" banner instead of a broken viewer (chat/flashcards/quiz
146+ still work since the extracted text is stored in MongoDB, not on disk).
131147- ` GET /health ` is a liveness check; ` GET /ready ` also verifies MongoDB is
132148 connected — point an orchestrator's readiness probe at the latter.
149+ - Set ` ADMIN_EMAILS ` (comma-separated) to unlock a read-only cost dashboard at
150+ ` /admin/costs ` — spend and token usage per user per day, from the ` LlmCall `
151+ ledger. It's an allowlist check on every request, not a stored role, so
152+ granting/revoking access is just an env var change.
153+ - CI runs backend/frontend tests, ` npm audit --audit-level=high ` on both,
154+ CodeQL static analysis, and the Playwright E2E suite against a real
155+ backend + ephemeral in-memory MongoDB — see
156+ [ .github/workflows/ci.yml] ( .github/workflows/ci.yml ) .
0 commit comments