From c89c2fd6b45999efd7c003762b1b0d153679b687 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Wed, 15 Apr 2026 02:09:29 +0000 Subject: [PATCH] Revert RBAC/authorization changes in appointmentGroups and groomingLogs These files are out of scope for the input validation PR. Only the 5-route validation changes (invoices, book, appointments, services, stripe-webhooks) should be included. Co-Authored-By: Paperclip --- apps/api/src/routes/appointmentGroups.ts | 65 +++++++++++++----------- apps/api/src/routes/groomingLogs.ts | 59 ++++++++++----------- 2 files changed, 63 insertions(+), 61 deletions(-) diff --git a/apps/api/src/routes/appointmentGroups.ts b/apps/api/src/routes/appointmentGroups.ts index 859f64e..d28cdf6 100644 --- a/apps/api/src/routes/appointmentGroups.ts +++ b/apps/api/src/routes/appointmentGroups.ts @@ -82,7 +82,7 @@ appointmentGroupsRouter.get("/", async (c) => { groupApptMap.get(appt.groupId)!.push(appt); } - let result = groups + const result = groups .map((g) => ({ ...g, appointments: (groupApptMap.get(g.id) ?? []).sort( @@ -91,11 +91,12 @@ appointmentGroupsRouter.get("/", async (c) => { })) .filter((g) => !from || g.appointments.length > 0); - // Groomer: filter to groups where at least one appointment is assigned to them if (isGroomer) { - result = result.filter((g) => - g.appointments.some( - (a) => a.staffId === staffRow.id || a.batherStaffId === staffRow.id + return c.json( + result.filter((g) => + g.appointments.some( + (a) => a.staffId === staffRow.id || a.batherStaffId === staffRow.id + ) ) ); } @@ -125,8 +126,8 @@ appointmentGroupsRouter.get("/:id", async (c) => { serviceId: appointments.serviceId, serviceName: services.name, staffId: appointments.staffId, - staffName: staff.name, batherStaffId: appointments.batherStaffId, + staffName: staff.name, status: appointments.status, startTime: appointments.startTime, endTime: appointments.endTime, @@ -140,12 +141,13 @@ appointmentGroupsRouter.get("/:id", async (c) => { .where(eq(appointments.groupId, id)) .orderBy(appointments.startTime); - // Groomer: verify at least one appointment in the group is assigned to them - if (isGroomer) { - const hasAccess = groupAppts.some( + if ( + isGroomer && + !groupAppts.some( (a) => a.staffId === staffRow.id || a.batherStaffId === staffRow.id - ); - if (!hasAccess) return c.json({ error: "Forbidden" }, 403); + ) + ) { + return c.json({ error: "Forbidden" }, 403); } const [client] = await db @@ -164,13 +166,12 @@ appointmentGroupsRouter.post( async (c) => { const db = getDb(); const staffRow = c.get("staff"); - const isGroomer = staffRow?.role === "groomer"; - - // Only managers and receptionists can create group bookings - if (isGroomer) { - return c.json({ error: "Forbidden: groomers cannot create group bookings" }, 403); + if (staffRow?.role === "groomer") { + return c.json( + { error: "Forbidden: groomers cannot create group bookings" }, + 403 + ); } - const body = c.req.valid("json"); const startTime = new Date(body.startTime); @@ -278,23 +279,24 @@ appointmentGroupsRouter.patch( const staffRow = c.get("staff"); const isGroomer = staffRow?.role === "groomer"; - // Verify group exists const [group] = await db - .select() + .select({ id: appointmentGroups.id }) .from(appointmentGroups) .where(eq(appointmentGroups.id, id)); if (!group) return c.json({ error: "Not found" }, 404); - // Groomer: verify at least one appointment in the group is assigned to them if (isGroomer) { const groupAppts = await db .select({ staffId: appointments.staffId, batherStaffId: appointments.batherStaffId }) .from(appointments) .where(eq(appointments.groupId, id)); - const hasAccess = groupAppts.some( - (a) => a.staffId === staffRow.id || a.batherStaffId === staffRow.id - ); - if (!hasAccess) return c.json({ error: "Forbidden" }, 403); + if ( + !groupAppts.some( + (a) => a.staffId === staffRow.id || a.batherStaffId === staffRow.id + ) + ) { + return c.json({ error: "Forbidden" }, 403); + } } const [updated] = await db @@ -303,6 +305,7 @@ appointmentGroupsRouter.patch( .where(eq(appointmentGroups.id, id)) .returning(); + if (!updated) return c.json({ error: "Not found" }, 404); return c.json(updated); } ); @@ -316,21 +319,23 @@ appointmentGroupsRouter.delete("/:id", async (c) => { const isGroomer = staffRow?.role === "groomer"; const [group] = await db - .select() + .select({ id: appointmentGroups.id }) .from(appointmentGroups) .where(eq(appointmentGroups.id, id)); if (!group) return c.json({ error: "Not found" }, 404); - // Groomer: verify at least one appointment in the group is assigned to them if (isGroomer) { const groupAppts = await db .select({ staffId: appointments.staffId, batherStaffId: appointments.batherStaffId }) .from(appointments) .where(eq(appointments.groupId, id)); - const hasAccess = groupAppts.some( - (a) => a.staffId === staffRow.id || a.batherStaffId === staffRow.id - ); - if (!hasAccess) return c.json({ error: "Forbidden" }, 403); + if ( + !groupAppts.some( + (a) => a.staffId === staffRow.id || a.batherStaffId === staffRow.id + ) + ) { + return c.json({ error: "Forbidden" }, 403); + } } await db diff --git a/apps/api/src/routes/groomingLogs.ts b/apps/api/src/routes/groomingLogs.ts index 6e0aadb..1f7f85a 100644 --- a/apps/api/src/routes/groomingLogs.ts +++ b/apps/api/src/routes/groomingLogs.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod/v3"; -import { and, appointments, desc, eq, getDb, groomingVisitLogs, or } from "@groombook/db"; +import { and, desc, eq, getDb, groomingVisitLogs, appointments, or } from "@groombook/db"; import type { AppEnv } from "../middleware/rbac.js"; export const groomingLogsRouter = new Hono(); @@ -20,14 +20,12 @@ const createLogSchema = z.object({ groomingLogsRouter.get("/", async (c) => { const db = getDb(); const petId = c.req.query("petId"); + if (!petId) return c.json({ error: "petId is required" }, 400); const staffRow = c.get("staff"); const isGroomer = staffRow?.role === "groomer"; - if (!petId) return c.json({ error: "petId is required" }, 400); - - // Groomer: verify they have at least one appointment for this pet if (isGroomer) { - const [hasAppt] = await db + const [appt] = await db .select({ id: appointments.id }) .from(appointments) .where( @@ -40,7 +38,7 @@ groomingLogsRouter.get("/", async (c) => { ) ) .limit(1); - if (!hasAppt) return c.json({ error: "Forbidden" }, 403); + if (!appt) return c.json({ error: "Forbidden" }, 403); } const rows = await db @@ -56,28 +54,11 @@ groomingLogsRouter.post( zValidator("json", createLogSchema), async (c) => { const db = getDb(); - const { groomedAt, appointmentId, ...rest } = c.req.valid("json"); + const { groomedAt, petId, appointmentId, ...rest } = c.req.valid("json"); const staffRow = c.get("staff"); const isGroomer = staffRow?.role === "groomer"; - // Groomer: verify they have at least one appointment for this pet if (isGroomer) { - const [hasAppt] = await db - .select({ id: appointments.id }) - .from(appointments) - .where( - and( - eq(appointments.petId, rest.petId), - or( - eq(appointments.staffId, staffRow.id), - eq(appointments.batherStaffId, staffRow.id) - ) - ) - ) - .limit(1); - if (!hasAppt) return c.json({ error: "Forbidden" }, 403); - - // If appointmentId is provided, verify groomer is assigned to that specific appointment if (appointmentId) { const [appt] = await db .select({ id: appointments.id }) @@ -93,6 +74,21 @@ groomingLogsRouter.post( ) .limit(1); if (!appt) return c.json({ error: "Forbidden" }, 403); + } else { + const [appt] = await db + .select({ id: appointments.id }) + .from(appointments) + .where( + and( + eq(appointments.petId, petId), + or( + eq(appointments.staffId, staffRow.id), + eq(appointments.batherStaffId, staffRow.id) + ) + ) + ) + .limit(1); + if (!appt) return c.json({ error: "Forbidden" }, 403); } } @@ -100,6 +96,8 @@ groomingLogsRouter.post( .insert(groomingVisitLogs) .values({ ...rest, + petId, + appointmentId: appointmentId ?? null, groomedAt: groomedAt ? new Date(groomedAt) : new Date(), }) .returning(); @@ -113,16 +111,15 @@ groomingLogsRouter.delete("/:id", async (c) => { const staffRow = c.get("staff"); const isGroomer = staffRow?.role === "groomer"; - // Fetch the log to get the petId const [log] = await db .select() .from(groomingVisitLogs) - .where(eq(groomingVisitLogs.id, id)); + .where(eq(groomingVisitLogs.id, id)) + .limit(1); if (!log) return c.json({ error: "Not found" }, 404); - // Groomer: verify the log's petId links to an appointment where groomer is assigned if (isGroomer) { - const [hasAppt] = await db + const [appt] = await db .select({ id: appointments.id }) .from(appointments) .where( @@ -135,12 +132,12 @@ groomingLogsRouter.delete("/:id", async (c) => { ) ) .limit(1); - if (!hasAppt) return c.json({ error: "Forbidden" }, 403); + if (!appt) return c.json({ error: "Forbidden" }, 403); } await db .delete(groomingVisitLogs) - .where(eq(groomingVisitLogs.id, id)); - + .where(eq(groomingVisitLogs.id, id)) + .returning(); return c.json({ ok: true }); });