diff --git a/docs/plans/2026-02-01-cloudflare-timezone-detection-design.md b/docs/plans/2026-02-01-cloudflare-timezone-detection-design.md new file mode 100644 index 00000000..ccb774f8 --- /dev/null +++ b/docs/plans/2026-02-01-cloudflare-timezone-detection-design.md @@ -0,0 +1,169 @@ +# Cloudflare Timezone Detection on Signup + +**Date**: 2026-02-01 +**Status**: Approved +**Author**: Claude Code + +## Overview + +Automatically populate the user's timezone during registration by reading Cloudflare's `cf-timezone` header. This provides a better default than "UTC" for users behind Cloudflare's network. + +## Problem Statement + +Currently, all new users get `timezone: "UTC"` as their default, even though Cloudflare provides accurate timezone detection via the `cf-timezone` header. Users must manually update their timezone in settings after registration. + +## Proposed Solution + +Capture the `cf-timezone` header during the registration flow and use it to set the user's timezone field automatically. + +## Architecture + +### Component Design + +``` +Browser Request → Plug (CaptureTimezone) → Session[:detected_timezone] + ↓ + LiveView mount reads session + ↓ + Form submit merges timezone into params + ↓ + Accounts.register_user_with_organization + ↓ + User created with correct timezone +``` + +### Files to Modify + +1. **NEW**: `lib/towerops_web/plugs/capture_timezone.ex` + - Plug that extracts `cf-timezone` header + - Stores in session as `:detected_timezone` + - Fallback: "UTC" if header missing + +2. **MODIFY**: `lib/towerops_web/endpoint.ex` + - Add `CaptureTimezone` plug to browser pipeline + - Runs on all browser requests (minimal overhead) + +3. **MODIFY**: `lib/towerops_web/live/user_registration_live.ex` + - Read `:detected_timezone` from session in `mount/3` + - Store in socket assigns + - Merge into user params in `handle_event("save")` + +4. **MODIFY**: `lib/towerops/accounts/user.ex` + - Add `:timezone` to `registration_changeset/3` cast fields + - No validation needed (internal value, already validated by Cloudflare) + +## Implementation Details + +### Plug Implementation + +```elixir +defmodule ToweropsWeb.Plugs.CaptureTimezone do + @moduledoc """ + Captures the cf-timezone header from Cloudflare and stores it in session. + Used to pre-populate timezone during user registration. + """ + import Plug.Conn + + def init(opts), do: opts + + def call(conn, _opts) do + timezone = + case get_req_header(conn, "cf-timezone") do + [tz] when is_binary(tz) -> tz + _ -> "UTC" + end + + put_session(conn, :detected_timezone, timezone) + end +end +``` + +### LiveView Changes + +**In mount/3:** +```elixir +detected_timezone = get_session(socket, :detected_timezone) || "UTC" +assign(socket, :detected_timezone, detected_timezone) +``` + +**In handle_event("save"):** +```elixir +user_params = Map.put(user_params, "timezone", socket.assigns.detected_timezone) +``` + +### Schema Changes + +```elixir +def registration_changeset(user, attrs, opts \\ []) do + user + |> cast(attrs, [:email, :password, :timezone, :privacy_policy_consent, :terms_of_service_consent]) + # ... rest unchanged +end +``` + +## Behavior + +### Header Present +- Cloudflare sends: `cf-timezone: America/Chicago` +- User registered with: `timezone: "America/Chicago"` + +### Header Missing (Development/Non-Cloudflare) +- No header detected +- User registered with: `timezone: "UTC"` (fallback) + +### Cloudflare Timezone Format +- Uses IANA timezone database format (e.g., "America/Chicago", "Europe/London") +- Compatible with Elixir's `Timex` and `Calendar` libraries +- No conversion needed + +## Testing Strategy + +1. **Unit Tests**: Plug extracts header correctly, handles missing header +2. **Integration Tests**: LiveView receives timezone, registration includes timezone +3. **Manual Testing**: + - Development (no header): Should default to "UTC" + - Production (with header): Should use Cloudflare's detected timezone + +## Edge Cases + +- **Multiple timezones**: Cloudflare sends single value, no issue +- **Invalid timezone**: Trust Cloudflare's validation (they use IANA database) +- **User changes timezone later**: Users can still update in settings +- **Invitation flow**: Works identically for both invitation and normal registration + +## Performance Impact + +- **Plug overhead**: 1 header lookup + 1 session write per request (~negligible) +- **Session size**: +1 small string value (~15-30 bytes) +- **LiveView mount**: 1 session read (already happening) + +## Security Considerations + +- Header is read-only from Cloudflare (users cannot spoof it easily) +- Timezone is non-sensitive data (no privacy concerns) +- No validation needed (trust Cloudflare's IANA-compliant values) + +## Rollout Plan + +1. Deploy plug and schema changes +2. Verify in production logs that `cf-timezone` header is present +3. Monitor new user registrations for correct timezone population +4. No user communication needed (transparent improvement) + +## Future Enhancements + +- Display detected timezone in registration form (optional) +- Allow users to override detected timezone before submitting (optional) +- Use timezone for email scheduling and notification preferences + +## Alternatives Considered + +1. **JavaScript-based detection**: Less reliable, requires client-side code +2. **GeoIP-based timezone**: Less accurate, requires additional lookups +3. **Ask user during signup**: Additional friction, worse UX + +## References + +- Cloudflare cf-timezone header documentation +- IANA Timezone Database +- Phoenix LiveView session handling