From 7d3cc87da904d5425368f72b7b10ea207059cf7d Mon Sep 17 00:00:00 2001 From: Lovebug MCP Date: Wed, 10 Jun 2026 03:09:24 +0000 Subject: [PATCH] =?UTF-8?q?mcp:=20project-plan=20=E2=80=94=20PBS=20Members?= =?UTF-8?q?hip=20LOE=201=20Phase=201=20=E2=80=94=20Backend=20Foundation=20?= =?UTF-8?q?(v3,=20build-ready)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pbs-membership-loe1-phase1-version3.md | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 Sources/Dev/pbs-membership-loe1-phase1-version3.md diff --git a/Sources/Dev/pbs-membership-loe1-phase1-version3.md b/Sources/Dev/pbs-membership-loe1-phase1-version3.md new file mode 100644 index 0000000..1d01a3e --- /dev/null +++ b/Sources/Dev/pbs-membership-loe1-phase1-version3.md @@ -0,0 +1,234 @@ +--- +created: '2026-06-10' +path: Sources/Dev +project: pbs-membership-loe1-phase1-version3 +tags: +- pbs +- membership +- loe1 +- flask +- mysql +- wordpress +- auth +- docker +type: project-plan +--- + +## Supersedes / Source-of-Truth Note + +This is the build-ready, canonical Phase 1 plan. It supersedes both prior docs: + +- **`pbs-membership-loe1-recipe-saving`** — stale on database name (`pbs_hub`), topology (extension of pbs-hub), and auth (Python reimplementation of WordPress cookie HMAC validation). **Architecture is wrong; implementation specifics were correct** and are carried forward here. +- **`pbs-membership-loe1-phase1-backend-foundation`** — correct architecture and auth, but written without the original in context, which de-specified the SQL schema and collapsed the API surface to a single vague checkbox. **Architecture is right; build detail was thin** and is restored here. + +Where any of the three disagree, **this doc wins**. + +--- + +## Goal + +Build the backend foundation for membership: the database, the data layer, and the WordPress-side auth endpoint that everything else (the Phase 2 save button, the member area later) builds on. No member-facing UI in this phase. + +**Success =** the data layer exists, a member's WordPress login can be validated server-side via WordPress itself, and an authenticated request can write/read/delete an interaction keyed to a WordPress user — testable via curl and verifiable in phpMyAdmin. + +Estimated time: 4–6 hours (carried from original; auth approach is smaller to build than the original's HMAC reimplementation, so this is conservative). + +--- + +## Locked Decisions + +### Identity vs. features +- **WordPress (Ultimate Member) owns identity** — registration, login, profiles, roles/tiers. The membership backend never manages credentials. +- **The membership backend (the Garden) owns features** — saves, and later likes/cooked/planned/rated, preferences. +- **The bridge** is the validated WordPress session plus read-only access to WordPress tables. + +### Topology +- The membership data layer belongs to a **standalone Flask application (the Garden) in its own container**, separate from `pbs-hub`. `pbs-hub` is a live team/production tool; the Garden must not share its deploy lifecycle or blast radius. +- Shared **MySQL server**, but **no shared application tables** and **no shared application logic** with `pbs-hub`. + +### Database — `pbs_garden` +- The Garden **owns a dedicated database, `pbs_garden`**, on the shared MySQL server. +- Holds the **generic interactions model** — a single `user_interactions` table serving all interaction types (save / like / cooked / planned / rated) rather than one table per feature, plus `user_preferences`. +- `wp_user_id` (the WordPress user ID, returned by the auth contract) is the foreign key on interaction rows. +- **Read-only, one-directional bridge to WordPress tables** (`wp_posts` for recipe title/slug). +- Scoped MySQL user: full control on `pbs_garden`, read-only on the specific WordPress tables needed, nothing else. No root for application access. + +#### Concrete schema (restored from original; confirm post type before first migration — see Open Items) + +```sql +CREATE DATABASE pbs_garden; + +CREATE TABLE pbs_garden.user_interactions ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + wp_user_id INT UNSIGNED NOT NULL, + wp_post_id BIGINT UNSIGNED NOT NULL, + action VARCHAR(50) NOT NULL, + metadata JSON NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY unique_action (wp_user_id, wp_post_id, action) +); + +CREATE TABLE pbs_garden.user_preferences ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + wp_user_id INT UNSIGNED NOT NULL, + pref_key VARCHAR(100) NOT NULL, + pref_value JSON NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_pref (wp_user_id, pref_key) +); +``` + +`wp_post_id` is `BIGINT UNSIGNED` to match WordPress's post ID type. (Instagram-style IDs are not relevant here — that VARCHAR concern is from the content-hub work, not this.) + +#### Scoped MySQL user (restored from original; database name updated) + +```sql +CREATE USER 'pbsgarden_app'@'%' IDENTIFIED BY 'strong_password_here'; + +-- Read-only on WordPress (auth + recipe display) +GRANT SELECT ON wordpress.wp_posts TO 'pbsgarden_app'@'%'; + +-- Full control on pbs_garden +GRANT ALL ON pbs_garden.* TO 'pbsgarden_app'@'%'; + +FLUSH PRIVILEGES; +``` + +Note: the app no longer needs `SELECT` on `wp_users` / `wp_usermeta` for auth — WordPress validates its own cookie and returns the user. The only WordPress read the app needs is `wp_posts` for recipe title/slug. (Audit existing MySQL users — pbs-api, n8n — and scope them off root while you're in here.) + +### Auth — mechanism (Option 2: WordPress validates its own cookie) +- On an authenticated request, the Garden forwards the incoming `wordpress_logged_in_*` cookie to a small **internal** WordPress endpoint (mu-plugin / REST route). WordPress reads its own session server-side and returns the user. +- Chosen for **security** (WP secret keys and auth crypto stay inside hardened WordPress — never copied into the Garden) and **reliability** (WordPress cannot drift out of sync with its own session format; survives WP/plugin/security-stack changes). The Garden already depends on WordPress at request time for read-only recipe data, so no runtime-independence is lost. +- Endpoint is **internal-network only** (server-to-server on the Docker network), **never exposed via Traefik/Cloudflare.** This constraint is load-bearing for security — an exposed endpoint would be an unauthenticated "who is this cookie" oracle. +- Validation responses are **cached per-session, short TTL (~5 min)**, so WordPress isn't queried on every request. Logout/expiry still works because the cookie itself becomes invalid. + +### Auth — contract (WordPress endpoint → Garden) + +Authenticated: +```json +{ + "authenticated": true, + "user_id": 47, + "username": "janedoe", + "display_name": "Jane Doe", + "email": "jane@example.com", + "roles": ["subscriber"] +} +``` +Unauthenticated: +```json +{ "authenticated": false } +``` + +- `user_id` — **critical**; foreign key into `pbs_garden` interaction rows (maps to `wp_user_id`). +- `roles` — **array**; membership **tiers are expressed as WordPress roles** (WordPress/Ultimate Member assigns them; the Garden is a pure reader). No separate tier field, no second source of truth. Multi-tier supported from day one. +- `email` — included now (cheap; wanted for later account/notification features). + +### API endpoint surface (restored from original; auth row reflects new mechanism) + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| POST | `/api/interactions` | Create interaction (save, like, etc.) | Required | +| DELETE | `/api/interactions` | Remove interaction | Required | +| GET | `/api/interactions?action=save` | List user's interactions by action type | Required | +| GET | `/api/interactions/check?post_id=123&action=save` | Check if a specific interaction exists | Required | + +The **check endpoint is not optional** — Phase 2's save button needs it to render a recipe as already-saved on page load. + +**Request body (POST):** +```json +{ "post_id": 123, "action": "save", "metadata": null } +``` + +**Response (GET list):** joins `pbs_garden.user_interactions` with `wordpress.wp_posts` to return title and slug: +```json +{ + "interactions": [ + { + "post_id": 123, + "action": "save", + "post_title": "Southern Black-Eyed Pea Stew", + "post_slug": "southern-black-eyed-pea-stew", + "saved_at": "2026-04-06T12:00:00" + } + ] +} +``` + +**`wp_posts` join filter (restored from original — load-bearing):** filter on `post_status = 'publish'` and `post_type IN ('post', 'wprm_recipe')` to exclude revisions, drafts, autosaves, and other post types. Forgetting this returns garbage rows. + +### Tier authorization — future-proofed, not built +Tier **gating** is out of scope for Phase 1 (everything is free). The pattern, for when tiers exist: + +```python +TIER_PERMISSIONS = { + "free": ["save", "like"], + "basic": ["save", "like", "planned", "cooked"], + "premium": ["save", "like", "planned", "cooked", "collections", "ai_suggest"], +} +``` + +Adding paid tiers later is a code change, not a schema change. The contract already carries `roles[]`, so the source of truth is in place; only the enforcement layer is deferred. + +--- + +## Open Items + +- [ ] **Recipe post type:** are recipes the `wprm_recipe` post type or regular WordPress `post` with recipe metadata? Determines the `post_type IN (...)` filter on the `wp_posts` join. Confirm before first migration. +- [ ] **`user_interactions` final columns:** schema above is the intended shape; confirm column set and the `wp_post_id` type matches WordPress (`BIGINT UNSIGNED`) before the first migration. +- [ ] **Ultimate Member tier roles:** confirm UM can assign tier roles/capabilities the way tiering will need. Contract is unaffected either way; flag if awkward. +- [ ] **WordPress endpoint form:** mu-plugin vs. custom REST route for the internal validation endpoint — pick at build, both satisfy the contract. + +--- + +## Build Phases + +### Phase 1 deliverables checklist +- [ ] Create `pbs_garden` database on staging MySQL +- [ ] Create `user_interactions` table +- [ ] Create `user_preferences` table +- [ ] Create `pbsgarden_app` MySQL user with scoped permissions (full on `pbs_garden`, read-only on `wp_posts`) +- [ ] Audit and scope existing MySQL users (pbs-api, n8n) off root +- [ ] Standalone Garden Flask app skeleton (its own container) with data layer / models +- [ ] WordPress-side internal validation endpoint (mu-plugin or REST route) returning the contract +- [ ] App-side auth: forward cookie → call endpoint → cache per-session (~5 min TTL) → attach `user_id` to request +- [ ] `POST /api/interactions` endpoint +- [ ] `DELETE /api/interactions` endpoint +- [ ] `GET /api/interactions` endpoint (with `wp_posts` join + publish/post-type filter) +- [ ] `GET /api/interactions/check` endpoint +- [ ] Test all endpoints via curl +- [ ] Verify data in phpMyAdmin +- [ ] Verify validation endpoint is unreachable from the public internet (internal-network only) +- [ ] Error handling and logging + +### Out of scope +- Save button UI on WordPress (Phase 2) +- Member area / browsing UI (later) +- Tier *gating* enforcement (the contract carries roles; enforcing comes when tiers exist) + +--- + +## Notes + +### LOE 1 phase roadmap (orienting context) +- **Phase 0** — Ultimate Member setup (registration, login, roles, basic profile) +- **Phase 1** — Backend foundation ← THIS DOC +- **Phase 2** — Save button on WordPress recipe pages (JS widget; needs the check endpoint) +- **Phase 3** — Saved recipes member area (the Garden frontend; see phase3a-garden-skeleton) +- **Phase 4** — Collections (named groups) +- **Phase 5** — Polish (mobile, loading states, error handling) + +### The four Lines of Effort +| LOE | Project | Status | +|-----|---------|--------| +| 1 | Recipe Saving | Active — Phase 1 | +| 2 | Weekly Meal Plan Viewer | Queued | +| 3 | Whole Food Diversity Tracker | Queued | +| 4 | User Profiles & Preferences | Queued | + +### Why the auth approach changed (for future readers) +The original doc had the Garden reimplementing WordPress's cookie HMAC validation in Python, which required copying `LOGGED_IN_KEY`/`LOGGED_IN_SALT` into the Garden and reproducing WP's crypto. That spreads auth secrets to a second app (security risk) and couples the Garden to WP's internal cookie format (breaks silently on WP/security-stack changes). Option 2 — WordPress validates its own cookie — keeps secrets sealed in hardened WordPress and cannot drift out of sync with itself. Chosen against the criteria "always works, always secure." + +### What this doc fixed relative to the two it supersedes +- From recipe-saving: corrected db name, topology, auth. +- From phase1-backend-foundation: restored the concrete SQL, the four-endpoint API surface (including the DELETE and check endpoints), and the `wp_posts` publish/post-type filter — all of which had been de-specified in the rewrite. \ No newline at end of file