Follow-up to PR #406 — addresses the two items deferred from the audit:
#7 — Centralize auth helpers
- New `requireSessionUser(request)` in lib/auth/session.server.ts that
returns the user or throws a redirect to /auth/login.
- New `requireSessionUserJson(request)` companion that throws a 401 JSON
response (for fetcher/JSON endpoints).
- Replace the repeated
const user = await getSessionUser(request);
if (!user) return redirect("/auth/login");
pattern across 18 route loaders/actions. Removes the duplicated guard
preamble and gives a single chokepoint to evolve later (e.g., for
terms-version gating).
#8 — Extract heavy loaders into .server.ts siblings
- routes/home.tsx → home.server.ts (DB count query + listActivities +
listRecentPublicActivities)
- routes/users.$username.tsx → users.$username.server.ts (user lookup +
follow state + counts + listPublicRoutes/Activities + persona check)
- routes/settings.connections.tsx → settings.connections.server.ts
(connected_services join + manifest merge)
Each route file shrinks to a thin delegator: `loader` calls
`loadXxx(request)`. The component module no longer transitively pulls
`getDb` and Drizzle schema into its import graph — Vite's tree-shake
already strips server-only code from the client bundle, but the
explicit `.server.ts` suffix makes that contract local and auditable.
Other 17 routes that mix loader/action with components are left as-is
for now: they're each small enough that the split adds churn without
buying much clarity. The pattern is documented by the three examples;
the rest can convert opportunistically when they grow.
Tests:
- lib/auth/session.server.test.ts (4 cases — redirect for missing
cookie, redirect for ghost userId, success path, JSON 401 variant)
Full repo: pnpm typecheck, pnpm lint, pnpm test all green
(181 passed | 31 integration-gated skipped).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cleanups in one pass:
1. Update import paths app-wide from `~/lib/auth.server` to
`~/lib/auth/session.server` for the four session helpers
(sessionStorage, createSession, getSessionUser, destroySession).
~40 files: 33 simple path swaps where the file imported only session
symbols, 5 splits where it also imported per-method auth functions
(auth.verify.tsx, api.settings.email.ts, activities.\$id.tsx,
routes.\$id.tsx, auth.accept-terms.tsx) — those keep one import
from auth.server (for verifyMagicToken, canView, recordTermsAcceptance,
etc.) and gain a second import from auth/session.server.
Two more files used relative paths and were missed by the first
grep pass (lib/oauth.server.ts and routes/oauth.authorize.tsx) —
migrated too.
The @deprecated re-exports block in auth.server.ts is gone.
2. Rename the new auth files to follow the project's `.server.ts`
convention so Vite/React Router treat them as server-only (they
read process.env.SESSION_SECRET, hit the DB, etc. — must NOT enter
the client bundle):
- auth/session.ts → auth/session.server.ts
- auth/completion.ts → auth/completion.server.ts
- auth/completion.test.ts → auth/completion.server.test.ts
Done with `git mv` so blame is preserved.
Verified: typecheck + lint green; 126 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Outcome of today's `/spec-drift-review`. Concentrated on the
high-and-medium-severity items; low-severity wording left for the
next per-feature change to pick up naturally.
High-severity URL fixes (specs were actively misleading):
- account-management — email-change verification URL was specced as
`/auth/verify-email-change?token=...`; the actual route is
`/auth/verify?email-change=1&token=...` (see auth.verify.tsx:8).
A reader implementing against the old wording would build a link
that 404s.
- observability — "Both apps SHALL expose a /metrics endpoint" was
half right: the planner is at /metrics, but the journal exposes
/api/metrics. The infrastructure spec already had the right URLs;
the observability spec disagreed with itself. Now both correct,
with a one-line note explaining the per-app split.
Medium-severity wording drift (Stream E aftermath):
- profile-settings, account-management, connected-services — all
three said "the settings page SHALL include a [...] section",
which described the old single-scrollable settings page. Stream E
(PR #323) split /settings into four sub-pages
(/settings/{profile,account,security,connections}); rewording each
spec to point at its specific sub-page. The API endpoints
(/api/settings/*) and behavior are unchanged.
- authentication-methods — passkey add/delete previously said "via
the settings page"; now specifically /settings/security.
Code drift fixed inline:
- apps/journal/app/routes/auth.verify.tsx — after a successful
email change, the redirect was going to `/settings#account` (an
anchor on the OLD single-scrollable settings page). Stream E
retired that page; the right destination now is
`/settings/account`. Without this the user would land on
/settings/profile (which is what /settings redirects to) instead
of the page that just changed.
sse-broker spec wording:
- The "no buffering tweaks in the Caddy reverse_proxy block"
scenario asserted the entry was "a plain `reverse_proxy
journal:3000`". After PR #329 the journal block has
`lb_try_duration 30s` / `lb_try_interval 250ms` — neither affects
streaming, so SSE still works, but the spec's "plain" language
was no longer literally true. Reworded to forbid only
buffering-related directives; explicitly call out that
retry-on-restart directives like lb_try_duration are fine.
Navbar consolidation (journal-landing):
- The shipped navbar's full shape was scattered: notifications said
"navbar has a bell," explore said "navbar has an Explore entry,"
but no spec described the avatar dropdown, the primary-nav
cluster (Feed/Routes/Activities), or the mobile drawer (Stream
C / PR #324). Added a "Top navbar shape" requirement to
journal-landing covering all of it — anonymous vs signed-in,
desktop vs mobile, dropdown contents, drawer behavior. The
per-feature specs (notifications, explore) still own their own
badges/entries; this requirement just says what the whole
cluster looks like.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Journal server (Phase 1.4 + 1.5):
- Add oauth_clients, oauth_codes, oauth_tokens tables to journal schema
- Implement GET /oauth/authorize with PKCE flow and login redirect
- Implement POST /oauth/token (authorization_code + refresh_token grants)
- Add validateBearerToken() + getAuthenticatedUser() middleware
- Seed trails-cool-mobile as trusted OAuth client on server startup
- Add GET /.well-known/trails-cool discovery endpoint
- Add returnTo support to login page and magic link verify
- Add @trails-cool/api workspace dependency to journal
Mobile app (Phase 1.5 + 1.6):
- Login screen with server URL input and discovery validation
- OAuth2 PKCE login via expo-web-browser with expo-crypto for Hermes
- Token storage in expo-secure-store with auto-refresh on 401
- API client with bearer token injection and typed errors
- Server URL persistence with localhost default in dev mode
- API version compatibility check on app foreground
- Log out + switch server on Profile tab
- iOS ATS exception for local networking
Tests:
- PKCE crypto verification, OAuthError, token generation
- Discovery endpoint response shape
- API version semver compatibility
- API client error types
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /settings with profile, security, and account sections
- Profile: edit display name and bio
- Security: list passkeys with device labels, add/delete with
last-passkey warning
- Account: email change with magic link verification, account
deletion with username confirmation
- Settings link in nav for authenticated users
- E2E tests: auth guard, profile editing, passkey add/delete,
last-passkey warning, account deletion, nav visibility
- i18n: full en + de translations for all settings UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>