# Buy Local Lowveld — System Map (Phase 2b) How the pieces connect now that Phase 2b has added PayFast, cron automations, rate limiting, and a minimal admin panel. ## Bird's-eye view ``` +--------------------+ +--------------------+ +------------+ | Public visitor | | Logged-in member | | Admin | +----------+---------+ +----------+---------+ +------+-----+ | | | | HTTPS | HTTPS + session | HTTPS + admin session v v v +-------------------------------------------------------------------+ | /buylocal/ (Apache/PHP) | | | | Public pages | Member portal | Admin panel | | Auth pages | /member/*.php | /admin/*.php | | Form handlers | Checkout flows | invoices, cron-status | | Cron scripts | ITN receiver | Rate-limit guard | +--+------+------+-----------+-----------+--------------------------+ | | | | | | v | v v | MySQL | +-----------+ +-----------+ | | | PayFast | | Mailchimp | | | | sandbox/ | | Marketing | | | | live | | API | | | +----+------+ +-----------+ | | | | | <-- ITN webhook (POST async) v v ``` Three external systems now: **MySQL** (DB), **PayFast** (payments, pushes us ITNs async), **Mailchimp** (audience sync + tags + journeys). State source-of-truth for payment status is the `payments` table after ITN verification; nothing else. ## Request-flow summary | Visitor does… | Pages involved | DB writes | Mailchimp calls | |---|---|---|---| | Browses directory | `directory.php`, `directory-item.php` | none | none | | Signs up | `become-member.php` → `signup-submit.php` | members insert | upsert + tier + lifecycle + journey + Become-a-Member tag | | Logs in | `login.php` | members.last_login_at update | none | | Edits profile | `member/edit-business.php` | members update + listings upsert | **upsert + tier tag + profile-update journey** (Flow D) | | Adds to cart | `member/add-to-cart.php` | orders, order_items | none | | Checks out | `member/cart.php` | orders status, invoices insert, transactions insert | none (Phase 2b adds `Payment Received` on PayFast success) | | Resets password | `forgot-password.php` → `reset-password.php` | password_resets + members.password_hash | none | | Claims a listing | `claim-business.php` | business_claims insert | none (admin approves later) | | Uses contact form | `contact.php` → `contact-submit.php` | none | upsert + `Lead` + `Contact Form` + journey | ## File map (Phase 2a) ``` buylocal/ | | public pages --------------------------------- |-- index.php Home |-- learn-more.php About |-- directory.php Grid w/ search + filter (DB-backed) |-- directory-item.php Single listing (DB-backed) |-- become-member.php Pricing + signup form (CSRF'd) |-- additional-branding.php Branding shop (DB-backed) |-- blog.php / blog-post.php Blog (DB-backed) |-- contact.php Contact form (CSRF'd) |-- login.php / logout.php |-- forgot-password.php / reset-password.php |-- claim-business.php | | form / event handlers ------------------------- |-- signup-submit.php Create DB member + email password + sync MC |-- contact-submit.php Log + sync to MC as Lead |-- newsletter-submit.php Simple MC upsert | | member portal (guarded) ----------------------- |-- member/_guard.php Auth gate + shared layout top |-- member/_footer.php Shared layout bottom |-- member/welcome.php Dashboard |-- member/edit-business.php ★ fires Mailchimp Flow D |-- member/view-statement.php Ledger + banking info |-- member/view-transactions.php Transactions w/ totals |-- member/cart.php View/update/checkout |-- member/add-to-cart.php POST handler from additional-branding | | infrastructure -------------------------------- |-- includes/config.php Credentials + tag taxonomy + journeys |-- includes/db.php PDO helper library |-- includes/session.php Session bootstrap |-- includes/csrf.php Token generate/verify |-- includes/auth.php Login, guards, reset tokens |-- includes/mail.php Thin mail() wrapper |-- includes/mailchimp.php API client + mc_sync_member_from_db() |-- includes/header.php Shared top w/ auth-aware nav |-- includes/footer.php Shared bottom | | data (Phase 1 dummy; now fallback) ------------ |-- data/{categories,listings,packages,blog}.php | | assets / docs --------------------------------- |-- assets/css/style.css |-- assets/js/main.js |-- db/schema.sql MySQL DDL |-- db/seed.php One-shot data import |-- docs/SYSTEM-MAP.md This file |-- docs/MAILCHIMP-FLOWS.md Per-event diagrams |-- docs/DB-SCHEMA.md Table-by-table |-- README.md ``` ## Auth model Sessions are stored in PHP's default file backend, cookie is `HttpOnly` + `SameSite=Lax` + `Secure` when HTTPS is present. Session name is `BLSESS`. On login we regenerate the session ID to prevent fixation. CSRF token rotates on login/logout and is verified on every POST handler. Guards: - `auth_require_login()` — redirects to `/login.php?next=...` if not signed in - `auth_require_admin()` — same + checks `role = 'admin'` Password storage: bcrypt, cost factor 12 (configurable in `AUTH_BCRYPT_COST`). Password reset: emailed link contains a raw random token; we only store its SHA-256 hash. 2-hour expiry, single-use. ## Where secrets live Everything sensitive is in `includes/config.php`: - `MC_API_KEY` — Mailchimp key - `MC_AUDIENCE_ID` — which list to sync to - `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASS` — MySQL credentials For production, move this file outside the web root or populate from environment variables. Add it to `.gitignore` before committing anywhere. ## Logs Two plain-text logs are written to the project root: - `mailchimp.log` — every API failure + signup / profile-update event - `contact-messages.log` — copy of every contact-form submission Plus one DB table: - `mailchimp_sync_log` — structured audit trail of every sync call The DB log is queryable from the admin panel (Phase 2c). The plain text logs are there for post-mortem debugging when the DB itself is the problem. ## Security posture (Phase 2a) | Threat | Mitigation | |---|---| | Password leak | Bcrypt hash (cost 12); no plaintext storage | | CSRF | Token on every form; `csrf_verify()` at top of every POST handler | | Session fixation | `session_regenerate_id(true)` on login | | Account enumeration | "Forgot password" returns success regardless of whether email exists | | Token replay | Reset tokens SHA-256 hashed + single-use + 2h expiry | | SQL injection | PDO prepared statements everywhere | | XSS | `htmlspecialchars()` around every dynamic value in templates | | Clickjacking | (Not addressed yet — add `X-Frame-Options: DENY` via webserver config) | | Brute-force login | (Not addressed yet — Phase 2b adds rate limiting) | --- _For per-event Mailchimp behaviour, see `docs/MAILCHIMP-FLOWS.md`._ _For the database design, see `docs/DB-SCHEMA.md`._