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 <noreply@paperclip.ing>
This commit is contained in:
Flea Flicker
2026-04-15 02:09:29 +00:00
parent 203b600713
commit c89c2fd6b4
2 changed files with 63 additions and 61 deletions
+35 -30
View File
@@ -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
+28 -31
View File
@@ -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<AppEnv>();
@@ -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 });
});