fix(portal): validate waitlist preferredTime/preferredDate, return 400 on bad input (GRO-2211) (#179)
CI / Test (pull_request) Successful in 26s
CI / Test (push) Successful in 29s
CI / Lint & Typecheck (pull_request) Successful in 31s
CI / Lint & Typecheck (push) Successful in 34s
CI / Build & Push Docker Images (pull_request) Failing after 13s
CI / Build & Push Docker Images (push) Successful in 48s
CI / Test (pull_request) Successful in 26s
CI / Test (push) Successful in 29s
CI / Lint & Typecheck (pull_request) Successful in 31s
CI / Lint & Typecheck (push) Successful in 34s
CI / Build & Push Docker Images (pull_request) Failing after 13s
CI / Build & Push Docker Images (push) Successful in 48s
This commit was merged in pull request #179.
This commit is contained in:
+22
-6
@@ -559,17 +559,33 @@ portalRouter.post("/appointments/:id/cancel", async (c) => {
|
||||
|
||||
// ─── Client-facing waitlist routes ────────────────────────────────────────────
|
||||
|
||||
// Postgres `date` / `time` columns reject arbitrary strings (e.g. a full ISO
|
||||
// datetime), throwing a DateTimeParseError that surfaces as an unhandled 500.
|
||||
// Constrain client input here so malformed values are rejected with a 400 by
|
||||
// zValidator before they ever reach the DB (GRO-2211 defense-in-depth).
|
||||
const preferredDateSchema = z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/, "preferredDate must be YYYY-MM-DD");
|
||||
const preferredTimeSchema = z
|
||||
.string()
|
||||
.regex(/^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/, "preferredTime must be HH:MM or HH:MM:SS");
|
||||
|
||||
// Normalize HH:MM → HH:MM:SS so it matches the Postgres `time` column format.
|
||||
function normalizeTime(value: string): string {
|
||||
return value.length === 5 ? `${value}:00` : value;
|
||||
}
|
||||
|
||||
const createWaitlistEntrySchema = z.object({
|
||||
petId: z.string().uuid(),
|
||||
serviceId: z.string().uuid(),
|
||||
preferredDate: z.string(),
|
||||
preferredTime: z.string(),
|
||||
preferredDate: preferredDateSchema,
|
||||
preferredTime: preferredTimeSchema,
|
||||
});
|
||||
|
||||
const updateWaitlistEntrySchema = z.object({
|
||||
status: z.literal("cancelled").optional(),
|
||||
preferredDate: z.string().optional(),
|
||||
preferredTime: z.string().optional(),
|
||||
preferredDate: preferredDateSchema.optional(),
|
||||
preferredTime: preferredTimeSchema.optional(),
|
||||
});
|
||||
|
||||
portalRouter.post(
|
||||
@@ -587,7 +603,7 @@ portalRouter.post(
|
||||
petId: body.petId,
|
||||
serviceId: body.serviceId,
|
||||
preferredDate: body.preferredDate,
|
||||
preferredTime: body.preferredTime,
|
||||
preferredTime: normalizeTime(body.preferredTime),
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -618,7 +634,7 @@ portalRouter.patch(
|
||||
const updateData: Record<string, unknown> = { updatedAt: new Date() };
|
||||
if (body.status !== undefined) updateData.status = body.status;
|
||||
if (body.preferredDate !== undefined) updateData.preferredDate = body.preferredDate;
|
||||
if (body.preferredTime !== undefined) updateData.preferredTime = body.preferredTime;
|
||||
if (body.preferredTime !== undefined) updateData.preferredTime = normalizeTime(body.preferredTime);
|
||||
|
||||
const [updated] = await db
|
||||
.update(waitlistEntries)
|
||||
|
||||
Reference in New Issue
Block a user