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 <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-07-03 01:51:18 +00:00
co-authored by Cursor
parent 78ffd0ae52
commit 8d5fbf18b4
3 changed files with 9 additions and 14 deletions
+1
View File
@@ -25,6 +25,7 @@
"hono": "^4.4.7", "hono": "^4.4.7",
"ioredis": "^5.11.1", "ioredis": "^5.11.1",
"jose": "^5.4.0", "jose": "^5.4.0",
"moment-timezone": "^0.6.2",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"nodemailer": "^7.0.13", "nodemailer": "^7.0.13",
"pdfkit": "^0.17.2", "pdfkit": "^0.17.2",
+5 -11
View File
@@ -1,5 +1,6 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import moment from 'moment-timezone';
/** /**
* Get database type (reads env var each time to handle module loading order) * Get database type (reads env var each time to handle module loading order)
@@ -59,17 +60,10 @@ export function parseEventDatetime(
return new Date(datetime); return new Date(datetime);
} }
// Treat the digits as UTC so we have a stable reference instant. // Interpret the wall-clock digits as local time in `timezone` using
const fakeUTC = new Date(datetime + 'Z'); // moment-timezone's bundled IANA data. This keeps the conversion correct
// regardless of the host Node runtime's (possibly stale) tz database.
// Ask Intl what that UTC instant looks like in both UTC and the target tz. return moment.tz(datetime, timezone).toDate();
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);
} }
/** /**
+3 -3
View File
@@ -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 { 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 { eq, desc, sql, and, gte, lte } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js'; import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js'; import { getNow, toDbDate } from '../lib/utils.js';
interface UserContext { interface UserContext {
id: string; id: string;
@@ -39,8 +39,8 @@ usersRouter.get('/', requireAuth(['admin']), async (c) => {
const conditions: any[] = []; const conditions: any[] = [];
if (role) conditions.push(eq((users as any).role, role)); if (role) conditions.push(eq((users as any).role, role));
if (accountStatus) conditions.push(eq((users as any).accountStatus, accountStatus)); if (accountStatus) conditions.push(eq((users as any).accountStatus, accountStatus));
if (registeredAfter) conditions.push(gte((users as any).createdAt, registeredAfter)); if (registeredAfter) conditions.push(gte((users as any).createdAt, toDbDate(registeredAfter)));
if (registeredBefore) conditions.push(lte((users as any).createdAt, registeredBefore)); if (registeredBefore) conditions.push(lte((users as any).createdAt, toDbDate(registeredBefore)));
if (search) { if (search) {
const like = `%${search.toLowerCase()}%`; const like = `%${search.toLowerCase()}%`;
conditions.push(sql`( conditions.push(sql`(