Commit Graph
8 Commits
Author SHA1 Message Date
bermudalamb 89fc7c5c1b feat(backend): add an application-layer gate to the admin API (#63)
Authorization for the admin panel and the admin API has lived entirely in one auth_request regex in an Nginx Proxy Manager config outside this repository. That control is real and it works — nothing is publicly exposed today — but it is invisible from the code, untested here, and not reviewed when this code changes. Three things follow from that, and the first is the one worth the change.

An admin route added at a path the regex does not match is unprotected the moment it is written, and nothing in Express indicates that. Anything reaching the published container port directly bypasses authentik entirely. And locally there is no gate at all, so no developer ever sees the boundary being enforced.

requireAdminGate is attached to each admin router rather than to a path prefix, which is what makes it useful rather than merely redundant with the proxy. An admin router added later at some other path inherits the gate; because the proxy only injects the header on paths its regex matches, that router refuses on its first request instead of being quietly public. A 403 in that situation is the boundary reporting that it has drifted.

The gate is optional, and unset means exactly today's behaviour. That keeps local development and all 113 existing admin test call sites working untouched, and means shipping the image before configuring the proxy cannot take the admin panel down. What it does not do is stay silent about it: the server warns at boot when the gate is inactive, naming what is unprotected. This project has been bitten repeatedly by controls that report success while doing nothing, and an unconfigured gate should be a visible choice rather than an invisible one.

An empty value is treated as unset rather than as a secret, because enforcing an empty secret would admit any caller sending an empty header. Comparison is timing-safe over SHA-256 digests of both sides: timingSafeEqual throws on buffers of unequal length, so comparing raw values would turn a short header into a 500 rather than a 403, and a length check first would leak the secret's length.

Turning it on requires the secret in two places at once — the stack environment and a proxy_set_header line on the gated location in NPM. Setting only one gives 403s until the other catches up. That coupling, and the three consequences above, are now written into the README beside the deployment section, since none of it is visible from the code.

Verified over real HTTP as well as in tests. Booting without the secret logs the warning and serves admin normally; booting with it returns 403 for a missing header, 403 for a wrong one, 200 for the right one, and leaves the public storefront at 200 throughout, with each refusal logged distinguishably and without echoing the value it was sent. 8 new unit tests, 9 new integration tests covering every admin router separately — a correct middleware nobody mounted would pass the unit tests and protect nothing. 106 unit and 153 integration passing, lint 0 errors and 8 warnings unchanged.

Refs #63
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:44:15 -05:00
bermudalambandClaude Opus 5 c546bd5141 docs: make QA review a standing step in the change checklist
Records the two-environment layout and a numbered checklist every change
follows, with QA review as a required stop rather than a judgement call.
Production is not where a bad deploy should be found, which is what
happened with the categories/tags release.

Production promotion now retags the image QA reviewed rather than
rebuilding, so what ships is exactly what was tested, and the README
carries the full command sequence with a verification gate at each step.

Also records the two gates that have already failed here: confirming a
pushed commit is actually on the branch, and that a schema/code ordering
problem cannot be caught by any local suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 14:28:33 -05:00
bermudalambandClaude Opus 5 c01e434747 fix(qa): build the QA image before deploying the stack (#25)
SonarQube Analysis / sonarqube (pull_request) Successful in 3m48s
Tests / backend-unit (pull_request) Successful in 1m18s
Tests / backend-integration (pull_request) Failing after 4m12s
Tests / frontend-e2e (pull_request) Failing after 12m52s
Deploying the stack first failed with "pull access denied for
redefined-designs, repository does not exist or may require docker
login". The README had the Portainer stack creation ahead of the build,
so no local redefined-designs:qa image existed and Compose fell back to
pulling from Docker Hub, where the repository does not exist.

The build step now comes first in the one-time setup, and the review
workflow says to rebuild before restarting the stack — the stack builds
nothing itself and would otherwise run whatever was last tagged :qa.

pull_policy: never makes the failure legible: a missing local image now
reports itself as missing rather than as a registry authentication
problem. Noted as safe to remove if the NAS's Compose ever rejects the
key, since it only affects the error message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:29:14 -05:00
bermudalambandClaude Opus 5 47f00a3485 feat(qa): add the QA environment stack definition (#25)
A disposable stack for reviewing merged-but-undeployed changes online,
running alongside production on the same NAS.

Every value that could collide with production is changed: container
names, host port (32751), volume paths, database name, and image tag.
The Postgres data directory is the critical one — sharing production's
would mean QA writing into production's database files.

QA deliberately differs from production in three ways. DEMO_MODE=true
with no PayPal credentials, so the full cart and checkout path is
exercisable with no route to live PayPal. No SMTP configuration, so a QA
run cannot email anyone; the mailer already degrades gracefully when
unset. And restart: "no", so a NAS reboot does not quietly bring QA back
up and leave it running.

The stack must be named redefined-designs-qa in Portainer: the stack name
becomes the compose project name, and reusing production's would make
compose reconcile the two against each other and remove the production
containers.

README covers the one-time NAS directory setup, the review workflow, and
how to reset QA data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 11:17:52 -05:00
bermudalambandClaude Opus 5 c77fdad2b9 fix: run migrations on boot and stop failures rendering as empty (#23)
The storefront showed no inventory after deploying the categories/tags
release. No data was lost: the code queried categories/item_tags/
items.category_id against a database where the migration had not been
run, and that failure was invisible at every layer.

Three changes, each addressing one layer:

Migrations now run at container start, so deployed code cannot be ahead
of the schema and the easily-forgotten manual `docker exec migrate.js
up` step disappears. migrate.js waits for Postgres to accept
connections first, since the NAS brings the DB container up slower than
the app, and still exits non-zero so a bad migration stops the
container rather than serving a half-migrated schema.

Express 4 does not forward a rejected async handler, and no error
middleware was mounted, so a failing query never responded at all. Async
routes are now wrapped and an error middleware guarantees a 500. A hung
request is indistinguishable from an empty result in the UI, which is
how a schema mismatch came to read as "the store has no items".

The storefront now separates "request failed" from "no items" and offers
a retry. fetchItems/fetchFilterOptions throw on a non-OK response rather
than returning the parsed error body, which would have been set as the
item list and crashed the grid on .map.

Also restores the project-context update from 7fb5764, which was left
out of PR #24 and ended up dangling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:41:24 -05:00
bermudalamb 7f4479605a chore: replace manual SQL migrations with node-pg-migrate
SonarQube Analysis / sonarqube (pull_request) Successful in 2m42s
Tests / backend-unit (pull_request) Successful in 34s
Tests / backend-integration (pull_request) Successful in 50s
Tests / frontend-e2e (pull_request) Failing after 38s
2026-08-14 10:05:09 -05:00
bermudalamb da240621db docs: fix local-dev commands for PowerShell, document branching/commit conventions
SonarQube Analysis / sonarqube (push) Successful in 4m42s
Tests / backend-unit (push) Successful in 49s
Tests / backend-integration (push) Successful in 51s
Tests / frontend-e2e (push) Failing after 40s
2026-08-14 08:50:46 -05:00
bermudalamb 921022c658 Add backend unit/integration tests, Playwright e2e tests, README, cookie Secure fix
SonarQube Analysis / sonarqube (push) Successful in 4m20s
2026-08-13 22:35:38 +00:00