Files
HealthLog/src/app
Marc-André Bombeck cc8c69aa07 Stop redirecting to the address the process bound to
Reported from the iOS side, iOS #96. `GET /api/auth/native/login` answers

    location: https://0.0.0.0:3000/auth/login?flow=native

behind a proxy that does not pass `Host` through. `NextResponse.redirect`
needs an absolute URL, the route built one with `new URL(path, req.url)`,
and `req.url` there is the address the Node process bound to.

That is the primary onboarding path. ASWebAuthenticationSession follows
the redirect, iOS refuses the restricted port, and the person lands on a
browser error page with no way forward. Distributed builds carry no
associated domains on purpose, so the browser handoff is also how
passkeys are meant to work; without it a new account is down to a
password and a confirmation code.

Two things hid it. Every error branch of that route builds the fixed
healthlog:// scheme with no host at all, so the route behaved correctly
on every failure and broke only when it was supposed to succeed. And its
own test built a request whose host matched, so the absolute URL it
produced looked right. The assertion there used toContain, which is
satisfied by the broken value just as happily as the correct one.

The same line was in six more places: the error redirects of the
Withings, WHOOP and Fitbit connect routes, which send a browser to
/settings/integrations. Behind the same proxy those land on the same
unreachable address, so all seven move to a relative Location.

A relative Location is valid per RFC 7231 and the client resolves it
against the address it actually called, which is the public one by
definition. The alternative is reading X-Forwarded-Host, and that would
put one more proxy setting between a self-hoster and a working sign-in.

Three checks, as the report asked for. The route test now drives a
request whose URL names the bind address, which is what a proxy hop
looks like from inside the handler. A structural test freezes the
pattern out of the four files and, separately, requires each of them to
still reach for the helper, because a file that simply deleted its
redirect would pass the first check and fail its users. The helper
refuses a protocol-relative value, which is a host in disguise.

Verified by breaking it: putting the old call back fails four checks,
the last of them reporting the reported value verbatim.

Refs iOS #96.
2026-08-22 10:55:40 +02:00
..
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00

src/app/

The Next.js App Router tree: the page surfaces a logged-in user sees and the REST API under api/. Server components are the default; "use client" is added only where a hook, state, or browser API needs it. Routing is file-based — a page.tsx is a page, a route.ts is an API endpoint.

API routes (api/)

Every endpoint wraps its handler in apiHandler (@/lib/api-handler), parses any body with Zod safeParse, narrows userId from the session or Bearer token (never from the body), and returns the { data, error, meta? } envelope. requireAdmin() is cookie-only, so api/admin/ is unreachable by Bearer token. Notable groups:

  • auth/ — login, register, password, passkey (WebAuthn), refresh, Codex device-OAuth.
  • measurements/, mood-entries/, medications/, sleep/ — the core health-data CRUD + batch/bulk ingest.
  • ingest/ — external Bearer-token ingest surfaces.
  • insights/, analytics/, dashboard/ — AI insights, the Coach SSE stream, trend summaries, the first-paint dashboard snapshot.
  • withings/, whoop/, fitbit/, integrations/, sync/ — OAuth connect/sync/webhook surfaces and the offline delta feed.
  • fhir/, share-links/, doctor-report/, export/, import/ — clinician-facing FHIR R4 read API, shareable records, report + data export.
  • notifications/, devices/, send/, telegram/ — channel registration, device tokens, dispatch, webhook intake.
  • admin/ — operator panel APIs (cookie-only).
  • health/, version/, meta/, internal/ — health check, build-version probe, runtime metadata, worker-internal routes.

Page surfaces

measurements/, medications/, mood/, insights/, achievements/, notifications/, settings/, onboarding/, admin/, plus the public about/, privacy/, bugreport/, the share-record route c/, and the root dashboard (page.tsx). Shared shell + chrome lives in layout.tsx; error.tsx / global-error.tsx / not-found.tsx / loading.tsx cover the framework boundaries. layout.tsx holds the one nonce-bound dangerouslySetInnerHTML (theme bootstrap).

Conventions

See ../../CLAUDE.md. The request-flow walk in .planning/codebase/arch.md annotates each layer with file:line citations. Related dirs: handlers compose ../lib/; pages render ../components/.