From ea5532c5c3c8527bfd2e281eb74a26cdcaf2536f Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 7 May 2026 08:01:23 -0400 Subject: [PATCH] feat(07.1-03): add lib/services/user-timezone.ts helper - getUserTimezone(session) returns validated IANA tz string with safe fallback - DEFAULT_TIMEZONE_FALLBACK reads process.env.DEFAULT_TIMEZONE || 'UTC' - Validates against Intl.supportedValuesOf('timeZone'); 64-char length cap - Pure / synchronous / no DB / no @/lib/auth-utils import (avoids circular) --- lib/services/user-timezone.ts | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lib/services/user-timezone.ts diff --git a/lib/services/user-timezone.ts b/lib/services/user-timezone.ts new file mode 100644 index 0000000..127f6e3 --- /dev/null +++ b/lib/services/user-timezone.ts @@ -0,0 +1,40 @@ +// Server-side helper for resolving the calling user's IANA timezone. +// +// After Phase 7.1 Plan 01, `session.user.timezone` is a string populated +// either from the stored `"user".timezone` column or from Better Auth's +// additionalField `defaultValue` (`process.env.DEFAULT_TIMEZONE || 'UTC'`). +// +// This helper: +// - reads the value off a Better Auth session +// - validates it against `Intl.supportedValuesOf('timeZone')` (defence in +// depth — Plan 02 already validates writes, but a corrupt row from +// before this phase, or a manual SQL edit, must not crash dashboards) +// - falls back to `process.env.DEFAULT_TIMEZONE || 'UTC'` if invalid +// +// Use this in every API route that does day/week/month boundary math. + +export const DEFAULT_TIMEZONE_FALLBACK = (): string => + process.env.DEFAULT_TIMEZONE || 'UTC'; + +type SessionLike = { + user?: { timezone?: unknown } | null; +} | null | undefined; + +function isValidIanaTimezone(tz: unknown): tz is string { + if (typeof tz !== 'string' || tz.length === 0 || tz.length > 64) return false; + try { + return Intl.supportedValuesOf('timeZone').includes(tz); + } catch { + return false; + } +} + +/** + * Returns a validated IANA timezone string for the given session. + * Never throws; always returns a usable string (worst case: 'UTC'). + */ +export function getUserTimezone(session: SessionLike): string { + const raw = session?.user?.timezone; + if (isValidIanaTimezone(raw)) return raw; + return DEFAULT_TIMEZONE_FALLBACK(); +}