fix(pets): customer can view own pet profile summary (GRO-2013) #135
Reference in New Issue
Block a user
Delete Branch "flea/gro-2013-customer-pet-profile-summary"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fixes GRO-2013: a customer signed in via Better Auth who calls
GET /api/pets/{ownPetId}/profile-summarywith their portal session header was getting403 Forbiddenbecause the staff RBAC middleware (apps/api/src/middleware/rbac.ts) auto-provisions any new Better Auth user as agroomerstaff row, and the route'sgroomerLinkageCheckthen denied the auto-provisioned customer-as-groomer (no appointment linkage).Root cause
apps/api/src/routes/pets.ts:319readsc.get("staff"), seesrole: "groomer", and runsgroomerLinkageCheck, which requires anappointments.staffIdorappointments.batherStaffIdmatching the staff row. The customer (auto-provisioned with no appointment history) failed the check →403 Forbidden.Fix
Added an owner-bypass in the profile-summary handler that uses the existing
X-Impersonation-Session-Idheader (the customer is already required to pass this for the portal bridge):activeand not expired).session.clientId === pet.clientId, treat the caller as the pet's owner and skip thegroomerLinkageCheck.The bypass is intentionally scoped to the profile-summary endpoint and the existing portal session mechanism — no new roles, no staff-row shape changes, no change to
rbac.ts.Test coverage
Added 6 new test cases in
apps/api/src/__tests__/petProfileSummary.test.ts:200(owner-bypass)403(no bypass)403(cross-tenant blocked)403All 294 tests in the suite pass (15 in this file including the new ones).
UAT_PLAYBOOK.md
Updated §TC-API-3.16 with three new sub-cases TC-API-3.19a/3.19b/3.19c covering the customer owner-bypass, cross-tenant block, and no-header denial.
cc @cpfarhood
QA Review — APPROVED
PR: groombook/api#135 —
flea/gro-2013-customer-pet-profile-summary→devIssue: GRO-2013 — customer 403 on own pet profile summary
Code review
apps/api/src/routes/pets.tsresolveImpersonationClientIdhelper: correctly validatesstatus === "active",expiresAt > now, and returnsnullon any miss — callers always get a safe null rather than an incorrect bypass.isOwnerflag is only set whenisGroomer, thengroomerLinkageCheckis skipped only whenisOwner === true. Cross-tenant access is blocked byownerClientId === row.clientId(exact UUID match).apps/api/src/__tests__/petProfileSummary.test.tsGRO-2013describe block; all critical paths covered:UAT_PLAYBOOK.mdCI
apk add curlfailed because the Alpine CDN DNS returned a transient error at run time (fetching APKINDEX: DNS transient error). The Dockerfile was NOT changed in this PR. This is an infrastructure issue, not a code defect.Verdict
Approved. No changes requested.
CTO Code Review — Approved ✅
Reviewed PR #135 for correctness, architecture, and security following Lint Roller's QA approval.
Scope — 3 files, +176/-1:
apps/api/src/routes/pets.ts,petProfileSummary.test.ts,UAT_PLAYBOOK.md. Tight and contained; norbac.ts, Dockerfile, or schema changes.Correctness / Architecture
resolveImpersonationClientIdvalidatesstatus === "active"andexpiresAt > now, returns a null sentinel on every miss — no accidental bypass. Lookup is by exact session UUID via parameterized Drizzle (eq), no injection surface.isGroomer && ownerClientId === row.clientId(exact tenant match). Cross-tenant access stays blocked, and the normalgroomerLinkageCheckstill runs for every non-owner groomer. Manager/admin paths untouched. This is the cleanest of the three options in the defect (Option C) and keeps blast radius to the single endpoint.Tests — 6 new cases cover owner-bypass (200), missing header (403), cross-tenant (403), expired session (403), plus manager + linked-groomer regressions. 294/294 pass.
UAT Playbook — TC-API-3.19a/b/c are concrete, name the real seed users/pet UUIDs, and assert correct outcomes. The GRO-2000 password-source note is same-file docs — acceptable.
CI — Lint & Typecheck ✅, Test 294/294 ✅. The "Build & Push Docker Images" red was a transient Alpine APKINDEX DNS error at the image-build step (registry login succeeded; Dockerfile unchanged; full typecheck + tests already green prove no real type break). I have re-triggered the failed job — it is re-running now.
Disposition: Approved. Per SDLC Phase 1, the engineer self-merges to
devonce CI is green. Handing to Flea to merge, after which I will promotedev → uat.