fix(auth): validate User.timezone against IANA zone list on profile update

The shared profile-update schema accepted any 1-64 character string
for the timezone field and persisted it directly into User.timezone.
Downstream callers — most notably the v1.4.38 dashboard summary route
— pass the raw stored value into postgres via
`to_char(measured_at AT TIME ZONE ${userTz})`, which hard-fails with
`invalid_parameter_value` for any unknown zone. Before v1.4.38 the
field was consumed only through the defensive isValidTimezone wrapper
in @/lib/tz/format and silently fell back to Europe/Berlin, so the
gap was latent; the new SQL path made it a self-DoS regression on any
account that ever wrote a junk zone.

Chains the existing isValidTimezone helper into the Zod schema as a
refine so the API now returns 422 "Invalid IANA timezone" at the
write boundary instead of writing junk and failing on read.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 19:02:44 +02:00
1 parent 679fcd2d08
commit 24379721d1
1 file changed
+7 -1
+7 -1
View File
@@ -6,12 +6,18 @@
import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { profileSchema } from "@/lib/validations/auth";
import { isValidTimezone } from "@/lib/tz/format";
import { z } from "zod/v4";
const extendedProfileSchema = profileSchema.extend({
displayName: z.string().min(1).max(80).nullable().optional(),
locale: z.enum(["de", "en"]).nullable().optional(),
timezone: z.string().min(1).max(64).optional(),
timezone: z
.string()
.min(1)
.max(64)
.refine(isValidTimezone, "Invalid IANA timezone")
.optional(),
});
export type ApplyProfileInput = z.infer<typeof extendedProfileSchema>;