Commit Graph

165 Commits

Author SHA1 Message Date
groombook-engineer[bot] fa92a65a35 fix(portal): revert Dashboard redirect to show message instead
Dashboard had a defense-in-depth Navigate to /login when sessionId is
null. This fires on initial render before the session is set, causing
E2E tests to fail (they wait for the impersonation banner which never
renders because Dashboard redirected away).

Revert to main-branch behavior: show "Please sign in" message instead
of redirecting. The CustomerPortal-level redirect is sufficient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 17:12:42 +00:00
groombook-engineer[bot] 49aa6ac989 fix(portal): prevent premature redirect with sessionAttempted flag
Fixes E2E race condition where setSession and setInitComplete are batched
by React concurrent rendering, causing redirect to fire before session
is set. The sessionAttempted flag tracks "did we try" so redirect only
fires when there was NO attempt, not when the state update is pending.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 16:54:22 +00:00
groombook-engineer[bot] 7443b66739 fix(e2e): remove portal/me mock entirely - not needed for impersonation tests
The portal/me endpoint is only called in the client dev user flow
(devUser.type === 'client'), NOT in the impersonation flow which uses
the sessionId param. Removing this mock eliminates potential interference.
2026-03-31 16:00:58 +00:00
groombook-engineer[bot] 50f3c961ff fix(e2e): simplify impersonation mocks - remove dead POST/dev-session mock, use broader portal/me pattern
The POST /api/portal/dev-session mock is dead code in impersonation tests
since the fixture seeds devUser.type=staff, which skips that code path.
Removed it to eliminate potential interference.

Also changed portal/me mock pattern from 'GET **/api/portal/me' to
'**/api/portal/me**' to ensure it matches correctly regardless of
how Playwright interprets the URL pattern syntax.
2026-03-31 15:45:04 +00:00
groombook-engineer[bot] 1eb274198c fix(e2e): revert portal/dev-session mock to flat ImpersonationSession
The API returns a flat ImpersonationSession object. CustomerPortal.tsx
reads s.id directly from the response. My previous fix incorrectly
wrapped the mock in { session: {...} }, causing s.id to be undefined
and setSession() to never fire.

This reverts the mock structure to be flat, matching the actual API
response format from portal.ts line 516.
2026-03-31 15:15:35 +00:00
groombook-engineer[bot] 6e6336e6ba fix(e2e): correct portal/dev-session mock structure for impersonation tests
The mock returned { id, client } but CustomerPortal.tsx expects
{ session: { id, client } }. This caused setSession to never be called,
leading to redirect to /login and test timeouts.

Also seed dev user in localStorage for impersonation tests to
ensure getDevUser() returns a known state.
2026-03-31 05:28:18 +00:00
Barkley Trimsworth 6f3e6b9bd9 fix(e2e): add portal session mocks to impersonation tests
QA identified that impersonation.spec.ts mocks impersonation
session endpoints but not portal session endpoints. When
CustomerPortal.tsx validates the session it calls GET /api/portal/me
which fails without a mock, causing the redirect to fire and tests
to fail.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-31 01:16:17 +00:00
Barkley Trimsworth 5860d822cf fix(portal): redirect unauthenticated users to login — never show portal chrome (GRO-309)
- CustomerPortal.tsx: add initComplete state to track async session
  initialization. After init completes with no valid session, redirect:
  staff dev users → /admin, all others → /login
- Dashboard.tsx: change !sessionId fallback from dead-end UI to
  <Navigate to="/login" replace /> (defense-in-depth)
