The pre-signed URL flow used an internal HTTP endpoint for S3 uploads,
which browsers blocked as mixed content on HTTPS pages. Instead of
generating a pre-signed URL that the browser uploads to directly,
the new /logo/upload endpoint receives the file via multipart POST
and streams it to S3 from the API server using the internal endpoint.
This resolves the mixed content error that was blocking logo uploads
on dev.groombook.dev.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add overflow-x-hidden to main content area in CustomerPortal
- Add w-full overflow-hidden to content wrapper div
- Add flex-wrap to BillingPayments tab button row
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Extend updateInvoiceSchema to accept optional tipSplits array in PATCH body
- Validate tip splits sum to 100% (10000 bps) when marking paid with tipCents > 0
- Return 422 if tipSplits not provided and no existing splits in DB
- Save tip splits atomically in same DB transaction as invoice status update
- Update frontend markPaid() to send tipSplits in PATCH body instead of separate POST
- Remove non-atomic POST /tip-splits call from markPaid flow
Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
QA found test assertion failures - tests were asserting the old (incorrect)
Authorization: Bearer header instead of the correct X-Impersonation-Session-Id.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace Authorization: Bearer with X-Impersonation-Session-Id in all 5
mutation handlers in Appointments.tsx (confirm, cancel, save-notes,
reschedule, booking). The portal backend validates X-Impersonation-Session-Id
header, not Authorization Bearer.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Exclude image/svg+xml from the frontend allowlist since SVG poses greater
XSS risk due to its ability to contain scripts, even with proper Content-Type
validation. The server-side validation (commit 8182870) still accepts SVG
and validates magic bytes, but the frontend restrict to safer bitmap formats
as specified in the issue.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add ALLOWED_LOGO_TYPES allowlist check before constructing data URL from
user-controlled logoBase64 and logoMimeType fields. Only MIME types that
the API explicitly accepts (image/png, image/jpeg, image/gif, image/webp,
image/svg+xml) can be rendered as data URLs.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Defensive validation in /api/branding ensures base64-encoded logo content
matches its declared MIME type by checking image magic bytes (PNG, JPEG,
GIF, WebP). If the content doesn't match, the legacy base64 fields are
nulled out before returning to prevent MIME type confusion attacks.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Use Drizzle's inArray() instead of raw sql template with = ANY()
to avoid PostgreSQL array binding issues in the reminder scheduler
bulk sent-check query.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add pre-submit validation in markPaid() that checks tip split percentages
sum to 100% before allowing the payment to be processed. This addresses
Finding #7 from the frontend code quality review (GRO-628).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
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>