Files
anwar-pi-website/docs/adr/0005-member-trust-model.md
AHMED 165efc3269 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)
2026-10-09 17:23:14 +03:00

48 lines
2.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.