feat(api): RBAC Phase 2 - row-level data scoping for groomer role #121

Closed
groombook-engineer[bot] wants to merge 17 commits from feature/gro-48-rbac-row-level into main

17 Commits

Author SHA1 Message Date
Flea Flicker 1477f4ee77 fix(e2e): remove stale DevLoginSelector tests
The /login route is now guarded by import.meta.env.DEV (fix from
GRO-56), so the DevLoginSelector never renders in production builds.
All 7 tests in login.spec.ts tested this dev-only page and were
correctly failing in the E2E suite. Remove them entirely.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-27 02:11:57 +00:00
Flea Flicker d1970bd3e2 fix(tests): use AppEnv type for clients test Hono app
Properly type the test app with AppEnv so c.set("staff", ...) satisfies
TypeScript's strict overload check. Fixes TS2769 typecheck failure on CI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-27 01:57:07 +00:00
Flea Flicker 1e86f1c088 Merge remote-tracking branch 'origin/main' into feature/gro-48-rbac-row-level 2026-03-27 01:55:20 +00:00
Flea Flicker 0fa53645b9 fix(tests): inject mock staff context in clients test suite
RBAC Phase 2 added row-level scoping that reads c.get("staff") on every
clients route. The test app had no middleware setting this context, causing
a TypeError (undefined.role) and 500 responses on all GET tests.

Add a manager-role mock staff middleware to the test Hono app so the
existing tests continue to cover the non-groomer code path.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-27 01:52:25 +00:00
Flea Flicker cd5feb1a14 fix(gro-56): guard dev login page behind import.meta.env.DEV
The DevLoginSelector page (including the "Continue as default dev user"
button) was rendering in production when AUTH_DISABLED=true. This guards
the /login route so the page only renders in Vite development mode
(import.meta.env.DEV). Also removes the skip-login button entirely since
it bypassed user selection without any identity assertion.

- Guard /login route with import.meta.env.DEV in App.tsx
- Remove skipLogin button from DevLoginSelector.tsx
- Add vite/client types to web tsconfig
- Remove corresponding e2e test

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-27 01:10:34 +00:00
Flea Flicker eeda5099be feat(api): RBAC Phase 2 - row-level data scoping for groomer role
Filter query results at the route handler level when the authenticated
staff role is 'groomer':

- GET /api/appointments: WHERE staffId = <groomer id>
- GET /api/appointments/🆔 403 if not assigned to groomer
- GET /api/clients: clients with ≥1 appointment for this groomer
- GET /api/clients/🆔 403 if no appointment linkage
- GET /api/pets: pets owned by groomer-linked clients
- GET /api/pets/:petId: 403 if no appointment linkage

Managers and receptionists: no change.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 21:57:09 +00:00
Flea Flicker 9535872bd2 fix(gro-38): add admin seed endpoint and SEED_KNOWN_USERS_ONLY mode
Add POST /api/admin/seed — a manager-only API endpoint that creates
minimal known users (Demo Manager staff + Demo Client + Demo Dog + basic
services) via the API instead of direct DB writes.

Add SEED_KNOWN_USERS_ONLY=true env var to seed.ts for lean prod/demo
seeding. Known users get deterministic UUIDs so seeding is idempotent.

Note: infra changes (disabling AUTH_DISABLED in prod/demo, fixing the
failing seed job) require updates to groombook/infra repo.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 18:55:31 +00:00
Flea Flicker c68159e97a fix(waitlist): update portal PATCH tests to use allowed status value
Now that portal clients are restricted to status:"cancelled" only,
update the PATCH /portal/waitlist/:id tests to send a valid value
so auth and ownership checks are exercised correctly.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:47:39 +00:00
Flea Flicker 8ea383b98b fix(waitlist): fix indentation on null check in portal.ts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:45:56 +00:00
Flea Flicker 0ff8dd161d fix(waitlist): address CTO review on PR #110
- Restrict portal PATCH waitlist status to z.literal("cancelled") only
- Appointment notes: field projection + null check from PR #109
- Resolve index.ts conflict: keep both portal and calendar public routes
- Resolve portal.ts conflict: keep min(1) validation for customerNotes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:39:38 +00:00
Flea Flicker 1e696ca684 fix(waitlist): address CTO review on PR #110
- Move client-facing POST/PATCH/DELETE waitlist routes to portalRouter
  so impersonation sessions can reach them (were blocked by requireRole guard)
- Fix portalRouter double-mount: remove from auth-protected api block,
  register publicly at app.route("/api/portal", ...) instead
- Replace N+1 queries in GET /waitlist with a single JOIN across
  clients, pets, and services tables
- Remove dead expiredIds variable in markExpiredEntries; use .some()
  instead of computing an array only for its length
- Fix stray indentation in appointments.ts DELETE handler (line 487)
- Update waitlist tests to exercise routes at new /portal/waitlist paths;
  add leftJoin and lt to chainable mock

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:30:35 +00:00
Scrubs McBarkley 84ab5a00f5 fix(waitlist): use slice instead of split to avoid TS strict null check errors
TypeScript's split()[0] is typed as string | undefined in strict mode.
Using slice(0, 10) is cleaner and avoids the type issue.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:30:35 +00:00
Scrubs McBarkley 1f56ba98f7 feat(waitlist): add lazy expiry for entries with past preferredDate (GRO-180)
When reading waitlist entries, active entries with preferredDate < today
are marked as expired both in the database and in the response.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:30:35 +00:00
Scrubs McBarkley a0f93fbb3f feat: add waitlist cancellation hook and email notification (GRO-180)
- Add buildWaitlistNotificationEmail() email template
- Add notifyWaitlistForAppointment() service to find matching waitlist
  entries and send email notifications when appointments are cancelled
- Wire up notifyWaitlist call in DELETE /api/appointments/:id handler
- Fire-and-forget notification (non-blocking, logs errors)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:30:35 +00:00
Scrubs McBarkley 09cbf00157 fix(waitlist): address QA review comments - auth fixes and pgEnum type
- Add 401 when DELETE /waitlist/:id has no session (auth bypass fix)
- Add auth to PATCH /waitlist/:id (was zero auth)
- Add RBAC guard for /waitlist/* routes
- Fix migration to use proper ENUM type instead of TEXT
- Add unit tests for auth scenarios

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:29:41 +00:00
Scrubs McBarkley 232827ad29 feat: add waitlist entries table and API routes (GRO-105)
- Migration 0015: new waitlist_entries table with indexes
- Schema update: add waitlistEntries table and waitlistStatusEnum
- Staff API: GET /api/waitlist, GET /api/waitlist/:id
- Portal API: POST /api/waitlist (via impersonation session), DELETE /api/waitlist/:id
- Note: cancellation hook and email notification pending

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:28:18 +00:00
Scrubs McBarkley 012b6bc1cd feat: add customer-facing appointment notes (GRO-106)
- Migration 0014: add customer_notes column to appointments
- Schema update: add customerNotes field to appointments table
- Factory update: include customerNotes in buildAppointment
- Portal route: PATCH /api/portal/appointments/:id/notes
  - Ownership validation via impersonation session
  - Future-only validation (no edits after start)
  - 500 character limit
- Register portal router in index.ts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 08:28:18 +00:00