- All 85 web unit tests pass

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-31 00:53:59 +00:00
groombook-ceo[bot] 026a2c8b0e fix(portal): wire dev client login to portal session
Merged by CEO (Scrubs McBarkley). Full pipeline complete: engineer fix → QA approved → CTO approved → all CI green. Restores critical customer portal auth functionality.
2026-03-30 18:25:01 +00:00
Barkley Trimsworth 4fb0c7b14d fix(api): use valid staff ID for dev-session impersonation
The hardcoded DEV_STAFF_ID (all zeros) did not exist in the staff
table, causing a foreign-key violation and 500 error. Now falls back
to the demo-manager (KNOWN_STAFF_ID from seed) or any active staff
record instead.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 18:09:09 +00:00
Barkley Trimsworth 08e2f8c1ab fix(web): add missing PWA icon and favicon assets
Adds pwa-192x192.png, pwa-512x512.png, and favicon.svg to the web
public directory. These are referenced by the VitePWA plugin manifest
and were causing 404 errors on every page load.

cc @cpfarhood

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 17:10:58 +00:00
Barkley Trimsworth 51431c7bc1 fix(portal): wire dev client login to portal session
When a client user selects their account from the dev login selector,
the portal previously had no way to establish an authenticated session —
it only checked for a ?sessionId= URL param (used by the real staff
impersonation flow). This caused the portal to always show "Hi, Guest".

Changes:
- POST /api/portal/dev-session: new endpoint (auth-disabled only) that
  creates an impersonation session for a given clientId, using a fixed
  dev staff ID to avoid conflicts with the one-active-session-per-staff
  rule in the real impersonation flow. Sessions are long-lived (24h).
- CustomerPortal: on mount, after checking for ?sessionId=, also check
  for a dev client user in localStorage and call /api/portal/dev-session
  to obtain a session. This mirrors the real impersonation flow so all
  existing portal API calls (which require X-Impersonation-Session-Id)
  work without modification.

cc @cpfarhood

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 17:07:49 +00:00
Barkley Trimsworth 853c55fd04 fix(staff): count only active super users in last-super-user guardrail
Add active=true filter to all 3 superUserCount queries in staff.ts
(revoke, deactivate, delete) so inactive super users aren't counted,
preventing false positives when checking the last-super-user guardrail.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:50:50 +00:00
groombook-engineer[bot] 40143c4efa fix(db): seed ON CONFLICT target uses clients.id instead of non-unique clients.email
Fixes seed script crash — both onConflictDoUpdate calls on clients table now use schema.clients.id (PK) as conflict target instead of non-unique schema.clients.email. Email added to set clause for both call sites.

Resolves GRO-298. Unblocks GRO-290, GRO-295, GRO-297.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 14:44:38 +00:00
groombook-ceo[bot] b385b32120 fix(db): guarantee 5 UAT test clients with pending invoices (GRO-290)
fix(db): guarantee 5 UAT test clients with pending invoices (GRO-290)
2026-03-30 13:40:15 +00:00
Paperclip f572e0a8f8 fix(ci): use valid GitHub Actions expression syntax for SHA
- Replace invalid ${{ github.sha::7 }} with ${{ github.sha }}
  and shell ${SHA::7} for substring extraction
- Add SHA env var to deploy-dev job

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 13:35:47 +00:00
Paperclip 1e80fa64e5 ci: trigger PR workflow via commit (GRO-290)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 13:33:53 +00:00
Flea Flicker 7759328283 chore: trigger CI pipeline
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 13:33:53 +00:00
groombook-ci[bot] d1e1206e40 chore: retrigger CI for PR review 2026-03-30 13:33:53 +00:00
groombook-ci[bot] f58f7fed5a ci: trigger CI run for PR 176 2026-03-30 13:33:53 +00:00
groombook-ci[bot] b06314efe2 fix(db): guarantee 5 UAT test clients with pending invoices (GRO-290)
Before: ~5% probabilistic pending invoices meant UAT couldn't reliably
find billing test data. Shedward was blocked from testing Pay Now flows.

After: deterministic 5 UAT clients (uat-alpha through uat-echo) each get
a completed appointment + pending invoice on every seed run. Client
names and emails documented in Shedward AGENTS.md for direct access.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 13:33:53 +00:00
groombook-ci[bot] 72ecd83d9e ci: trigger build 2026-03-30 13:33:10 +00:00
groombook-ci[bot] 0e1c36a407 feat(staff): super user grant/revoke UI + last-super-user guardrail (GRO-206)
Backend:
- PATCH /api/staff/:id now accepts optional isSuperUser field
- Only super users can change isSuperUser (403 otherwise)
- Revoke (isSuperUser=false) blocked if target is last super user (400)
- Deactivate (active=false) blocked if target is last super user (400)
- DELETE /:id blocked if target is last super user (400)
- New GET /api/staff/me returns current authenticated staff record

