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(); +}