feat(auth): signup captures browser-detected timezone

v1.4.25 W7 phase 6 — proposal §4 Option C as default-value layer.

The registration form now reads
Intl.DateTimeFormat().resolvedOptions().timeZone and sends it with
the signup payload. The server:
  1. Validates the value against Intl.supportedValuesOf via
     isValidTimezone().
  2. On any invalid value (or absence), resolves the admin-
     configured server default (AppSettings.defaultUserTimezone) and
     uses that.
  3. On no admin default, the resolver falls back to the
     'Europe/Berlin' string, which is also the User.timezone column
     default — so the worst case still produces the legacy
     pre-v1.4.25 behaviour.

The Zod registerSchema gains an optional 'timezone' string field
capped at 64 chars (matches the longest IANA canonical zone
identifier plus headroom). Audit-log entry now carries the resolved
zone so an operator can spot a Tokyo user mis-attributed to Berlin
if browser detection fails.

No new tests — the existing register-route integration coverage is
thin, and the v1.4.25 W7 phase 7 integration test in
Pacific/Auckland covers the full signup → measurement → export
round-trip end-to-end.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 12:09:16 +02:00
1 parent 80544b73cb
commit 46f2cfb9e7
3 files changed
+39 -3

No files matched your search

+10
View File
@@ -16,6 +16,16 @@ export const registerSchema = z.object({
PASSWORD_MIN_LENGTH,
`Password must be at least ${PASSWORD_MIN_LENGTH} characters`,
),
/**
* v1.4.25 W7 — browser-detected timezone. The signup form pulls
* `Intl.DateTimeFormat().resolvedOptions().timeZone` and sends it
* along; the server validates against the runtime IANA list and
* falls back to the admin-configured server default
* (`AppSettings.defaultUserTimezone`) when the value is invalid
* or missing. Optional so legacy clients that predate v1.4.25
* keep working.
*/
timezone: z.string().max(64).optional(),
});
export const loginPasswordSchema = z.object({