Add design for Cloudflare timezone detection on signup
Documents approach to automatically populate user timezone from cf-timezone header during registration, with UTC fallback when header is not present.
This commit is contained in:
parent
ad753cf0c6
commit
45f8bc7459
1 changed files with 169 additions and 0 deletions
169
docs/plans/2026-02-01-cloudflare-timezone-detection-design.md
Normal file
169
docs/plans/2026-02-01-cloudflare-timezone-detection-design.md
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Reference in a new issue