Frontend (Staff.tsx):
- Super User column in staff table with badge indicator
- Grant/Revoke SU button visible only to super users
- Last-super-user guardrail disables revoke button with tooltip
- API errors shown inline below table header

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 13:33:10 +00:00
groombook-ci[bot] db21947323 fix(ci): include GitHub SHA in image tag to prevent stale cache reuse
Each CI build now produces an immutable tag (pr-N-sha7 or
YYYY.MM.DD-sha7) so that docker/build-push-action cache-from
type=gha cannot cross-contaminate between commits.

Previously the shared pr-N tag caused GHA layer cache to reuse
stale JS bundles from earlier builds of the same PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:33:10 +00:00
groombook-ceo[bot] ca84ccc5e8 ci: add workflow_dispatch trigger for manual CI runs (GRO-293)
ci: add workflow_dispatch trigger for manual CI runs
2026-03-30 13:12:23 +00:00
groombook-ceo[bot] 753e3f38cb Merge branch 'main' into fix/ci-workflow-dispatch 2026-03-30 13:05:20 +00:00
groombook-ceo[bot] ca437088a4 fix(web): portal header fixes (GRO-286) + password/retry fixes (GRO-287)
fix(web): portal header fixes (GRO-286) + password/retry fixes (GRO-287)
2026-03-30 13:04:46 +00:00
Barkley Trimsworth bf1b93aead ci: add workflow_dispatch trigger for manual CI runs
GitHub App token pushes do not trigger pull_request workflow events,
blocking CI on bot-authored PRs. Add workflow_dispatch to allow manual
CI runs via: gh workflow run ci.yml --ref <branch>

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 12:37:13 +00:00
Flea Flicker fe9f1f8f78 fix(GRO-286): remove unused useCallback import and eslint-disable
- ReportCards.tsx: remove unused useCallback from imports
- AccountSettings.tsx: remove unused eslint-disable-next-line directive

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 11:24:17 +00:00
Barkley Trimsworth 5aad2da55a fix(web): add X-Impersonation-Session-Id header to portal API calls
This commit also includes GRO-287 fixes:
- PasswordChange: add stateful form with password-match validation
- ReportCards: replace window.location.reload() with refetch via useRef

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 11:04:03 +00:00
groombook-engineer[bot] 8437dc43dc fix: show Pay Now button during impersonation
Remove readOnly guard from Pay Now button and PaymentModal in BillingPayments.
The readOnly guard was too broad — it hid the Pay Now button during staff
impersonation sessions, making it impossible for staff to collect payments.

Other readOnly guards (Remove payment method, Autopay toggle) remain intact.

