package middleware import ( "time" "github.com/gin-gonic/gin" ) const ( // TimezoneKey is the key used to store the user's timezone in the context TimezoneKey = "user_timezone" // UserNowKey is the key used to store the timezone-aware "now" time in the context UserNowKey = "user_now" // TimezoneHeader is the HTTP header name for the user's timezone TimezoneHeader = "X-Timezone" ) // TimezoneMiddleware extracts the user's timezone from the request header // and stores a timezone-aware "now" time in the context. // // The timezone should be an IANA timezone name (e.g., "America/Los_Angeles", "Europe/London") // or a UTC offset (e.g., "-08:00", "+05:30"). // // If no timezone is provided or it's invalid, UTC is used as the default. func TimezoneMiddleware() gin.HandlerFunc { return func(c *gin.Context) { tzName := c.GetHeader(TimezoneHeader) loc := parseTimezone(tzName) // Store the location and the current time in that timezone c.Set(TimezoneKey, loc) // Calculate "now" in the user's timezone, then get start of day // For date comparisons, we want to compare against the START of the user's current day userNow := time.Now().In(loc) startOfDay := time.Date(userNow.Year(), userNow.Month(), userNow.Day(), 0, 0, 0, 0, loc) c.Set(UserNowKey, startOfDay) c.Next() } } // parseTimezone parses a timezone string and returns a *time.Location. // Supports IANA timezone names (e.g., "America/Los_Angeles") and // UTC offsets (e.g., "-08:00", "+05:30"). // Returns UTC if the timezone is invalid or empty. func parseTimezone(tz string) *time.Location { if tz == "" { return time.UTC } // Try parsing as IANA timezone name first loc, err := time.LoadLocation(tz) if err == nil { return loc } // Try parsing as UTC offset (e.g., "-08:00", "+05:30") // We parse a reference time with the given offset to extract the offset value t, err := time.Parse("-07:00", tz) if err == nil { // time.Parse returns a time, we need to extract the offset // The parsed time will have the offset embedded _, offset := t.Zone() return time.FixedZone(tz, offset) } // Also try without colon (e.g., "-0800") t, err = time.Parse("-0700", tz) if err == nil { _, offset := t.Zone() return time.FixedZone(tz, offset) } // Default to UTC return time.UTC } // GetUserTimezone retrieves the user's timezone from the Gin context. // Returns UTC if not set. func GetUserTimezone(c *gin.Context) *time.Location { loc, exists := c.Get(TimezoneKey) if !exists { return time.UTC } return loc.(*time.Location) } // GetUserNow retrieves the timezone-aware "now" time from the Gin context. // This represents the start of the current day in the user's timezone. // Returns time.Now().UTC() if not set. func GetUserNow(c *gin.Context) time.Time { now, exists := c.Get(UserNowKey) if !exists { return time.Now().UTC() } return now.(time.Time) }