Update README for today's security hardening and 2FA work
Several changes merged today were not reflected in the README. Bring it back in sync with main: - Security section: rewrite into Session/authorization, Login hardening, Uploads/input, and Platform groups — documents DB re-validation of the JWT per request (#12), role-based authorization (#10), optional TOTP 2FA (#9), login throttling + per-IP backoff, honeypot, bot-scoring/IP ban, and mimetype-derived upload extensions (#11) + username uniqueness checks on update (#13). - Environment variables: add TRUST_PROXY, DEBUG_TRUST_PROXY, TOTP_ISSUER, TOTP_CHALLENGE_TTL, and UPLOAD_DIR. - Routes/API tables: add /admin/account and the account/totp endpoints plus the login/totp second-factor step. - Tech stack + project structure: note TOTP (speakeasy/qrcode), the loginProtection/botScore middleware, the totp util, and the Account view. Docs-only; no code changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
55
README.md
55
README.md
@@ -35,7 +35,7 @@ The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, sc
|
|||||||
| Layer | Tech |
|
| Layer | Tech |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Backend | Node.js 20+, Express 4, `mariadb` driver (parameterized SQL, no ORM) |
|
| Backend | Node.js 20+, Express 4, `mariadb` driver (parameterized SQL, no ORM) |
|
||||||
| Auth | JWT in an httpOnly cookie, bcrypt password hashing |
|
| Auth | JWT in an httpOnly cookie, bcrypt password hashing, optional TOTP 2FA (`speakeasy` + `qrcode`) |
|
||||||
| Database | MariaDB 11 (own container) |
|
| Database | MariaDB 11 (own container) |
|
||||||
| Frontend | React 18, Vite 5, React Router 6 |
|
| Frontend | React 18, Vite 5, React Router 6 |
|
||||||
| Email | Nodemailer (SMTP) with a `mailto:` fallback |
|
| Email | Nodemailer (SMTP) with a `mailto:` fallback |
|
||||||
@@ -53,15 +53,15 @@ UOMSITE/
|
|||||||
│ │ ├─ app.js middleware + static SPA + routes
|
│ │ ├─ app.js middleware + static SPA + routes
|
||||||
│ │ ├─ router/v1/ auth / public / admin route groups
|
│ │ ├─ router/v1/ auth / public / admin route groups
|
||||||
│ │ ├─ model/ users · posts · wiki · settings · activity (.model + .db)
|
│ │ ├─ model/ users · posts · wiki · settings · activity (.model + .db)
|
||||||
│ │ ├─ middleware/ siteMode · noindex · rateLimit · validate
|
│ │ ├─ middleware/ siteMode · noindex · rateLimit · loginProtection · botScore · validate
|
||||||
│ │ └─ utils/ auth (JWT/cookies) · db (pool) · mailer · logger
|
│ │ └─ utils/ auth (JWT/cookies/roles) · totp (2FA) · db (pool) · mailer · logger
|
||||||
│ ├─ db/ schema.sql + seed.js
|
│ ├─ db/ schema.sql + seed.js
|
||||||
│ └─ .env.example
|
│ └─ .env.example
|
||||||
├─ client/ React + Vite SPA
|
├─ client/ React + Vite SPA
|
||||||
│ ├─ src/
|
│ ├─ src/
|
||||||
│ │ ├─ routes/public/ Portal, Website, News, Screenshots, FiveOnFriday, Newsletter(+Issue), Status, About, Maintenance
|
│ │ ├─ routes/public/ Portal, Website, News, Screenshots, FiveOnFriday, Newsletter(+Issue), Status, About, Maintenance
|
||||||
│ │ ├─ routes/wiki/ Wiki landing + WikiArticle
|
│ │ ├─ routes/wiki/ Wiki landing + WikiArticle
|
||||||
│ │ ├─ routes/admin/ AdminLogin, AdminLayout, views/ (Dashboard, Posts, Wiki, Settings, Activity, Users) + editors
|
│ │ ├─ routes/admin/ AdminLogin, AdminLayout, views/ (Dashboard, Posts, Wiki, Settings, Activity, Users, Account) + editors
|
||||||
│ │ ├─ components/ SiteHeader, SiteFooter, layout, guards, Modal, …
|
│ │ ├─ components/ SiteHeader, SiteFooter, layout, guards, Modal, …
|
||||||
│ │ ├─ contexts/ AuthContext, SiteContext
|
│ │ ├─ contexts/ AuthContext, SiteContext
|
||||||
│ │ ├─ api/client.js fetch wrapper (sends cookies)
|
│ │ ├─ api/client.js fetch wrapper (sends cookies)
|
||||||
@@ -188,6 +188,7 @@ npm start # node server → serves API + SPA at http://localhost:3
|
|||||||
| `/admin/settings` | Site settings |
|
| `/admin/settings` | Site settings |
|
||||||
| `/admin/activity` | Activity log |
|
| `/admin/activity` | Activity log |
|
||||||
| `/admin/users` | User management |
|
| `/admin/users` | User management |
|
||||||
|
| `/admin/account` | Account security (self-service TOTP two-factor) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -195,9 +196,9 @@ npm start # node server → serves API + SPA at http://localhost:3
|
|||||||
|
|
||||||
| Group | Base | Auth |
|
| Group | Base | Auth |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Auth | `/api/v1/auth` (`login`, `logout`, `me`) | cookie |
|
| Auth | `/api/v1/auth` (`login`, `login/totp`, `logout`, `me`) | cookie |
|
||||||
| Public | `/api/v1/public` (`settings`, `status`, `posts/:category`, `posts/:category/:idOrSlug`, `wiki`, `wiki/:slug`, `contact`) | none |
|
| Public | `/api/v1/public` (`settings`, `status`, `posts/:category`, `posts/:category/:idOrSlug`, `wiki`, `wiki/:slug`, `contact`) | none |
|
||||||
| Admin | `/api/v1/admin` (`dashboard`, `site-mode`, `posts`, `posts/upload`, `wiki`, `settings`, `activity`, `users`) | cookie (admin) |
|
| Admin | `/api/v1/admin` (`dashboard`, `site-mode`, `posts`, `posts/upload`, `wiki`, `settings`, `activity`, `users`, `account`, `account/totp/*`) | cookie (admin) |
|
||||||
|
|
||||||
Post categories (URL form): `news`, `five-on-friday`, `newsletter`, `screenshots`.
|
Post categories (URL form): `news`, `five-on-friday`, `newsletter`, `screenshots`.
|
||||||
See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) §4 for the full contract.
|
See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) §4 for the full contract.
|
||||||
@@ -212,6 +213,7 @@ Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `NODE_ENV` | `production` | |
|
| `NODE_ENV` | `production` | |
|
||||||
| `PORT` | `3000` | server listens on `0.0.0.0:PORT` |
|
| `PORT` | `3000` | server listens on `0.0.0.0:PORT` |
|
||||||
|
| `UPLOAD_DIR` | `<server>/uploads` | where post images are written (`/app/uploads`, volume-mounted, in Compose) |
|
||||||
| `DB_HOST` / `DB_PORT` | `db` / `3306` | `db` in Compose; `127.0.0.1` for local dev |
|
| `DB_HOST` / `DB_PORT` | `db` / `3306` | `db` in Compose; `127.0.0.1` for local dev |
|
||||||
| `DB_NAME` / `DB_USER` / `DB_PASSWORD` | `uomysticmoon` / `uomm` / — | app database credentials |
|
| `DB_NAME` / `DB_USER` / `DB_PASSWORD` | `uomysticmoon` / `uomm` / — | app database credentials |
|
||||||
| `DB_ROOT_PASSWORD` | — | MariaDB root (Compose only) |
|
| `DB_ROOT_PASSWORD` | — | MariaDB root (Compose only) |
|
||||||
@@ -219,6 +221,10 @@ Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.
|
|||||||
| `JWT_EXPIRES_IN` | `1d` | token + cookie lifetime |
|
| `JWT_EXPIRES_IN` | `1d` | token + cookie lifetime |
|
||||||
| `COOKIE_SECURE` | `auto` | `auto` = Secure only over HTTPS (works on LAN HTTP + Pangolin HTTPS) |
|
| `COOKIE_SECURE` | `auto` | `auto` = Secure only over HTTPS (works on LAN HTTP + Pangolin HTTPS) |
|
||||||
| `COOKIE_NAME` | `uomm_token` | |
|
| `COOKIE_NAME` | `uomm_token` | |
|
||||||
|
| `TRUST_PROXY` | `1` | reverse-proxy trust for correct `req.ip` / `req.secure` (rate limiting, backoff, bot-ban). Pin to the proxy hop's LAN IP in prod. A blanket `true` is rejected (coerced to `1`) to block `X-Forwarded-For` spoofing |
|
||||||
|
| `DEBUG_TRUST_PROXY` | `0` | `1` logs raw peer address + `X-Forwarded-For` + resolved `req.ip` per request (to verify/refresh the proxy IP). Noisy — leave off |
|
||||||
|
| `TOTP_ISSUER` | `UOMysticmoon` | label shown in authenticator apps for optional per-user 2FA |
|
||||||
|
| `TOTP_CHALLENGE_TTL` | `5m` | lifetime of the short-lived post-password "awaiting code" step |
|
||||||
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | — | first-admin bootstrap (first boot only) |
|
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | — | first-admin bootstrap (first boot only) |
|
||||||
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` | — | optional; blank → contact form uses `mailto:` |
|
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` | — | optional; blank → contact form uses `mailto:` |
|
||||||
| `CONTACT_TO` | `UOMysticmoon@gmail.com` | contact recipient |
|
| `CONTACT_TO` | `UOMysticmoon@gmail.com` | contact recipient |
|
||||||
@@ -230,11 +236,38 @@ Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.
|
|||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
JWT in an httpOnly, `SameSite=Lax` cookie (`Secure` auto-detected) · bcrypt hashing · login &
|
**Session & authorization**
|
||||||
contact rate limiting · `express-validator` on writes · `helmet` · admin routes `noindex` +
|
|
||||||
`robots.txt` disallow · `trust proxy` for correct client IPs behind Pangolin · first admin seeded
|
- JWT in an httpOnly, `SameSite=Lax` cookie (`Secure` auto-detected), bcrypt password hashing.
|
||||||
from env (no hardcoded credentials) · `.env` git-ignored. Passwords and request bodies are never
|
- Admin routes are **re-validated against the database on every request**, so a demoted or deleted
|
||||||
logged. SMTP is optional — the contact form falls back to a `mailto:` link when unconfigured.
|
user loses access immediately instead of keeping their old role until the token expires.
|
||||||
|
- **Role-based authorization** — admin-only endpoints (users, site mode, settings) are gated by a
|
||||||
|
`requireRole` check, so a lower-privilege editor can't reach them.
|
||||||
|
|
||||||
|
**Login hardening**
|
||||||
|
|
||||||
|
- **Optional per-user TOTP two-factor** (opt-in, self-service on `/admin/account`). When enabled,
|
||||||
|
the password step issues only a short-lived, non-session `stage:'totp'` challenge; a session
|
||||||
|
cookie is granted only after the second factor verifies.
|
||||||
|
- **Login throttling** — `express-slow-down` + a hard rate cap + a separate per-IP exponential
|
||||||
|
backoff, with generic error messages that don't reveal whether the username exists.
|
||||||
|
- **Honeypot** field on the login form; submissions that fill it are treated as bots.
|
||||||
|
- **Bot-scoring + automatic IP ban** — weighted scoring of CMS-scanner paths and junk 404s (with a
|
||||||
|
periodic sweep of stale entries) bans hostile scanners; failed logins and honeypot hits feed the
|
||||||
|
score.
|
||||||
|
|
||||||
|
**Uploads & input**
|
||||||
|
|
||||||
|
- Uploaded file extensions are derived from the **validated mimetype**, not the client-supplied
|
||||||
|
filename (prevents a disguised-extension upload).
|
||||||
|
- `express-validator` on all writes; usernames are validated **and** uniqueness-checked on update.
|
||||||
|
|
||||||
|
**Platform**
|
||||||
|
|
||||||
|
- `helmet`, admin routes `noindex` + `robots.txt` disallow, `trust proxy` for correct client IPs
|
||||||
|
behind Pangolin (see `TRUST_PROXY`), first admin seeded from env (no hardcoded credentials),
|
||||||
|
`.env` git-ignored. Passwords and request bodies are never logged. SMTP is optional — the contact
|
||||||
|
form falls back to a `mailto:` link when unconfigured.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user