Audit: posting pipeline / auth / API surface review with prioritized P1-P3 findings and a morning checklist; P1-1 (prod 2M upload limit) marked resolved 2026-07-09 via the CGI->FPM switch in Webmin. Ideation: 7 ranked, repo-grounded ideas (top pick: OG meta + RSS follow-along stack). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
171 lines
9.4 KiB
Markdown
171 lines
9.4 KiB
Markdown
# Pre-Trip Readiness Audit — 2026-07-08 (overnight)
|
||
|
||
**Scope:** everything the trip depends on from the road — the posting pipeline
|
||
(/post → cache-on-save → add-page-by-form), photo handling, edit mode, auth &
|
||
sessions, GPX manager, the custom API surface, and prod's anonymous exposure.
|
||
**Method:** read-only code audit of the current `main` + anonymous HTTP probes
|
||
against production. **No code was changed.** Findings are prioritized; a
|
||
10-minute morning checklist is at the bottom.
|
||
|
||
---
|
||
|
||
## What was verified and looks solid ✅
|
||
|
||
- **Prod anonymous surface holds.** Probed 2026-07-08 (UTC night): `GET
|
||
/api/v1/pages` → 401 with a clean JSON error; `/post` and `/gpx-manager`
|
||
render the login form; no `X-Powered-By` leak. API CORS is same-origin
|
||
(`origins: {}`), rate limiting on (120 req/60s), session auth enabled.
|
||
- **The custom API routes are properly hardened.** `entry-actions`
|
||
(DELETE entry / reorder photos / trip publish) all require the authenticated
|
||
**owner** (`site.owner_username`, not just any login), enforce
|
||
`api.pages.write`, validate slugs as safe single segments, and resolve
|
||
targets through the page tree via the shared `EntryScopeGuard` — no raw path
|
||
concatenation anywhere. The publish route handles the APCu/in-place-write
|
||
cache gotcha explicitly and never turns a cache-invalidation failure into a
|
||
fake 500. Audit logging on all three.
|
||
- **Text can't be lost while composing.** `post-form.js` mirrors every text
|
||
field to localStorage on each keystroke and clears the draft **only** on a
|
||
server-confirmed success notice. Any failure path (validation error, expired
|
||
session, network drop, closed tab) re-offers the text on the next visit.
|
||
- **HEIC handling fails closed.** Sniffed from bytes (not filename), converted
|
||
client-side, submit is gated while a conversion is in flight, and a failed
|
||
conversion skips the file with a visible message instead of uploading a
|
||
broken HEIC.
|
||
- **Photo reconcile is fail-safe.** Runs exactly once per submit (latched),
|
||
an empty/missing `photo_order` touches nothing, only image extensions are
|
||
ever deleted, and edit-mode targets resolve through the same scope guard.
|
||
- **Edit-mode photo editor has honest error paths.** Failed reorder → revert
|
||
to last-known-good; failed refresh after a successful save → keeps the saved
|
||
order; failed batch-add → rollback with an explicit warning when rollback
|
||
itself was incomplete; 404 on delete treated as convergent success.
|
||
- **Cache invalidation on post/edit is correct even under prod caching.**
|
||
cache-on-save does `deleteAll()` + `Cache::invalidateCache()` (config
|
||
checksum bump → new page-tree index key), so in-place edits appear without
|
||
needing APCu-specific clearing on that path.
|
||
|
||
---
|
||
|
||
## Findings — do before departure (P1)
|
||
|
||
### P1-1 · Prod PHP upload limits are unverified — could block photo posting entirely
|
||
`php/php-local.ini` (100M upload / 500M post) is **mounted only into the local
|
||
Docker container** (`docker-compose.yml`); nothing in `scripts/` or `deploy/`
|
||
ships PHP limits to the prod Apache server. If prod runs distro defaults
|
||
(`upload_max_filesize=2M` is common), a single modern phone photo (3–8 MB)
|
||
fails to upload — the exact core use case of the trip.
|
||
**Action:** `make remote-diag` (or a one-off phpinfo check) to read prod's
|
||
`upload_max_filesize` / `post_max_size` / `max_file_uploads`. If low, add a
|
||
`.user.ini` (FPM) or `.htaccess` `php_value` (mod_php) via a new make target.
|
||
The real proof is P1-2's live post with photos.
|
||
|
||
> **Resolved 2026-07-09.** Confirmed prod was at the 2M default. Fixed by
|
||
> Mischa via Webmin: PHP execution switched from CGI to **PHP-FPM** (package
|
||
> was already installed) and the upload limits raised in the FPM
|
||
> configuration. Because the setting lives in the server-side FPM config —
|
||
> not in the webroot — it survives fresh Grav installs, so no
|
||
> `deploy/`-versioned `.user.ini` / make target is needed. Side benefit: APCu
|
||
> now persists in shared memory, matching the assumptions in the
|
||
> entry-actions publish endpoint's cache invalidation.
|
||
> Config location for future reference: Webmin → PHP-FPM Configuration.
|
||
> Still owed: P1-2's live phone post is the end-to-end proof.
|
||
|
||
### P1-2 · One real end-to-end post from the actual phone, on prod, over cellular
|
||
The runbook's pre-launch smoke (handover step 7) calls for one `/post` submit
|
||
on prod. After the 2026-07-08 deploy, confirm this happened **from the phone
|
||
you'll travel with, on cellular, with 2+ HEIC photos** — that exercises HEIC
|
||
conversion, FilePond upload, prod PHP limits, cache-on-save under
|
||
`twig.cache:true`, and the feed render in one shot. Then edit that entry
|
||
(reorder + delete a photo), then delete it — the edit/delete paths shipped
|
||
today and deserve one prod rep.
|
||
|
||
### P1-3 · Session expiry mid-compose: test the 30-minute window once
|
||
`system.yaml` has `session.timeout: 1800` (30 min) and `form.yaml` has
|
||
`refresh_nonce: false`. A slow entry written on a train can easily outlive the
|
||
session; rememberme (enabled, 7-day cookie) should transparently re-auth the
|
||
next request, but the form **nonce** and the FilePond **flash uploads** were
|
||
created under the old session. The localStorage draft guarantees the text
|
||
survives whatever happens — but you should see the actual failure mode once
|
||
now, not first in a hostel.
|
||
**Test:** open `/post`, add a photo, wait 35+ minutes, submit.
|
||
**If it's ugly:** consider raising `session.timeout` in the prod env override
|
||
(`deploy/env/prod/system.yaml`, e.g. 4–12 h) — single-owner site, low risk,
|
||
big comfort. (Per-env override, not the committed dev `system.yaml`.)
|
||
|
||
### P1-4 · Make sure you can log back in from the road
|
||
The rememberme cookie lasts **7 days** — on a multi-week trip you *will* be
|
||
re-typing the password, possibly on hotel wifi after a cookie wipe. Login
|
||
throttling is 5 attempts / 10 min (easy to hit with phone typos).
|
||
**Action:** confirm the password is in the phone's password manager and test a
|
||
fresh login on the phone once. Know that after 5 typos you wait 10 minutes —
|
||
don't panic-retry.
|
||
|
||
---
|
||
|
||
## Findings — worth doing before departure (P2)
|
||
|
||
### P2-1 · Duplicate home page: `user/pages/home/` shadows `00.home/`
|
||
Both `user/pages/home/home.md` (old, committed in `a440583`, carries
|
||
`routes: default: /`) and `user/pages/00.home/home.md` (the real one) exist
|
||
with the same slug and near-identical content — which is exactly why a silent
|
||
mix-up would go unnoticed. Which page wins `/home` (the `home.alias` target)
|
||
depends on page-index ordering luck.
|
||
**Action:** delete `user/pages/home/` (verify `/` and `/home` still render the
|
||
context-aware home from `00.home` afterwards, incl. the pre-departure branch).
|
||
|
||
### P2-2 · Docs drift in CLAUDE.md
|
||
- `active_trip: japan-korea-2026` example — that trip doesn't exist; the real
|
||
upcoming trip is **`/trips/denmark-2026`** (local `site.yaml`, uncommitted).
|
||
- The `entry-actions` plugin (three owner-only API routes, shipped with the
|
||
journal-post-form feature) isn't mentioned in CLAUDE.md's plugin list or
|
||
architecture sections, and the "post form uses filepond via cache-on-save"
|
||
description predates the edit-mode photo editor.
|
||
|
||
### P2-3 · No HSTS header on prod
|
||
Apache serves without `Strict-Transport-Security`. One-line header addition;
|
||
the login form and session cookie deserve it (`secure_https: true` is already
|
||
set for the cookie).
|
||
|
||
### P2-4 · Shrink the unused API auth surface
|
||
`api.yaml` enables **api_keys + JWT + session** auth. The site only uses
|
||
session auth (gpx-manager, post-form edit, entry-actions). If no API keys are
|
||
in use (`user/config/plugins/api-private.php` is untracked/local — not
|
||
audited), disabling `api_keys_enabled`/`jwt_enabled` in config removes two
|
||
whole credential classes from the attack surface. Not urgent — the endpoints
|
||
behind them still enforce owner checks.
|
||
|
||
### P2-5 · Confirm the backup path is live
|
||
Every road post only exists on the prod disk until git-sync commits it to
|
||
Gitea. **Action:** `make remote-content-status` — confirm git-sync is enabled
|
||
on prod and the working tree is clean/pushed. (Photos live under `pages/`, so
|
||
they ride along in the content repo — the backup covers them too.)
|
||
|
||
---
|
||
|
||
## Known limitations — accepted, no action (P3)
|
||
|
||
- **Photos are not draft-persisted** (File/Blob can't go to localStorage); the
|
||
restore hint says so explicitly. Re-selecting photos after a failure is the
|
||
designed trade-off.
|
||
- **Location/weather helpers depend on free third-party APIs** (BigDataCloud
|
||
reverse-geocode, Open-Meteo). Both are best-effort with manual fallbacks —
|
||
fine.
|
||
- **No offline mode.** `/post` needs connectivity to load; composing offline
|
||
means the phone's notes app. (Logged as an ideation candidate, not a bug.)
|
||
- **Rate limit 120 req/60s** is generous for a single owner; a 6-photo edit
|
||
batch stays far below it.
|
||
|
||
---
|
||
|
||
## Morning checklist (~10 minutes + one coffee)
|
||
|
||
1. `make remote-diag` → check `upload_max_filesize` / `post_max_size` on prod
|
||
(P1-1). Fix limits first if they're at defaults.
|
||
2. From the phone, on cellular, on prod: log in fresh → post a test entry with
|
||
2 HEIC photos → verify it's in the feed immediately → edit it (reorder +
|
||
remove a photo) → delete it (P1-2, P1-4).
|
||
3. `make remote-content-status` → git-sync clean and pushing (P2-5).
|
||
4. Optional but cheap: start the 35-minute `/post` session-expiry test in a
|
||
background tab while doing the above (P1-3).
|
||
5. Queue the P2 cleanups (duplicate home folder, CLAUDE.md drift, HSTS) for a
|
||
normal dev session — none block departure.
|