Docs: new README (Arabic flows, screenshots, architecture) + AGENT.md for AI agents
- README rewritten for the real Next.js + Payload + Postgres stack (old one described the dead static prototype) - Logo + 4 live screenshots (home, articles, post, subscription) - Arabic flow explanations with mermaid diagrams: visitor journey, content pipeline, request path - Architecture table, quick start, API reference, security summary, deploy commands - AGENT.md: orientation for future AI agents (data model, trust rules, rate limits, routes, env, deploy, house rules) - GLOSSARY.md and docs/adr committed (linked from README)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# ADR 0005 — Member trust model (server-only privilege/verification/billing fields)
|
||||
|
||||
Date: 2026-10-09
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The 2026-10-08 pentest (findings #1, #3, #4, #6) confirmed that anonymous or self-authenticated
|
||||
client requests could write privileged fields on `users`:
|
||||
|
||||
1. `POST /api/users` accepted client-supplied `emailVerified`, `role`, `onboarded`,
|
||||
`stripeCustomerId`, `verificationToken` — e.g. instant self-verified admin.
|
||||
2. Self-`PATCH /api/users/{id}` accepted the same fields on update.
|
||||
3. Per-account login lockout (no time-box) let an attacker lock the admin out of the site
|
||||
indefinitely with wrong-password floods.
|
||||
|
||||
## Decision
|
||||
|
||||
- `role`, `emailVerified`, `verificationToken`, `stripeCustomerId`, `onboarded` are stripped from
|
||||
every **untrusted write** in `Users.beforeChange` (`web/src/collections/Users.ts`). Untrusted =
|
||||
no authenticated admin **and** no `context.systemActivation === true`. On create, the hook forces
|
||||
`role: 'free'`, `emailVerified: false`, `onboarded: false`.
|
||||
- Server-side writers opt back in with `context: { systemActivation: true }` on their local-API
|
||||
`payload.update`/`create` calls. Enumerated writers: `verify-email`, `resend-verification`,
|
||||
`subscribe`, `stripe/webhook` (×2), `payment-success`, `seed`. **Omitting the context silently
|
||||
no-ops the write (the field is deleted) — that is the intended failure mode**, so a missing
|
||||
context is caught in QA rather than leaking privilege.
|
||||
- Login lockout is per-account, time-boxed: 5 failed attempts → 15-minute lock (`auth:
|
||||
{ maxLoginAttempts: 5, lockTime: 15 * 60 * 1000 }`).
|
||||
- `role === 'admin'` accounts are **exempt from lockout** (owner decision 2026-10-09): a
|
||||
`beforeOperation` hook on `login` pre-clears `lockUntil`/`loginAttempts` via
|
||||
`db.updateOne` (which, like Payload's own `incrementLoginAttempts`, bypasses hooks). Free/VIP
|
||||
accounts keep the normal 5/15-min lockout.
|
||||
- Commenting requires a verified email; admins are exempt (see the 403 gate in
|
||||
`api/posts/[slug]/comments`).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every future server-side writer of these fields MUST pass `context.systemActivation`; a writer
|
||||
that forgets it silently loses its write.
|
||||
- The admin password has **no brute-force throttle** — compensate with a strong password. If that
|
||||
posture changes, delete the `beforeOperation` hook and the account falls back to 5/15-min lockout.
|
||||
- Client-sent profile fields (`fullName`, `termsVersion`, `interest`, `level`, `goal`, `prefs`) stay
|
||||
writable; only privilege/verification/billing fields are protected.
|
||||
Reference in New Issue
Block a user