Co-authored-by: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-03-30 10:56:21 +00:00
groombook-engineer[bot] 73ce16ee74 fix: billing portal session header and response format mismatch (#168)
Fixes GRO-261 — billing portal session header mismatch and response format bug.

- x-session-id → X-Impersonation-Session-Id in BillingPayments.tsx and Dashboard.tsx
- Handle bare array response from /api/portal/invoices

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 01:54:11 +00:00
groombook-engineer[bot] 753080ecc4 fix: show login page before needsSetup guard for unauthenticated users (#166)
cc @cpfarhood

Unauthenticated users saw a blank screen because the needsSetup null-guard
fired before the LoginPage render check. needsSetup stays null for
unauthenticated users since the setup-check effect early-returns when
!session. Now the login check runs first so users see the login page.

Co-authored-by: Flea Flicker <flea-flicker@groombook.io>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Scrubs McBarkley (CEO) <ceo-bot@groombook.farh.net>
2026-03-29 20:58:02 +00:00
groombook-engineer[bot] 6cd2ea6ca9 fix(portal): wire Pay Now button with payment modal (GRO-261)
Closes GRO-261 — Pay Now button on Billing page now opens a payment modal with invoice selection and simulated payment flow.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 20:24:56 +00:00
groombook-engineer[bot] 4dabb25ee1 fix(portal/book): wire Rebook Now button + date format validation (GRO-265, GRO-266)
* fix(portal): wire Rebook Now button to navigate to booking wizard (GRO-265)

The "Rebook Now" button on the Report Card detail view had no click
handler. Now navigates to /admin/book with pet info pre-filled via URL
params (petName, serviceName). Button text changed from "Book Now" to
"Rebook Now" per the bug report.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(book): pre-fill form from URL params to ensure React state is set

Add useSearchParams to read URL parameters (e.g., ?clientName=Jane)
and sync them to the BookingBody state on mount via useEffect.
This ensures validation checks React state, not empty initial state.

Fixes GRO-255

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(book): add inline validation for date input format (GRO-266)

Date picker now shows a clear error when the value doesn't match
YYYY-MM-DD, instead of silently failing with a browser console warning.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(portal): wire Rebook Now button + clean .js artifacts (GRO-265)

Cherry-picked from contaminated PR #160:
- ReportCards.tsx: Rebook Now button navigates to /admin/book with pet info
- Book.tsx: pre-fill form from URL params (GRO-255)
- Book.tsx: inline date validation (GRO-266)

Also removes compiled .js artifacts (Book.js, ReportCards.js)
that were incorrectly committed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: groombook-ci[bot] <ci@groombook.bot>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-03-29 15:14:44 +00:00
groombook-ceo[bot] 20920022a6 fix: increase deployment rollout timeout to 300s (GRO-147) (#148)
Squash merge. CTO + QA approved, all CI checks green.

- Helm progressDeadlineSeconds: 120s → 300s (api + web)
- CI kubectl rollout timeout: 120s → 300s

Fixes groombook-dev CI deploy step timing out while pods complete successfully.

cc @cpfarhood
2026-03-29 14:07:21 +00:00
groombook-ceo[bot] 6565710091 fix(web): set VITE_API_URL= empty for production builds
Merges PR #158 — fixes critical production login bug.

- Adds apps/web/.env.production with VITE_API_URL= (empty)
- Prevents localhost:3000 from being baked into the prod bundle
- Auth client now uses relative URLs through the gateway

GRO-258 | QA: Lint Roller ✓ | CTO: The Dogfather ✓

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 13:08:59 +00:00
groombook-ceo[bot] fe9d39b004 Merge branch 'main' into fix/gro-258-vite-api-url 2026-03-29 13:02:13 +00:00
groombook-engineer[bot] b09606f5f0 ci: add production promotion workflow
Manual workflow_dispatch trigger to promote a tested image tag
to production by creating an infra PR. No auto-merge — UAT sign-off
required before prod deploy.

Co-authored-by: groombook-ci[bot] <ci@groombook.bot>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: groombook-ceo[bot] <269735724+groombook-ceo[bot]@users.noreply.github.com>
2026-03-29 12:36:08 +00:00
Flea Flicker 9e01d37087 fix(web): set VITE_API_URL= empty for production builds
Prevents localhost:3000 from being baked into the production bundle.
Vite automatically loads .env.production for prod builds, which
with VITE_API_URL= explicitly sets the var to empty string so
auth-client.ts uses relative URLs (?? "" fallback).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 10:24:21 +00:00
groombook-engineer[bot] 8de0a00a2b ci: update cd job to target dev overlay (#156)
Squash merge. All checks green, CTO + QA approved.

cc @cpfarhood
2026-03-29 09:46:37 +00:00
groombook-ceo[bot] 0a6d2911f5 fix(db): make seed script idempotent (GRO-179)
Merges seed script upsert changes. All inserts converted to onConflictDoUpdate for staff, services, clients, pets tables. CTO + QA approved.
2026-03-29 08:11:33 +00:00
groombook-ceo[bot] 0506385157 Merge branch 'main' into fix/make-seed-idempotent-gro-179 2026-03-29 08:05:14 +00:00
groombook-engineer[bot] 4746a63292 feat(portal): replace mock data with real session-driven API calls (#152)
Closes GRO-205. Reviewed and approved by CTO (The Dogfather) and QA (Lint Roller). cc @cpfarhood
2026-03-29 07:08:35 +00:00
groombook-ci[bot] eb48d97ee3 fix(db): make seed script idempotent using upserts
Convert raw inserts to upserts (ON CONFLICT DO UPDATE) for:
- staff: upsert on email (unique constraint)
- services: upsert on id (deterministic UUID)
- clients: upsert on email (unique constraint)
- pets: upsert on id (deterministic UUID)

This fixes the duplicate key violation when re-running the seed
script against an existing database (e.g., after schema migrations
or test restarts).

Note: appointments, invoices, visit logs still use raw inserts
and would need DELETE-before-insert for full idempotency. Those
tables use deterministic UUIDs so a second seed run without
prior DELETE would still fail. This is scoped to the immediate
staff email constraint violation reported.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 02:23:02 +00:00
groombook-engineer[bot] 3834e45b66 feat: add cd job to update groombook/infra image tags on main merge (GRO-178) (#147)
* feat: add cd job to update groombook/infra image tags on main merge (GRO-178)

- Adds `cd` job that runs after `docker` on main branch pushes only
- Uses tibdex/github-app-token to get infra repo push token
- Updates image tags in apps/groombook/base/{api,web,migrate-job,seed-job}.yaml
- Opens auto-merge PR on groombook/infra

Trade-off: deploy-dev continues using kubectl set image directly for PR
previews (speed over full GitOps auditability for short-lived previews).

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: correct --head branch format and use --enable-auto-merge (GRO-178)

CTO review fixes:
- Remove bogus "groombook-engineer[bot]:" prefix from --head — gh pr
  create does not use owner:branch syntax when pushing from a cloned
  repo; just the branch name is needed
- Replace invalid --auto-merges-branch=main flag with
  --enable-auto-merge (valid gh flag that activates repo auto-merge)

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: broaden annotation sed pattern, fix PR body link, remove error swallowing (GRO-178)

CTO review remaining fixes:
- Annotation sed pattern: broaden [a-f0-9]* to [a-zA-Z0-9-]* since
  migrate-job and seed-job use "groXXX" suffixes (e.g. "2026.03.28-gro177")
  which contain non-hex letters
- PR body link: fix /d50d9792/issues/GRO-178 → /GRO/issues/GRO-178
- Remove error swallowing: "|| echo" was hiding PR creation failures;
  let the step fail naturally so CI catches it

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(cd): split --enable-auto-merge into separate merge command

CTO review fix: gh pr create does not support --enable-auto-merge flag.
Split into two commands: create PR, then gh pr merge with --auto --merge.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: groombook-engineer[bot] <3141748+groombook-engineer[bot]@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Flea Flicker <flea-flicker@groombook.io>
Co-authored-by: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com>
2026-03-28 23:19:29 +00:00
groombook-engineer[bot] 6872342d8f fix(auth): resolve redirect loop and mount Better-Auth as sub-app (#144)
## Changes
- Replace toNodeHandler with auth.handler(c.req.raw) sub-app mount for Hono compatibility
- Add /api/auth/ path skip in authMiddleware and resolveStaffMiddleware
- Add OIDC_INTERNAL_BASE env var for split-horizon (hairpin NAT) URL resolution
- Replace render-time signIn.social() with LoginPage component (fixes redirect loop)
- Change auth-client baseURL to relative (empty string) for deployed environments
- Add POST /api/portal/appointments/:id/reschedule endpoint with session auth
- Add RescheduleFlow modal, PetForm component, and wire Dashboard/Appointments UI

## CTO Note
Auth fix is P0-critical. Portal mock data (UAT blocker) predates this PR and is tracked separately in GRO-218.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-28 22:10:50 +00:00
groombook-engineer[bot] 3a31ad71c2 feat(schema): add is_super_user to staff table (GRO-201)
Add boolean is_super_user column (default false) to staff table.
Update Staff interface in shared types.
Mark first manager as super user in both seed modes.
Update test fixtures to include isSuperUser field.

Co-authored-by: groombook-ci[bot] <ci@groombook.bot>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 20:39:46 +00:00
groombook-cto[bot] f1b85bf294 fix(portal): disable non-functional stub buttons in customer portal (#142)
All CI checks pass. Verified on groombook.dev.farh.net. Second approval from groombook-ceo[bot] per GRO-171.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-28 08:24:32 +00:00
groombook-engineer[bot] ad1f32eb8f feat(auth): replace OIDC/jose with Better-Auth (#136)
* feat(db): add Better-Auth schema tables (GRO-118)

Add user, session, account, and verification tables required by
Better-Auth's Drizzle adapter. Add nullable userId FK on staff to
link business identity to auth identity. Fix test fixtures and
factory to include the new column.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(api): mount Better-Auth handler at /api/auth/** (GRO-118)

- Import toNodeHandler from better-auth/node and auth from ./lib/auth.js
- Mount Better-Auth HTTP handler before auth middleware block
- Handles OAuth callbacks, sign-in/sign-out, session management
- Supports GET/POST/PUT/PATCH/DELETE/OPTIONS methods

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(api): replace JWT auth with Better-Auth session validation (GRO-118)

- Replace jose/jwtVerify with auth.api.getSession()
- Session token validated via cookie/header, DB-backed
- jwtPayload.sub now = Better-Auth user ID (not OIDC sub)
- Dev mode bypass preserved; production guard against AUTH_DISABLED preserved
- rbac.ts and tests updated in subsequent tasks

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(api): update resolveStaffMiddleware for Better-Auth userId (GRO-118)

- Remove JwtPayload import; use inline type in AppEnv
- Production and dev mode lookups now use staff.userId (not oidcSub)
- Backward compat: jwtPayload.sub now = Better-Auth user ID

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* chore(api): remove jose and openid-client deps (GRO-118)

- Remove unused jose and openid-client packages
- Regenerate pnpm lockfile
- Pre-existing Zod type errors resolved (1 remaining: JwtPayload in test)

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(api): remove stale JwtPayload import from impersonation test (GRO-118)

auth.ts no longer exports JwtPayload — replace with inline type.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* test(api): update RBAC tests for Better-Auth userId (GRO-128)

- Add userId field to mock staff records (MANAGER, RECEPTIONIST, GROOMER)
- Update jwtPayload.sub to use userId instead of oidcSub in test helpers
- Update dev mode X-Dev-User-Id header to use userId

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* chore(api): upgrade zod to v4 with v3 compat layer (GRO-131)

- Bump zod from ^3.24.1 to ^4.3.6
- Bump @hono/zod-validator from ^0.4.3 to ^0.7.6
- Update all 12 route files to import from "zod/v3" compat layer

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(api): add Better-Auth configuration (GRO-118)

Exports the better-auth() instance configured with:
- Drizzle PG adapter
- genericOAuth plugin for Authentik OIDC
- 7-day session with 5-min cookie cache

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(web): install Better-Auth client and create config (GRO-118)

- Add better-auth to apps/web/package.json dependencies
- Create apps/web/src/lib/auth-client.ts with createAuthClient config
- Export signIn, signOut, useSession from the client
- Add vite-env.d.ts for Vite client types

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(web): use Better-Auth session state in App.tsx (GRO-126)

Add useSession hook to check Better-Auth session for production auth.
Redirect to Authentik sign-in when no session in production mode.
Dev mode flow (DevLoginSelector) preserved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(web): scope devFetch interceptor to dev mode only (GRO-127)

* fix(api): validate BETTER_AUTH_SECRET and fix lockfile specifier (GRO-118)

- Add startup validation for BETTER_AUTH_SECRET when auth is enabled
- Fix pnpm-lock.yaml typescript specifier mismatch (^5.9.3 → ^5.7.3)

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(web): mock authDisabled=true in App.test.tsx to fix CI failures

App.test.tsx "App navigation" tests were failing because the beforeEach
set authDisabled=false (production mode), which triggers the Better Auth
useSession() path. Since useSession() was not mocked in tests, the
component rendered null instead of the admin nav.

Now uses authDisabled=true + dev user in localStorage for those tests,
bypassing the Better Auth dependency while still testing the nav render.

Also removes duplicate App.test.js (compiled artifact).

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): set authDisabled=true in fixtures to bypass Better Auth

The App.tsx production auth path calls signIn.social() when
authDisabled=false, causing E2E tests to render blank. The fixtures
must mock authDisabled=true so the dev login selector is used instead.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): add dev/config, dev/users, and branding mocks to navigation.spec.ts

Playwright matches routes in last-registered-first-served order, so the
catch-all /api/** handler was overwriting the authDisabled: true fixture.
Added specific handlers before the catch-all to ensure auth config,
user list, and branding responses are properly shaped.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(web): gate DevLoginSelector on API authDisabled, not import.meta.env.DEV

Move the DevLoginSelector rendering check from import.meta.env.DEV to the
API-driven authDisabled state, after the loading guard. Simplify the redirect
condition to remove the now-redundant pathname exception.

Fixes E2E login tests that were failing because DevLoginSelector was never
rendered in Docker production builds where import.meta.env.DEV is false.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(db): add missing migration journal entries 0012-0017

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(web): import App.tsx (not App.js) in App.test.tsx (#137)

* fix(web): mock /api/auth/get-session in Dev login selector test

The "redirects to /login when auth is disabled and no user selected" test
fails because useSession() from better-auth/react calls /api/auth/get-session
which wasn't mocked, causing sessionLoading to stay true indefinitely.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(web): import App.tsx (not App.js) in test to get authDisabled bypass

The Dev login selector test was importing the compiled App.js instead of
the source App.tsx. App.js has different logic (uses import.meta.env.DEV
instead of API-based authDisabled) and doesn't implement the
sessionLoading bypass needed for tests to pass.

Also applied the rawSession/rawSessionLoading refactor in App.tsx that
bypasses useSession result when authDisabled=true.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(web): use extensionless import for App in test

The `.tsx` extension in the import path is not allowed without
`allowImportingTsExtensions` (TS5097). Use extensionless `../App`
which resolves correctly via moduleResolution: "bundler".

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>

* fix(auth): dev login resolve staff by id, not userId

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(rbac): fallback lookup for staff records predating Better-Auth userId (#140)

GRO-153: /api/staff returned 403 for all staff because resolveStaffMiddleware
looked up by staff.userId (Better-Auth ID) but dev login sent staff.id (PK),
and existing staff records had userId=NULL.

Changes:
- resolveStaffMiddleware: try userId first, fall back to staff.id (dev mode)
- resolveStaffMiddleware: try userId first, fall back to oidcSub (production)
- GET /api/dev/users: include userId field for DevLoginSelector
- DevLoginSelector: send userId (not staff.id) as X-Dev-User-Id
- Migration 0018: backfill userId for known demo staff

Co-authored-by: groombook-engineer[bot] <groombook-engineer@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Barkley Trimsworth <barkley@groombook.farh.net>

* fix(rbac): allow all staff roles to READ /api/staff

GRO-156 follow-up: RBAC middleware was blocking groomer/receptionist
from GET /api/staff. The QA review found 403 with "role groomer is not
permitted" after PR #140 deployment.

Fix: split the /staff/* guard — GET requests allow all roles
(groomer, receptionist, manager); write operations remain manager-only.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com>
Co-authored-by: Flea Flicker <flea-flicker@paperclip.ing>
Co-authored-by: groombook-engineer[bot] <groombook-engineer@users.noreply.github.com>
Co-authored-by: Barkley Trimsworth <barkley@groombook.farh.net>
2026-03-28 03:50:45 +00:00