Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server half of the active-sessions surface. Web UI follows.
The operator wants this specifically to notice a compromised account, which
sets the bar: the addresses have to be trustworthy, or the feature is worse
than absent because it looks like evidence.
Migration 0052 adds created_ip + last_ip. Two columns, not one, and the pair
is the signal: a session issued at home and now being used from elsewhere is
the shape of a stolen token, and neither column alone can show that. Typed
text, matching the user_agent column beside it — these are displayed, never
queried by subnet, and inet round-trips through pgx as a netip.Prefix that
renders "1.2.3.4/32".
The rest of the schema was already waiting. Migration 0004 anticipated this
exactly: "last_seen_at enables an 'active sessions' UI later (not wired in
this plan) without schema churn." last_seen_at is live data — the auth
middleware already touches it per request — so last_ip rides that same
UPDATE for free.
Getting the address right is the substance here. Nothing extracted a client
IP anywhere before, and both obvious approaches are wrong:
- RemoteAddr alone shows the reverse proxy on every session, which is the
normal self-hosted deployment. Noise shaped like data.
- Trusting X-Forwarded-For lets any client choose what its victim sees. A
security surface an attacker can write to is worse than none.
So auth.ClientIP trusts the header only when the request actually arrived
from a proxy range. Public RemoteAddr means a direct connection, so XFF is
attacker-controlled and ignored outright. Private RemoteAddr means we walk
XFF right-to-left — proxies append, so the right end is what our own
infrastructure wrote — and take the first non-proxy address. A forged XFF
only prepends to the left end, which that walk never reaches. Unit-tested,
including both spoofing shapes.
Fails closed on a public-addressed proxy (separate host, CDN): we report the
proxy rather than trusting a forgeable header. Documented at the function.
Endpoints, all scoped by user_id per rule #47:
GET /api/me/sessions → list, flagging the current row
DELETE /api/me/sessions/{id} → 204, or 404 if not yours
POST /api/me/sessions/logout-others → {"revoked": n}
Keyed on session id alone, any household member could revoke another's
session by guessing a uuid, so the delete carries user_id in its WHERE and
:execrows distinguishes "not yours" (404) from a false 204. There's a test
that asserts the row actually survives, not merely that we returned 404.
The middleware now also puts the session id in context. logout-others is
defined by exclusion, and without knowing which session is ours the
safe-looking action deletes everything including the caller's — so it
refuses rather than guesses when the id is absent, and that refusal is
tested for non-deletion too.
audit_log.action is plain text with no CHECK, so the two new actions need no
migration (rule #36 checked, not assumed).
Codegen is real sqlc 1.31.1 via the container in `make generate` — docker is
present on this workstation even though Go and sqlc aren't — rather than the
hand-written .sql.go shortcut used in milestone #268.