Accounts and sign-in, and fix the read toggle

epAction's redraw closure called itself when handed a row, so Mark read
recursed until the stack blew; it now swaps that row in place. Opening an
item also marks it read, redrawn where it stands so nothing vanishes from
under the pointer on the Unread tab.

Step A of multi-user: users and sessions tables, Argon2id, a session
cookie, ipx user subcommands, and a trusted proxy header for Cloudflare
Zero Trust -- honoured only from a trusted_proxies address. The shared
token still works and is the admin. A new database starts with
admin/ipodderx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-10 23:11:20 +00:00
parent ed26fa2061
commit 06f182b555
12 changed files with 876 additions and 53 deletions

View File

@@ -56,19 +56,55 @@ and until now nothing set them.
---
## 2026-09-10 — Marking an item read
Two bugs in one place. `epAction`'s `redraw` closure called *itself* when it had a row to
update -- `if(el) redraw()` where it meant to swap the row -- so the Mark read button in the
text pane recursed until the stack blew. It now swaps that one row in place and refreshes the
text below only when it is the item being read.
And opening an item now marks it read, which is what clicking a thing to read it means. The row
is redrawn where it stands rather than the list reloaded, so an item does not vanish from under
the pointer on the Unread tab.
---
## 2026-09-10 — Step A: accounts and sign-in
`users` and `sessions` tables, Argon2id hashing, a session cookie, and `ipx user add|list|passwd|rm`
(passwords come in on stdin, so they miss the shell history and any `ps` listing).
Three ways in, in order of how specific the claim is:
1. **A proxy header** naming the user -- `Cf-Access-Authenticated-User-Email` for the Cloudflare
Zero Trust in front of `ipodderx.sdf1.net`. Honoured **only** from an address in
`trusted_proxies` (loopback by default): a header is worth exactly as much as the hop that set
it, and the LAN port would otherwise let anyone claim to be anyone. Verified both ways -- a
spoof from an untrusted address is refused.
2. **A session cookie** from signing in at `/login`.
3. **The shared token**, which is the admin, so the healthcheck and existing links keep working.
A database with no accounts creates **admin / ipodderx** and says so loudly in the log. The UI shows
who is signed in above the sidebar footer, with a sign-out, and a 401 sends the page to `/login`.
Nothing is per-user *yet*: everyone still sees the same feeds and read state. That is step B.
---
## Multi-user — the plan
Decided with Ray: **stay on SQLite** (Postgres was considered and dropped -- it is a deployment
choice, not a capability one, and nothing here contends for writes). Sign-in is either a local
username and password or the Authentik that already fronts `ipodderx.sdf1.net` through a Cloudflare
tunnel. Feeds, items and files are **shared**; read state and subscriptions are **per user**.
username and password or the Cloudflare Zero Trust that already fronts `ipodderx.sdf1.net`, which
puts the authenticated identity in `Cf-Access-Authenticated-User-Email`. Feeds, items and files are **shared**; read state and subscriptions are **per user**.
The point of sharing: two people subscribed to the same show cost one fetch, one parse, and one file
on disk. `enclosures.url` is already globally UNIQUE, so the file half is nearly free.
- [ ] **A. Users, sessions, sign-in.** `users` + `sessions` tables, Argon2 hashing, session cookie,
`ipx user add|list|passwd|rm`. Authentik/proxy header (`trusted_header` in `[web]`) signs in and
optionally creates a user. The existing shared token keeps working and resolves to the admin, so
- [x] **A. Users, sessions, sign-in.** `users` + `sessions` tables, Argon2 hashing, session cookie,
`ipx user add|list|passwd|rm`. A proxy header (`trusted_header` in `[web]`) signs in and
optionally creates a user -- honoured only from a `trusted_proxies` address, so a LAN client
cannot simply assert it. The existing shared token keeps working and resolves to the admin, so
the healthcheck and any scripts survive. Login page for direct access.
- [ ] **B. Per-user read state.** `entry_state(user_id, feed_id, guid, read, flagged, position)`;
the current columns on `entries` migrate into the first user's rows. Unread counts, filters and