diff --git a/apps/api/src/__tests__/confirmation.test.ts b/apps/api/src/__tests__/confirmation.test.ts index 091268f..351a644 100644 --- a/apps/api/src/__tests__/confirmation.test.ts +++ b/apps/api/src/__tests__/confirmation.test.ts @@ -68,6 +68,7 @@ vi.mock("@groombook/db", () => { }), appointments, eq: () => ({}), + and: (...clauses: unknown[]) => ({}), }; }); diff --git a/apps/api/src/__tests__/rbac.test.ts b/apps/api/src/__tests__/rbac.test.ts index 7351c6a..f975316 100644 --- a/apps/api/src/__tests__/rbac.test.ts +++ b/apps/api/src/__tests__/rbac.test.ts @@ -78,6 +78,7 @@ vi.mock("@groombook/db", () => { }), staff, eq: vi.fn((_col: unknown, _val: unknown) => ({ col: _col, val: _val })), + and: vi.fn((..._clauses: unknown[]) => ({})), }; }); @@ -362,7 +363,7 @@ describe("requireRoleOrSuperUser", () => { const res = await app.request("/test"); expect(res.status).toBe(403); const body = await res.json(); - expect(body.error).toMatch(/super user privileges required/i); + expect(body.error).toMatch(/role.*not permitted/i); }); it("blocks a non-super-user groomer from manager-only routes", async () => { @@ -370,7 +371,7 @@ describe("requireRoleOrSuperUser", () => { const res = await app.request("/test"); expect(res.status).toBe(403); const body = await res.json(); - expect(body.error).toMatch(/super user privileges required/i); + expect(body.error).toMatch(/role.*not permitted/i); }); it("allows a manager with multiple allowed roles", async () => { diff --git a/apps/api/src/middleware/rbac.ts b/apps/api/src/middleware/rbac.ts index b8473e8..2f5acdd 100644 --- a/apps/api/src/middleware/rbac.ts +++ b/apps/api/src/middleware/rbac.ts @@ -149,9 +149,9 @@ export function requireRoleOrSuperUser( } return c.json( { - error: staffRow.isSuperUser - ? `Forbidden: role '${staffRow.role}' is not permitted` - : "Forbidden: super user privileges required", + error: hasAllowedRole + ? "Forbidden: super user privileges required" + : `Forbidden: role '${staffRow.role}' is not permitted`, }, 403 ); diff --git a/apps/api/src/routes/book.ts b/apps/api/src/routes/book.ts index aab3399..26b7cae 100644 --- a/apps/api/src/routes/book.ts +++ b/apps/api/src/routes/book.ts @@ -277,14 +277,24 @@ bookRouter.get("/confirm/:token", async (c) => { return c.redirect(`${BASE_URL()}/booking/error`); } - await db + const updated = await db .update(appointments) .set({ confirmationStatus: "confirmed", confirmedAt: new Date(), updatedAt: new Date(), }) - .where(eq(appointments.id, appt.id)); + .where( + and( + eq(appointments.confirmationToken, token), + eq(appointments.confirmationStatus, "pending") + ) + ) + .returning(); + + if (updated.length === 0) { + return c.redirect(`${BASE_URL()}/booking/error`); + } return c.redirect(`${BASE_URL()}/booking/confirmed`); }); @@ -314,7 +324,7 @@ bookRouter.get("/cancel/:token", async (c) => { return c.redirect(`${BASE_URL()}/booking/error`); } - await db + const updated = await db .update(appointments) .set({ confirmationStatus: "cancelled", @@ -322,7 +332,17 @@ bookRouter.get("/cancel/:token", async (c) => { confirmationToken: null, updatedAt: new Date(), }) - .where(eq(appointments.id, appt.id)); + .where( + and( + eq(appointments.confirmationToken, token), + eq(appointments.confirmationStatus, "pending") + ) + ) + .returning(); + + if (updated.length === 0) { + return c.redirect(`${BASE_URL()}/booking/error`); + } return c.redirect(`${BASE_URL()}/booking/cancelled`); });