- 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)
2.5 KiB
2.5 KiB
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:
POST /api/usersaccepted client-suppliedemailVerified,role,onboarded,stripeCustomerId,verificationToken— e.g. instant self-verified admin.- Self-
PATCH /api/users/{id}accepted the same fields on update. - 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,onboardedare stripped from every untrusted write inUsers.beforeChange(web/src/collections/Users.ts). Untrusted = no authenticated admin and nocontext.systemActivation === true. On create, the hook forcesrole: 'free',emailVerified: false,onboarded: false.- Server-side writers opt back in with
context: { systemActivation: true }on their local-APIpayload.update/createcalls. 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): abeforeOperationhook onloginpre-clearslockUntil/loginAttemptsviadb.updateOne(which, like Payload's ownincrementLoginAttempts, 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
beforeOperationhook 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.