Updates playwright baseURL to the canonical dev.groombook.dev FQDN
per canonical infra targets.
Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Move hostname validation to run AFTER OIDC_INTERNAL_BASE replacement
(was checking raw discovery URLs before replacement caused false positives)
- Only validate authorizationUrl hostname against issuer; token/userinfo
are server-to-server and may legitimately use internal hostnames
- Infra: add OIDC_INTERNAL_BASE env var to dev overlay (was missing, matches UAT)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add SMS opt-in fields to clients schema (smsOptIn, smsConsentDate, smsOptOutDate, smsConsentText)
- Add channel column to reminderLogs with per-channel idempotency
- Create SMS service with Telnyx SDK integration and E.164 validation
- Update reminders service to conditionally send SMS to opted-in clients
- Add TCPA opt-out text to SMS reminders
- Graceful degradation: catch SMS errors without blocking email
- Fix: use clients.phone instead of non-existent clients.phoneE164
- Update clients route to expose SMS fields in API
- Add telnyx dependency to API package
- Create database migration 0028_sms_reminders
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* Fix invoice status transitions, tip-split validation, refund idempotency, and tip-split response format
- Add ALLOWED_TRANSITIONS state machine for invoice status changes (GRO-637)
- Replace floating-point tip-split validation with integer basis-points math
- Add idempotency key support to refund endpoint with new refunds table
- Return full invoice shape from POST /:id/tip-splits matching GET response
- All existing tests pass
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(invoices): wrap refund flow in transaction for idempotency safety
- Wrap idempotency check + processRefund() + db.insert() in db.transaction()
- This prevents duplicate Stripe refunds if the DB insert fails after Stripe processes the refund
- Add migration 0027_refunds for the refunds table (was missing)
- Removes out-of-scope changes from PR #278 (csrf.ts, appointmentGroups, appointments, book, groomingLogs, services, stripe-webhooks)
Fixes GRO-637 per CTO review
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(api): wire up CSRF middleware for protected routes
Register csrfMiddleware in the protected API routes after authMiddleware
and resolveStaffMiddleware to protect against CSRF attacks on state-
changing operations (POST, PUT, PATCH, DELETE).
Addresses CTO review feedback on PR #278.
* fix(api): remove CSRF middleware that breaks POST/PUT/PATCH/DELETE
The CSRF middleware requires x-csrf-token header but the frontend never
sends it, which would break all mutating operations with 403 errors.
CSRF protection should be implemented in a separate coordinated PR with
frontend changes.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Flea Flicker <flea-flicker@groombook.ai>
Auto-link staff records by email when userId is NULL on first authenticated request.
Resolves GRO-667 UAT 403 blocker.
Co-Authored-By: Flea Flicker <noreply@anthropic.com>
Adds Zod validation across 5 API routes:
1. invoices GET / — query param validation (uuid, enum, int bounds)
2. book POST / — future-time refinement on startTime
3. appointments — recurrence series capped at 1 year
4. services — durationMinutes capped at 480 (8 hours)
5. stripe-webhooks — UUID validation on invoice IDs before DB lookup
Closes GRO-636
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add SQL-level LIMIT/OFFSET pagination to churn risk query
- Add separate COUNT(*) subquery for total without fetching all rows
- Accept page and limit query params with sensible defaults and bounds
- Return page, limit, and churnRiskTotal in response
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Replace SELECT-then-UPDATE with atomic UPDATE ... WHERE token=? AND status='pending' RETURNING *
to prevent confirmation token replay attacks (TOCTOU race condition)
- Fix requireRoleOrSuperUser() error message: swap the conditional branches so
'Forbidden: super user privileges required' is returned when user lacks role,
and 'Forbidden: role X is not permitted' when user is not superuser
- Add 'and' mock export to confirmation.test.ts and rbac.test.ts for new query patterns
- Update test expectations to match corrected error message semantics
Prevents ENOENT crash in migrate and seed jobs.
Root cause: corepack tries to mkdir /home/node/.cache/node/corepack/v1
but the directory does not exist in the builder stage. This was a
regression in c438f57 where the cache directory was not pre-created.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- appointmentGroups: Hono<AppEnv>() + groomer isolation on all 5 endpoints
- groomingLogs: Hono<AppEnv>() + groomer isolation on GET, POST, DELETE with appointmentId preserved
- appointments: batherStaffId conflict checks in POST and PATCH handlers
- Non-groomer roles retain full access
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add X-Content-Type-Options, X-Frame-Options, Referrer-Policy, X-XSS-Protection,
and Permissions-Policy headers to server block and static assets location.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(GRO-566): add SKIP_OOBE env var to bypass setup wizard
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* Add rate_limit table migration for Better Auth (GRO-574)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-574): switch rate limit to memory storage to unblock UAT
Better Auth rate_limit table migration exists on branch but hasn't
been deployed to UAT. Switching to memory storage bypasses the
missing table entirely, restoring auth functionality immediately.
Memory storage is per-instance (not shared) — rate limiting still
functions but won't be distributed across pods. This is acceptable
for UAT while the migration is being promoted through the pipeline.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: groombook-qa[bot] <269744346+groombook-qa[bot]@users.noreply.github.com>
Adds the missing rate_limit table that Better Auth v1.5.6 requires when rateLimit.storage is set to 'database'. Without this table, all auth endpoints return HTTP 500.
Also includes GRO-566: SKIP_OOBE env var to bypass setup wizard in dev/test.
cc @cpfarhood
- Add flexShrink:0 to logo div to prevent shrinking
- Wrap Book + NAV_LINKS in scrollable div with overflow-x:auto, flex:1, minWidth:0
- Add flexShrink:0 to all nav links
- Move logout button outside scrollable div with flexShrink:0 instead of marginLeft:auto
- Keeps logout button always visible regardless of nav item count
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- apps/web: upgrade better-auth from ^1.0.0 to ^1.5.6 (matches API)
- apps/web/vite.config.ts: exclude /api/auth/* from service worker caching
- apps/api/index.ts: return 503 when auth not configured
- apps/api/middleware/auth.ts: return 503 when auth not initialized
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The OAuth callback was failing with "please_restart_the_process" because
Better-Auth's default DB-backed state (verification table) was unreliable —
the UAT hourly reset wipes all tables including verification records. Switch
to cookie-based state storage so the encrypted state survives in the browser
cookie across the redirect flow.
Also removes explicit redirectURI from socialProviders (Better-Auth derives
it from baseURL) and adds visible error feedback on the login page when
OAuth callbacks fail.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The networkidle wait causes flakiness in CI due to slow external resource loading.
Use domcontentloaded which fires earlier and is sufficient for SPA navigation checks.
Co-authored-by: Pawla Abdul (Bot) <pawla@groombook.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
fix(e2e): add paginated mock for /api/invoices in navigation.spec.ts
Fixes GRO-557. The generic E2E API mock returned [] for /api/invoices, but the InvoicesPage component expects { data: [], total: 0 }. This crashed React and prevented the page from rendering, causing the admin invoices test to fail consistently.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Generated diverse set of professional pet photos covering:
- Large breeds: German Shepherds (3), Golden Retrievers (2), Labradors (1)
- Medium breeds: Beagle, Cocker Spaniel, Boxer, Bulldog, Corgi, Dachshund, English Springer Spaniel, Husky
- Small breeds: Maltese, Shih Tzu, Pomeranian, Poodle, Pug, Yorkshire Terrier
- Mixed breeds: 4 variations
Total demo pet images: 55 (11MB)
Puggle-specific: 4 images for the 250+ seeded Puggles
This maximizes the MiniMax image generation quota to provide a rich,
diverse visual library for the grooming demo site.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Explicitly set redirectURI in social provider configs to ensure
Better-Auth uses the correct callback URL for OAuth providers.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Generated 13 new diverse dog images using MiniMax (Afghan Hound, Basset Hound, Bichon Frise variants, Boxer, Cavalier, Cocker Spaniel variants, Corgi, Dachshund variants, Pomeranian variants, Schnauzer variants, Setter, Sheepdog)
- Updated seed script to include all 28 dog images in demoPetImages array
- Ensures wider variety of dog breeds and grooming styles in demo seed data
- All images are photorealistic and suitable for pet grooming demo site
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- auth.ts: add google/github social providers from better-auth/social-providers
- auth.ts: add getActiveProviders() to enumerate configured OAuth/social providers
- index.ts: add /api/auth/providers public endpoint for frontend
- App.tsx: update LoginPage to show Google/GitHub buttons based on /api/auth/providers response
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Replace loadAll() with single GET /api/invoices?limit=50&offset=0
- Remove parallel fetches of clients/appointments/services/staff from list load
- Use clientName from API response instead of client-side enrichment
- Add offset-based pagination controls with Previous/Next buttons
- Lazy-load staff/appointments only when opening invoice detail modal
- Lazy-load clients/appointments/services only when opening create form
- Filter changes only re-fetch invoices, not all endpoints
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add ALLOW_RESET env var override to reset.ts safety guard
- Add reset Docker build target to Dockerfile
- Add reset image build step to CI docker job
- Add reset image tag update to CD job dev overlay update
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add database migration 0024 with indexes on invoices, invoice_line_items, and invoice_tip_splits
- Update Drizzle schema with index definitions for sync
- Add pagination (limit/offset) to GET /api/invoices with max 200 limit
- Add LEFT JOIN to include clientName in invoice list response
- Return { data: [...], total: N } response shape for pagination
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When SetupWizard completes POST /api/setup and navigates to /admin,
App.tsx still has needsSetup=true in React state, causing an immediate
redirect back to /setup. Pass onSetupComplete callback to SetupWizard
which clears the state before navigating, breaking the loop.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Fix type errors that caused CI Lint & Typecheck job to fail:
- setup.ts: replace unavailable isNull import with sql template tag
(isNull not exported from @groombook/db; sql IS exported)
- setup.ts: add non-null assertion on newStaff after insert.returning()
- setup.test.ts: add sql mock template tag to @groombook/db mock
- setup.test.ts: fix evaluateCond to handle sql template tag type
- setup.test.ts: add type assertions for body.staff in OOBE regression tests
- setup.test.ts: fix dbStaffRows type casts in mock insert function
All 18 tests pass, full typecheck clean.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Exempt POST /api/setup from resolveStaffMiddleware so OOBE users (with no
pre-existing staff record) can complete the out-of-box experience without
getting blocked by the "no staff record found" 403 error.
Changes:
- rbac.ts: add /api/setup to path exemption alongside /api/auth/
- setup.ts POST /: add find-or-create logic that:
- Looks up existing staff by userId from JWT
- Auto-links legacy staff records by email if userId is null
- Creates a new staff record if none exists (OOBE case)
- Returns 400 if JWT has no email and no staff record found
- setup.test.ts: add regression tests for all scenarios
Fixes GRO-485 (OOBE regression introduced by GRO-480).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
drizzle-orm is not a direct dependency of @groombook/api, causing
TS2307 at typecheck time. Re-export isNull from @groombook/db and
update the import in rbac.ts.
Co-Authored-By: Paperclip <noreply@paperclip.ing>