The previous fix for GRO-2013 (customer cannot view own pet profile
summary) landed in apps/api/src/routes/pets.ts, which is dead code
in the Docker build path. The Dockerfile does COPY src/ + pnpm build
from the repo root, so apps/api/ is never copied into the image and
is not a pnpm-workspace member.
Port the owner-bypass into the deployed-tree handler src/routes/pets.ts:
- Add resolveImpersonationClientId(db, c) helper that reads the
X-Impersonation-Session-Id header, validates the session is active
and not expired, and returns its clientId (or null).
- Gate the existing groomer 403 in GET /:id/profile-summary so an
owner (session.clientId === pet.clientId) bypasses the
appointment-linkage check. This mirrors the already-reviewed logic
from apps/api/src/routes/pets.ts:318-364.
- Cross-tenant access remains blocked: the bypass requires
session.clientId === pet.clientId, and groomers with no portal
session still 403 as before.
Tests (src/__tests__/petProfileSummary.test.ts — new file, mirroring
the dead-tree test pattern but pointing at the deployed handler):
- Customer with valid active session for pet's client → 200
- Customer with no header → 403
- Customer with session for a different client → 403
- Customer with expired session → 403
- Customer with ended (status != active) session → 403
- Customer with unknown session id → 403
- Manager does not need the impersonation header (regression)
- Groomer with linkage to pet's client still works (regression)
- Customer cannot view another client's pet (cross-tenant block)
Full @groombook/api test suite: 560 passed (39 files).
Note (out of scope): the apps/api/ duplicate tree is dead code
producing false-green coverage — recommend filing a separate tech-debt
issue to delete apps/api/ or wire it into the workspace, but not
blocking this fix on it.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When a customer (e.g. uat-customer@groombook.dev) signs in via Better Auth
and calls GET /api/pets/{ownPetId}/profile-summary with their portal
session header, the staff RBAC middleware auto-provisions a 'groomer'
staff row for them (rbac.ts) and the profile-summary route's
groomerLinkageCheck then denies the request with 403 Forbidden, because
the auto-provisioned customer-as-groomer has no appointment linkage.
This adds an owner-bypass: when a groomer-role staff row is making the
request with a valid X-Impersonation-Session-Id header, and the resolved
impersonation session's clientId matches the pet's clientId, we treat
the caller as the pet's owner and skip the groomerLinkageCheck.
The bypass is intentionally scoped to the profile-summary endpoint and
to the existing portal session mechanism (no new roles, no staff-row
shape changes). Cross-tenant access is still blocked because the
bypass requires session.clientId === pet.clientId.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The 'Source of truth for UAT passwords' subsection under Pre-conditions
records:
- The seed-uat-passwords Secret in groombook-uat is the live source.
- The Bitnami SealedSecret apps/overlays/uat/ss-seed-uat-passwords.yaml
in groombook/infra is the single upstream source of truth.
- A kubectl recipe to pull the current values for SUPER / GROOMER /
TESTER / CUSTOMER at the start of every UAT run.
- The 'captured env var from a previous rotation produces 401' failure
mode that GRO-2000 hit, and the manual-reseed escape hatch if the
login still 401s after pulling the live value.
Refs: GRO-2000, GRO-1977 (idempotent re-hash), GRO-1999 (enum fix that
allowed the seed Job to run cleanly again).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>