Add timezone backend support (handlers, utilities, user context)

- Add FormatInTimezone and FormatTimestamptzInTimezone helpers
  in templates/utils.go for timezone-aware time display
- Add Timezone field to templates.User struct
- Pass user timezone from DB to template context in helpers.go
- Add timezone update handling in auth.go UpdateProfile with
  validation via time.LoadLocation
- Add UpdateTimezoneSettings handler in system_settings.go for
  admin system-wide default timezone using UpdateSystemSetting
This commit is contained in:
2026-04-27 21:30:53 -04:00
parent caf50ade31
commit 27e9a654bf
5 changed files with 70 additions and 0 deletions
+17
View File
@@ -87,6 +87,7 @@ type UpdateProfileRequest struct {
FirstName string `json:"first_name,omitempty" validate:"omitempty,max=100"` FirstName string `json:"first_name,omitempty" validate:"omitempty,max=100"`
LastName string `json:"last_name,omitempty" validate:"omitempty,max=100"` LastName string `json:"last_name,omitempty" validate:"omitempty,max=100"`
Theme string `json:"theme,omitempty" validate:"omitempty"` Theme string `json:"theme,omitempty" validate:"omitempty"`
Timezone string `json:"timezone,omitempty" validate:"omitempty"`
} }
type AdminUpdateUserRequest struct { type AdminUpdateUserRequest struct {
@@ -564,6 +565,22 @@ func (h *AuthHandler) UpdateProfile(c *echo.Context) error {
} }
} }
// Update timezone
if req.Timezone != "" {
if _, err := time.LoadLocation(req.Timezone); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid timezone",
})
}
err := h.db.UpdateUserTimezone(ctx, database.UpdateUserTimezoneParams{
ID: pgtype.UUID{Bytes: userUUID, Valid: true},
Timezone: pgtype.Text{String: req.Timezone, Valid: true},
})
if err != nil {
return err
}
}
// Update email (if provided) // Update email (if provided)
if req.Email != "" { if req.Email != "" {
existingUser, err := h.db.GetUserByEmail(c.Request().Context(), req.Email) existingUser, err := h.db.GetUserByEmail(c.Request().Context(), req.Email)
+23
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
"time"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5"
@@ -31,6 +32,28 @@ type ScanSettingsResponse struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
} }
type UpdateTimezoneSettingsRequest struct {
DefaultTimezone string `json:"default_timezone" validate:"required"`
}
func (h *SystemSettingsHandler) UpdateTimezoneSettings(c *echo.Context) error {
var req UpdateTimezoneSettingsRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
}
if _, err := time.LoadLocation(req.DefaultTimezone); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid timezone"})
}
err := h.db.UpdateSystemSetting(c.Request().Context(), database.UpdateSystemSettingParams{
SettingKey: "default_timezone",
SettingValue: req.DefaultTimezone,
})
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{"message": "Timezone updated"})
}
func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error { func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
var req UpdateScanSettingsRequest var req UpdateScanSettingsRequest
if err := c.Bind(&req); err != nil { if err := c.Bind(&req); err != nil {
+6
View File
@@ -35,6 +35,11 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
userTheme = userDB.Theme.String userTheme = userDB.Theme.String
} }
userTimezone := "UTC"
if userDB.Timezone.Valid {
userTimezone = userDB.Timezone.String
}
// Extract JWT token for WebSocket authentication // Extract JWT token for WebSocket authentication
token := "" token := ""
if cookie, err := c.Cookie("token"); err == nil { if cookie, err := c.Cookie("token"); err == nil {
@@ -48,6 +53,7 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
Role: userRole, Role: userRole,
Theme: userTheme, Theme: userTheme,
Token: token, Token: token,
Timezone: userTimezone,
}, nil }, nil
} }
+1
View File
@@ -16,6 +16,7 @@ type User struct {
LastName string LastName string
CreatedAt time.Time CreatedAt time.Time
Token string Token string
Timezone string
} }
type PageData struct { type PageData struct {
+23
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"strings" "strings"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
@@ -197,3 +198,25 @@ func formatAlternateInfo(data []byte) string {
return strings.Join(parts, " ") return strings.Join(parts, " ")
} }
// FormatInTimezone formats a time.Time in the specified timezone as MM-DD-YYYY HH:MM
func FormatInTimezone(t time.Time, timezone string) string {
if t.IsZero() {
return ""
}
loc, err := time.LoadLocation(timezone)
if err != nil {
loc = time.UTC
}
return t.In(loc).Format("01-02-2006 03:04 PM")
}
// FormatTimestamptzInTimezone formats a pgtype.Timestamptz in the specified timezone
func FormatTimestamptzInTimezone(t pgtype.Timestamptz, timezone string) string {
if !t.Valid {
return ""
}
return FormatInTimezone(t.Time, timezone)
}