- 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)
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
// 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();
|
|
}
|