Commit Graph

10 Commits

Author SHA1 Message Date
null 2a07e762a2 fix: four review-confirmed regressions in auth + OIDC flows
All found by the multi-angle /code-review over the branch and verified
against the installed libraries:

- 401 discriminator: the session-expiry redirect keyed off a path prefix
  (!/auth/*) and falsely logged users out on /profile/change-password
  with a wrong current password. requireAuth's no-session 401 now carries
  the distinct AUTH_REQUIRED code — the only signal api.ts dispatches
  auth:expired on; wrong-credential 401s keep AUTH_ERROR on any path.
  Test updated to pin the code-based contract on both directions.
- OIDC RFC 9207: exchangeAndVerifyTokens rebuilt the callback URL with
  only code+state, dropping the iss parameter openid-client v6 validates
  when the provider advertises support (authentik does) — every OIDC
  login would fail post-upgrade. The full callback query is now forwarded.
- OIDC http issuers: v6 enforces HTTPS on discovery/endpoints (v5
  didn't) — breaking internal http:// providers on Docker networks.
  OIDC_ALLOW_INSECURE_HTTP=true opts back in (documented in .env.example).
- 2FA downgrade: authService's webauthn fallback caught ANY challenge
  error and silently downgraded to password-only login; now only the
  stale-flag 'No registered WebAuthn credentials' case falls through,
  everything else rethrows.
- Burned-challenge retry: the server consumes the single-use 2FA
  challenge before verifying (anti-replay), so after a server rejection
  a retry can only ever return 'Challenge expired'. LoginPage now resets
  to sign-in on server failure (WebAuthn AND the pre-existing TOTP trap);
  a browser-prompt cancel keeps retry (token still live). Also lazy-loads
  @simplewebauthn/browser out of the entry bundle, matching ProfilePage.

Server 252/252, client 52/52, OIDC smoke 44/44, probe 33/33, e2e 27.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:57:07 -05:00
null 562da73a76 Polish product artwork and branding 2026-07-10 19:10:42 -05:00
null 522eaaf101 chore(deps)!: tailwind 3 -> 4 via @tailwindcss/vite (4i)
Official codemod (run in a clean worktree; docker-owned data/ blocks it
in-tree): tailwind.config.js inlined into client/index.css as CSS-first
@theme/@utility (config file deleted — v4 auto-detects content, which
also retires the .tsx-content-glob failure class from the TS migration),
33 template files' class renames applied, tailwindcss-animate loads via
the v4 @plugin bridge. Build runs through @tailwindcss/vite; postcss
config is now plugin-free. tailwind-merge -> 3 (v4 class model).

Visual-regression gate: only diffs were a ~2px global preflight shift +
the version string (baseline predated 0.41.1); re-baselined both login
screenshots in this commit after review. webauthn.probe confined to the
probe project (it enables WebAuthn for the seeded user, racing the
parallel UI projects' password logins).

e2e 27 passed, probe 18/18, PROD SMOKE PASS, typecheck/lint/tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:03:15 -05:00
null 88a32a983f feat(auth): passkey / security-key UI — WebAuthn ships end to end
The backend existed since 99abca9 with no way to reach it (QA-B1-01).
Now wired:

- LoginPage: requires_webauthn second step mirrors TOTP — the key prompt
  fires automatically after the password; cancel/retry + back-to-sign-in
- ProfilePage: PasskeySection (mirrors TotpSection) — enroll with a name,
  list keys, password-gated per-key remove + remove-all
- webauthnService: WEBAUTHN_ORIGIN stays authoritative; otherwise accept
  the browser's origin when its hostname equals the RP ID — fixes real
  dev/e2e enrollment where the Vite UI port differs from the API port
  (the browser already enforces RP ID ⊆ origin, so no widened trust)
- Deploy safety: WEBAUTHN_RP_ID documented in .env.example + a boot-time
  prod warning in utils/env.cts (localhost-bound keys silently fail
  behind a real domain)
- e2e/webauthn.probe.spec.js: CDP virtual authenticator drives the full
  lifecycle (enroll -> key sign-in -> password-gated removal), retiring
  Cycle 1's 'needs a human with a key' assumption. Probe suite 18/18.

Server 252/252, client 52/52, typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:30:14 -05:00
null f587d09d4a style: apply Prettier across client + server (no behavior change)
One-time repo-wide `prettier --write` (isolated commit so future diffs
stay reviewable). Mechanical formatting only.

Verified unchanged: typecheck + typecheck:server + check:server clean;
server tests 232/232; client tests 50/50; Vite build succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:23:53 -05:00
null f71b433323 feat(ui): refine OnboardingWizard, LoginPage, and SettingsPage 2026-07-06 12:22:47 -05:00
null 0eb024de74 refactor(brand): adopt new primitives in pages
The user-facing pages pick up the new primitives: LoginPage (21
lines) gets the new form tone + the Bill Tracker wordmark on the
auth card; TrackerPage (109 lines) gets the new cashflow timeline
geometry, the new BrandGlyph on the bucket empty state, and the
new TonedCard on the summary stack; the rest are 1-9 line
import/header adjustments. App.tsx is the lazy-loaded import for
the new brand module on the about/privacy paths.

This is commit 6 of 9 in the v0.42.0 brand refresh. Depends on
b714715 (tracker cards).
2026-07-05 17:18:56 -05:00
null 7e5e012434 feat(settings): default landing page (plan Tier 6)
- New per-user 'default_landing_page' setting (tracker default / calendar /
  summary / spending), registered in the whitelist + defaults.
- Implemented as a post-login redirect (keeps /=Tracker so the Home link and
  direct nav are unaffected). Precedence: admin → deep-link they were bounced
  from (state.from) → default landing → /. Resolved by fetching settings in the
  login success path, with a safe / fallback so a fetch failure never blocks login.
- Settings: Select in the Regional section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 16:03:01 -05:00
null 1df4b1bf87 refactor(ts): move User to @/types + type auth endpoints; add code to 500 (Track C/D)
Track C (auth): User lived in @/hooks/useAuth; moved it to @/types
(re-exported from useAuth for the 7 importers) and typed me/login/
totpChallenge/hasUsers, dropping those Admin/Login casts. Left the
single-consumer non-money casts (adminUsers, version/about/privacy/
health docs) and the optimistic-UI rewrite as deliberate low-value/
high-churn stops.

Track D: global 500 handler now emits code:'INTERNAL_ERROR' so an
unexpected error carries the same {error,code} field as structured 4xx.

typecheck/lint(0 err)/build/server-193 all green; probe 17/17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:26:11 -05:00
null 14f1fedfd0 refactor(ts): LoginPage → TypeScript (auth-mode/TOTP/change-pw/privacy flows) 2026-07-04 22:23:31 -05:00
Renamed from client/pages/LoginPage.jsx (Browse further)