4 Commits

Author SHA1 Message Date
tony 9f489d1fb6 fix(security): tighten pair-code defences per follow-up review
- Per-code failure threshold dropped 5 → 3. After 3 wrong guesses
  during a code's 5-min TTL, the code is burned and the owner must
  re-mint. Still tolerates the occasional typo while collapsing the
  brute-force window further.
- Cap concurrent active codes to 1: minting a new code clears any
  prior code. Single-user app, no need to juggle multiple.
- Add a global brute-force lockout: after 100 failed attempts in
  any 5-minute window, the redeem endpoint returns 429 endpoint_locked
  for the next 10 minutes, regardless of source IP. Logged at error
  level so the operator notices.
2026-06-01 10:21:03 +01:00
tony 36118b4a4f fix(security): harden owner pair-code against brute-force
- Set express trust proxy=1 so req.ip is the real client (one Dokku
  nginx hop). Drop the X-Forwarded-For fallback — it was client-spoofable.
- Add a global redeem rate limit (30/min across all IPs) so rotating
  proxies can't outpace the per-IP limiter.
- Every failed redeem now decrements life from all active pair codes
  via a shared failures counter. After 5 wrong guesses during a code's
  5-minute TTL, the owner's real code gets burned and they must re-mint.
  This makes the 20-bit entropy of a 6-digit code defensible: an
  attacker doesn't know which code is real and can't probe long enough
  to find it before triggering the burn.
2026-06-01 10:16:30 +01:00
tony 90a4459e63 feat: 6-digit pair code login + responsive layout for narrow car browsers
Pair code:
- POST /api/auth/owner/pair-code (owner-gated) mints a single-use,
  5-minute, 6-digit code held in memory.
- POST /api/auth/owner/pair-redeem swaps the code for an owner cookie,
  with basic IP-based rate limiting (max 8 attempts/min).
- OwnerLoginModal gains a tab: "Use 6-digit code" (default) vs the
  long secret. The PIN input is big, monospaced, numeric-only.
- Small smartphone icon next to the connected Tesla chip opens
  OwnerPairCodeDisplay, which shows the code in 56px type with a
  live countdown so it's tappable on the touchscreen of the other
  device while you stare at it.

Responsive layout:
- TopBar wraps when narrower than the chip row. minWidth on OD strip
  drops 380→280 and chat composer drops 360→240 so they stack cleanly.
- Main body switches to flex-col below the lg breakpoint (Tailwind
  1024px), so the map sits on top of the stops rail on Tesla 3/Y
  portrait and any window not maximised. Map gets 40vh min-height
  so it never disappears.
2026-06-01 10:10:30 +01:00
tony f793b526aa fix(security): owner auth gate, OAuth state cookie binding, 0600 token perms
- Add OWNER_SECRET-based session: signed HMAC cookie, /api/auth/owner login,
  requireOwner middleware. All Tesla routes refuse 401 without it.
- Bind OAuth state to a SameSite=Lax httpOnly cookie at /start, validate
  match in /callback with constant-time compare. Refuses unmatched callbacks.
- Token store now mkdir 0700, writeFile + rename atomic, mode 0600 with
  defensive chmod. Owner-only on disk.
- VIN masked to last 4 in responses; partner-register no longer echoes raw
  Tesla body to clients; coord bounds checked on send-to-nav.
- Client: useTesla also tracks owner status; Connect Tesla button opens an
  OwnerLoginModal when not authenticated, then continues to Tesla OAuth.

Conscious deferrals:
- Explicit CSRF tokens on POST routes: mitigated by SameSite=Lax cookies
  + same-origin CORS. Will revisit if cross-origin clients land.
- At-rest token encryption: deferred for single-user app; tokens are on a
  0700 Dokku volume readable only by the app uid. Will add AES-GCM if we
  multi-tenant.
2026-05-31 22:32:22 +01:00