From 8d5fbf18b4dd603b0f18741628f7b682b243adc0 Mon Sep 17 00:00:00 2001 From: Michilis Date: Fri, 3 Jul 2026 01:51:18 +0000 Subject: [PATCH] Fix event time save off-by-one using bundled tz data. Use moment-timezone in parseEventDatetime so naive America/Asuncion wall-clock times convert correctly regardless of the host Node tz database. Also normalize user registration date filters with toDbDate for Postgres. Co-authored-by: Cursor --- backend/package.json | 1 + backend/src/lib/utils.ts | 16 +++++----------- backend/src/routes/users.ts | 6 +++--- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/backend/package.json b/backend/package.json index f2a5bed..a045340 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,7 @@ "hono": "^4.4.7", "ioredis": "^5.11.1", "jose": "^5.4.0", + "moment-timezone": "^0.6.2", "nanoid": "^5.0.7", "nodemailer": "^7.0.13", "pdfkit": "^0.17.2", diff --git a/backend/src/lib/utils.ts b/backend/src/lib/utils.ts index ac4e7da..103ded7 100644 --- a/backend/src/lib/utils.ts +++ b/backend/src/lib/utils.ts @@ -1,5 +1,6 @@ import { nanoid } from 'nanoid'; import { randomUUID } from 'crypto'; +import moment from 'moment-timezone'; /** * Get database type (reads env var each time to handle module loading order) @@ -59,17 +60,10 @@ export function parseEventDatetime( return new Date(datetime); } - // Treat the digits as UTC so we have a stable reference instant. - const fakeUTC = new Date(datetime + 'Z'); - - // Ask Intl what that UTC instant looks like in both UTC and the target tz. - const utcStr = fakeUTC.toLocaleString('en-US', { timeZone: 'UTC' }); - const tzStr = fakeUTC.toLocaleString('en-US', { timeZone: timezone }); - - // The gap between the two tells us the tz offset at this point in time. - const offsetMs = new Date(utcStr).getTime() - new Date(tzStr).getTime(); - - return new Date(fakeUTC.getTime() + offsetMs); + // Interpret the wall-clock digits as local time in `timezone` using + // moment-timezone's bundled IANA data. This keeps the conversion correct + // regardless of the host Node runtime's (possibly stale) tz database. + return moment.tz(datetime, timezone).toDate(); } /** diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts index 03dcada..5c21728 100644 --- a/backend/src/routes/users.ts +++ b/backend/src/routes/users.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { db, dbGet, dbAll, users, tickets, events, payments, magicLinkTokens, userSessions, invoices, auditLogs, emailLogs, paymentOptions, legalPages, siteSettings } from '../db/index.js'; import { eq, desc, sql, and, gte, lte } from 'drizzle-orm'; import { requireAuth } from '../lib/auth.js'; -import { getNow } from '../lib/utils.js'; +import { getNow, toDbDate } from '../lib/utils.js'; interface UserContext { id: string; @@ -39,8 +39,8 @@ usersRouter.get('/', requireAuth(['admin']), async (c) => { const conditions: any[] = []; if (role) conditions.push(eq((users as any).role, role)); if (accountStatus) conditions.push(eq((users as any).accountStatus, accountStatus)); - if (registeredAfter) conditions.push(gte((users as any).createdAt, registeredAfter)); - if (registeredBefore) conditions.push(lte((users as any).createdAt, registeredBefore)); + if (registeredAfter) conditions.push(gte((users as any).createdAt, toDbDate(registeredAfter))); + if (registeredBefore) conditions.push(lte((users as any).createdAt, toDbDate(registeredBefore))); if (search) { const like = `%${search.toLowerCase()}%`; conditions.push(sql`(