Scope Ecommerce down to Lib helpers; prune stale Done entries in ISSUES.md
Replace the single Ecommerce entry with three composable Lib\ pieces (Money, Cart, payment gateway) instead of a framework-owned catalog/ checkout/order-admin system — a project's idea of a "product" is too site-specific to standardize, so it builds its own on Lib\Db like any other feature. Also removes four Done entries (In-house comments, Email verification, Media/file manager, User deletion & email addresses) that nothing open still depends on or references, per this file's own stated retention policy — their history remains in git log and the shipping commits. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+63
-185
@@ -58,52 +58,85 @@ expected vs. actual behavior. For features, include the motivating use case.>
|
|||||||
|
|
||||||
Suggested build order (foundations first):
|
Suggested build order (foundations first):
|
||||||
|
|
||||||
1. **In-house comments** — admin login & user management (Done
|
1. **Ecommerce: Lib\Money** — no dependencies, and the other two Ecommerce
|
||||||
2026-07-14) unblocked this; comments are tied to real user accounts,
|
pieces below both need it.
|
||||||
not anonymous.
|
2. **Ecommerce: Lib\Cart** — needs Lib\Money.
|
||||||
2. **Ecommerce functionality** — its dependencies (SQLite groundwork,
|
3. **Ecommerce: payment gateway helper** — needs Lib\Money; independent of
|
||||||
session handling for the cart, admin login for order/product admin)
|
Lib\Cart, so it could also go before it.
|
||||||
are all done now.
|
4. **Paywall functionality** — needs the payment gateway helper above for
|
||||||
3. **Paywall functionality** — needs everything Ecommerce needs, plus
|
its recurring-billing/payment plumbing; build after it rather than in
|
||||||
Ecommerce itself for the recurring-billing/payment-gateway plumbing;
|
parallel — also now has a concrete precedent to follow for the "gated
|
||||||
build after it rather than in parallel — also now has a concrete
|
content must skip the static cache" part of its design (see Draft
|
||||||
precedent to follow for the "gated content must skip the static cache"
|
pages (admin-only preview) in Done, and the caching/auth standing rule
|
||||||
part of its design (see Draft pages (admin-only preview) in Done, and
|
in `AGENTS.md`), which was still an open question when this entry was
|
||||||
the caching/auth standing rule in `AGENTS.md`), which was still an open
|
originally written.
|
||||||
question when this entry was originally written.
|
|
||||||
|
|
||||||
Session handling (with flash sessions), Draft pages (admin-only preview),
|
Session handling (with flash sessions), Draft pages (admin-only preview),
|
||||||
Admin login & user management, and Media/file manager all shipped (see
|
and Admin login & user management all shipped (see Done) — every open
|
||||||
Done) — 2026-07-14 for the first three, 2026-07-15 for Media/file manager.
|
entry above still depends on at least one of them. The original single
|
||||||
|
"Ecommerce functionality" entry was scoped down into the three
|
||||||
|
Lib\-helper pieces above on 2026-07-15 — see Lib\Money's entry for why.
|
||||||
|
|
||||||
See **Won't Do** below for 404 tracking, dropped in favor of Matomo.
|
See **Won't Do** below for 404 tracking, dropped in favor of Matomo.
|
||||||
|
|
||||||
### Ecommerce functionality
|
### Ecommerce: Lib\Money
|
||||||
|
|
||||||
- **Type:** Feature
|
- **Type:** Feature
|
||||||
- **Status:** Backlog
|
- **Status:** Backlog
|
||||||
- **Priority:** Low
|
- **Priority:** Low
|
||||||
- **Depends on:** SQLite groundwork (Done), Session handling (with flash sessions) (Done), Admin login & user management (Done)
|
- **Added:** 2026-07-15
|
||||||
- **Added:** 2026-07-12
|
|
||||||
|
|
||||||
Product catalog, cart, checkout, and order storage — a `products` /
|
First and smallest piece of the former "Ecommerce functionality" entry —
|
||||||
`orders` table in SQLite, a session-based cart (rides on the flash-session
|
scoped down (2026-07-15) from a full catalog/cart/checkout/order-admin
|
||||||
work above), and a payment gateway integration for actually taking money.
|
system to a handful of composable `Lib\` helpers, since a project's idea
|
||||||
Given the project's no-Composer/no-vendored-SDK philosophy, prefer calling
|
of a "product" is too site-specific to standardize; the project builds
|
||||||
a payment provider's HTTP API directly (e.g. Stripe's REST API via cURL)
|
its own catalog and admin UI on `Lib\Db` the same way it would for any
|
||||||
over vendoring a full SDK, same reasoning as vendoring only Twig's `src/`
|
other feature, the same split `Lib\Comments` already makes for what a
|
||||||
rather than pulling in a package manager. Needs a decision on which
|
"page" is. `Lib\Money`: integer-cents arithmetic (add/subtract/multiply
|
||||||
provider(s) to support first. Order/product management rides on admin
|
by a quantity) and formatting, avoiding the classic float-rounding bugs
|
||||||
login above. Large feature — likely worth its own sub-breakdown (catalog,
|
of storing prices as floats. No dependencies — the foundation `Lib\Cart`
|
||||||
cart, checkout, order admin) once it's actually picked up rather than
|
and the payment gateway helper below both need a non-lossy way to
|
||||||
planning it all up front here.
|
represent an amount before either can be built.
|
||||||
|
|
||||||
|
### Ecommerce: Lib\Cart
|
||||||
|
|
||||||
|
- **Type:** Feature
|
||||||
|
- **Status:** Backlog
|
||||||
|
- **Priority:** Low
|
||||||
|
- **Depends on:** Ecommerce: Lib\Money, Session handling (with flash sessions) (Done)
|
||||||
|
- **Added:** 2026-07-15
|
||||||
|
|
||||||
|
Second piece of the former "Ecommerce functionality" entry (see Lib\Money
|
||||||
|
above for the scoping note). A session-based cart primitive — add/remove/
|
||||||
|
update line items, line and cart totals via `Lib\Money` — riding on
|
||||||
|
`Lib\Session` the same way `Lib\Csrf`/`Lib\AdminAuth` lazily touch the
|
||||||
|
native session. Generic on purpose: the cart holds id/qty/price entries a
|
||||||
|
sidecar hands it, with no opinion on what a "product" is or where its
|
||||||
|
catalog data comes from.
|
||||||
|
|
||||||
|
### Ecommerce: payment gateway helper
|
||||||
|
|
||||||
|
- **Type:** Feature
|
||||||
|
- **Status:** Backlog
|
||||||
|
- **Priority:** Low
|
||||||
|
- **Depends on:** Ecommerce: Lib\Money
|
||||||
|
- **Added:** 2026-07-15
|
||||||
|
|
||||||
|
Third piece of the former "Ecommerce functionality" entry (see Lib\Money
|
||||||
|
above for the scoping note). A driver-dispatched `Lib\` class for taking
|
||||||
|
a payment, mirroring `Lib\Mailer`'s `mail_driver` config-key pattern —
|
||||||
|
Stripe first (one `charge()`-shaped call plus webhook signature
|
||||||
|
verification), calling the provider's REST API directly via cURL rather
|
||||||
|
than vendoring an SDK, same reasoning as vendoring only Twig's `src/`
|
||||||
|
rather than pulling in a package manager. Adding a second provider later
|
||||||
|
means one more driver case, same as `Lib\Mailer::sendMail()`.
|
||||||
|
|
||||||
### Paywall functionality
|
### Paywall functionality
|
||||||
|
|
||||||
- **Type:** Feature
|
- **Type:** Feature
|
||||||
- **Status:** Backlog
|
- **Status:** Backlog
|
||||||
- **Priority:** Low
|
- **Priority:** Low
|
||||||
- **Depends on:** Ecommerce functionality (recurring billing/payment plumbing), SQLite groundwork (Done), Session handling (with flash sessions) (Done), Admin login & user management (Done)
|
- **Depends on:** Ecommerce: payment gateway helper, SQLite groundwork (Done), Session handling (with flash sessions) (Done), Admin login & user management (Done)
|
||||||
- **Added:** 2026-07-12
|
- **Added:** 2026-07-12
|
||||||
|
|
||||||
Subscription/membership content gating, similar to OnlyFans/Patreon:
|
Subscription/membership content gating, similar to OnlyFans/Patreon:
|
||||||
@@ -130,161 +163,6 @@ _Nothing yet._
|
|||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
### In-house comments
|
|
||||||
|
|
||||||
- **Type:** Feature
|
|
||||||
- **Status:** Done
|
|
||||||
- **Priority:** Medium
|
|
||||||
- **Depends on:** Admin login & user management (Done), SQLite groundwork (Done)
|
|
||||||
- **Added:** 2026-07-14
|
|
||||||
- **Shipped:** 2026-07-15
|
|
||||||
|
|
||||||
A self-hosted comment thread — no third-party service (Disqus, Commento,
|
|
||||||
etc.) — `Lib\Comments` (`novaconium/lib/Comments.php`), a plain static
|
|
||||||
class any sidecar can call to attach comments to any page, not just blog
|
|
||||||
posts, the same way `Lib\SpamGuard`/`Lib\FormValidator` are reusable
|
|
||||||
across any form rather than hardcoded to the contact page. Comments are
|
|
||||||
tied to a real logged-in account, never anonymous name/email fields —
|
|
||||||
`App\AdminAuth::currentUser()` already excludes disabled and unverified
|
|
||||||
accounts (see Email verification below), so there's nothing further to
|
|
||||||
check before accepting a comment from whoever it returns.
|
|
||||||
|
|
||||||
**Framework-level, not `App/migrations/`.** The original backlog draft
|
|
||||||
sketched the migration under `App/migrations/`, but comments — like Admin
|
|
||||||
auth and Media manager — is reusable framework functionality, not project
|
|
||||||
content, so the migration (`novaconium/migrations/0004_create_comments.sql`),
|
|
||||||
the class, and the moderation UI (`novaconium/pages/admin/comments/`) all
|
|
||||||
live under `novaconium/`, consistent with every other feature of this
|
|
||||||
shape.
|
|
||||||
|
|
||||||
**Moderation model: auto-approve, then moderate.** A comment is visible
|
|
||||||
the instant it's posted — no pending queue — since only a verified
|
|
||||||
account can post one at all, there's no anonymous-spam vector to pre-vet
|
|
||||||
against. An admin can hide (excluded at the query level, not just
|
|
||||||
visually) or permanently delete any comment after the fact at
|
|
||||||
`/admin/comments`, mirroring how `/admin/users` disables rather than
|
|
||||||
pre-vets accounts. Submission still runs through `Lib\SpamGuard`'s
|
|
||||||
honeypot/timing check and `Lib\Csrf`, same as the contact form.
|
|
||||||
|
|
||||||
**A page needs its own sidecar to use it.** This codebase has no
|
|
||||||
client-side JS/fetch anywhere — every dynamic feature is a plain
|
|
||||||
server-rendered POST form — so a comment thread is no different: giving a
|
|
||||||
page a sidecar is what excludes it from the static HTML cache (only
|
|
||||||
sidecar-less pages are ever cached), an explicit per-page tradeoff the
|
|
||||||
same as `/admin/media` and `/search`. `App/pages/blog/comments-demo/` is
|
|
||||||
the one post under `App/pages/blog/` with a sidecar, added specifically
|
|
||||||
to demonstrate the pattern without disturbing the other posts (several of
|
|
||||||
which reference `hello-world` by name as the canonical *sidecar-less*
|
|
||||||
example — retrofitting it instead would have made those references
|
|
||||||
wrong). The reusable Twig half is
|
|
||||||
`novaconium/pages/_partials/comments/thread.twig` — a `_`-prefixed,
|
|
||||||
never-routable partial, included with `{% if comments is defined %}` so
|
|
||||||
a layout shared by comment-enabled and sidecar-less pages alike can
|
|
||||||
include it unconditionally. Documented at `/admin/docs/comments`.
|
|
||||||
|
|
||||||
### Email verification for user accounts
|
|
||||||
|
|
||||||
- **Type:** Feature
|
|
||||||
- **Status:** Done
|
|
||||||
- **Priority:** Medium
|
|
||||||
- **Depends on:** Admin login & user management (Done)
|
|
||||||
- **Added:** 2026-07-14
|
|
||||||
- **Shipped:** 2026-07-15
|
|
||||||
|
|
||||||
Every user created after the first must click a 24-hour link
|
|
||||||
(`bin2hex(random_bytes(32))`, same pattern as `Lib\Csrf::token()`) emailed
|
|
||||||
to their address before `AdminAuth::attempt()` lets them log in at all —
|
|
||||||
folded into the same generic pass/fail as a disabled account or a wrong
|
|
||||||
password, no oracle revealing which. Two exemptions, both following the
|
|
||||||
last-active-admin guard's reasoning that a safety mechanism can't depend on
|
|
||||||
something that might not be configured: the very first user (auto-verified
|
|
||||||
at creation, since there's no other admin to vouch for them, and they're
|
|
||||||
auto-logged-in the same instant) and every row that predates this migration
|
|
||||||
(`novaconium/migrations/0003_add_users_verification.sql` backfills
|
|
||||||
`verified_at` from `created_at`). `create-admin-user.php` also auto-
|
|
||||||
verifies, since lockout recovery can't depend on a working mail transport
|
|
||||||
either.
|
|
||||||
|
|
||||||
`/verify-email` follows `/admin/logout`'s GET-confirms/POST-mutates shape
|
|
||||||
(the content-index crawl forces a GET on every route, and mail-security
|
|
||||||
scanners prefetch links — either would burn a GET-mutates token); an
|
|
||||||
invalid/expired/already-used token never touches the database either way.
|
|
||||||
Changing an account's email at `/admin/users` resets verification and
|
|
||||||
re-sends to the new address; `/admin/users` also grew a per-row **Resend
|
|
||||||
verification** button, visible only when unverified.
|
|
||||||
|
|
||||||
`Lib\Mailer` grew a second method, `sendMail()`, for this and future
|
|
||||||
transactional mail (password-reset-by-email later, per the original spec)
|
|
||||||
— separate from `send()`, which the contact form keeps using unchanged.
|
|
||||||
`sendMail()` is driver-dispatched via a new `mail_driver` config key:
|
|
||||||
`'log'` (default) reuses the same `contact-log.txt` fallback so a fresh
|
|
||||||
checkout can create and verify accounts with zero external dependency;
|
|
||||||
`'mailjet'` sends through MailJet's Send API v3.1 (`mailjet_api_key`/
|
|
||||||
`mailjet_api_secret`/`mail_from_email`/`mail_from_name` in `App/config.php`
|
|
||||||
— not gitignored, same as every other config key, per the user's decision
|
|
||||||
that deployer-owned secrets are acceptable here). Adding another provider
|
|
||||||
later means one more `match` case in `sendMail()` — callers never change.
|
|
||||||
Documented in `/admin/docs/admin-auth`'s new "Email verification" section.
|
|
||||||
|
|
||||||
### Media/file manager
|
|
||||||
|
|
||||||
- **Type:** Feature
|
|
||||||
- **Status:** Done
|
|
||||||
- **Priority:** Medium
|
|
||||||
- **Added:** 2026-07-12
|
|
||||||
- **Shipped:** 2026-07-15
|
|
||||||
|
|
||||||
An upload/browse/delete UI for media (images, PDFs, etc.) at `/admin/media`
|
|
||||||
so sidecars and Twig templates have a consistent place to reference
|
|
||||||
uploaded files from — e.g. a blog post's header image — instead of authors
|
|
||||||
manually copying files into `public/`. Covered by the existing
|
|
||||||
`/admin/*` auth gate the moment the page was added, no extra wiring
|
|
||||||
needed — as planned, there's no separate `*_enabled` flag (unlike admin
|
|
||||||
auth/content index) since it has no SQLite dependency to gate. Backed by a
|
|
||||||
plain directory, `public/uploads/` (gitignored per-file with a tracked
|
|
||||||
`.gitkeep`, same convention as `data/`), rather than a database — no
|
|
||||||
metadata store (alt text, captions) yet; that's flagged as a natural
|
|
||||||
SQLite fit if wanted later, not built now. Also removed the top-level
|
|
||||||
`images/` scaffolding directory/volume (reserved for a future
|
|
||||||
image-upload feature, per the prior `AGENTS.md` wording): nothing had
|
|
||||||
ever consumed it, and `public/uploads/` now covers the use case it was
|
|
||||||
held open for. Safety handling: an extension
|
|
||||||
allowlist and max upload size, both configurable
|
|
||||||
(`media_upload_extensions`/`media_upload_max_bytes` in `App/config.php`),
|
|
||||||
plus filename sanitization (`basename()` + safe-charset reduction,
|
|
||||||
collision-safe via a `-1`/`-2`/... suffix) and a `realpath()` re-check on
|
|
||||||
delete to confirm the resolved path still lands inside `public/uploads/`
|
|
||||||
before unlinking. Documented at `/admin/docs/media-manager` (new topic,
|
|
||||||
linked from the docs nav/index and the admin dashboard), with a matching
|
|
||||||
README feature/docs-index entry.
|
|
||||||
|
|
||||||
### User deletion & email addresses
|
|
||||||
|
|
||||||
- **Type:** Feature
|
|
||||||
- **Status:** Done
|
|
||||||
- **Priority:** Medium
|
|
||||||
- **Depends on:** Admin login & user management (Done)
|
|
||||||
- **Added:** 2026-07-14
|
|
||||||
- **Shipped:** 2026-07-14 (b882c30)
|
|
||||||
|
|
||||||
Second same-day follow-up to Admin login & user management (below),
|
|
||||||
also pre-commit — so the `email` column went into the existing
|
|
||||||
`0002_create_users.sql` like the roles change before it. `/admin/users`
|
|
||||||
gained a hard-delete action (username/email become reusable; any live
|
|
||||||
session dies on its next request via the same per-request row re-check
|
|
||||||
disabling uses; the last-active-admin guard covers delete as well as
|
|
||||||
disable/demote) and a change-email action. Every account now requires a
|
|
||||||
unique email address — validated and normalized (trim + lowercase) via
|
|
||||||
the existing `Lib\Validate::isEmail()`, so uniqueness is
|
|
||||||
case-insensitive by construction (verified: `BOB@example.com` collides
|
|
||||||
with `bob@example.com`) — on the create form, the change-email action,
|
|
||||||
and `bin/create-admin-user.php` (now `<username> <email>`). Not used
|
|
||||||
for login or any mail yet; it exists so the planned email-verification
|
|
||||||
flow (new Backlog entry above) has an address for every account that
|
|
||||||
predates it. Delete stays deliberately distinct from disable in the UI
|
|
||||||
(inside a confirm-style `<details>` with a warning): disable is
|
|
||||||
keep-but-shut-out, delete is gone-for-good.
|
|
||||||
|
|
||||||
### User roles, groups & page access control
|
### User roles, groups & page access control
|
||||||
|
|
||||||
- **Type:** Feature
|
- **Type:** Feature
|
||||||
|
|||||||
Reference in New Issue
Block a user