Skip to content

Require email verification before a booking is finalized - #2

Open
jeremymanning wants to merge 3 commits into
mainfrom
ccr-18709472-ht5pud
Open

jeremymanning wants to merge 3 commits into
mainfrom
ccr-18709472-ht5pud

Conversation

@jeremymanning

Copy link
Copy Markdown
Member

Closes #1.

Summary

Non-legitimate bookings were landing straight on the calendar. They had throwaway names and addresses and keyboard-mash notes, and were often booked then cancelled within a minute. Each one also made the owner's account send an invite to an arbitrary address.

  • createBooking now stores a pending row and emails the visitor a link to confirm.html?token=…. There is no calendar event, invite or owner email until they click Confirm booking.
  • confirmBooking re-checks availability under the lock, then creates the event and invite and sends the normal confirmation emails.
  • Pending rows don't hold the slot: the first visitor to verify wins.
  • Links expire after VERIFY_LINK_MINUTES (default 60). Setting REQUIRE_EMAIL_VERIFICATION=false restores immediate booking.
  • Confirming requires a click, so mail link-scanners can't verify on the visitor's behalf.
  • Fixes from a red-team pass:
    • createdAt is reset when a booking is confirmed, so the ghost guard can't open a double-booking.
    • The minimum-notice check now measures from the request time, not the confirm click.
    • A failed verification send, or an exhausted MailApp quota, now shows the visitor an error instead of telling them to check their email.
  • Also:
    • The cancel page now shows formatted dates instead of raw ISO strings, and its action buttons line up.
    • Reconcile tests no longer fail from hardcoded fixture dates that are now in the past.

Deploy order

  1. Merge this PR first; GitHub Pages then serves the new frontend. It is backward compatible: the old backend never returns pending, so visitors still see the current confirmation.
  2. Then push the backend: clasp push, and update the existing deployment so the web-app URL stays the same. Doing this before step 1 would make the old frontend show "Booking Confirmed!" for bookings that are only pending.

Testing

  • npm test: all backend files pass (17 new email-verification tests) and 78/78 frontend tests pass. Each red-team fix has a test that fails on the pre-fix code.
  • A Playwright run of the real frontend against the real .gs handlers (Google services stubbed) covered: submit → "check your email" → confirm page (load doesn't confirm) → click → confirmed → reload shows already confirmed. It also covered two visitors racing for the same slot, where the second sees "time no longer available". Screenshots were checked at desktop and 375px widths.
  • Not yet tested against the live Apps Script deployment.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SP8VHeDuMhdC6865MY5VfR


Generated by Claude Code

Fixtures used September 2026 dates; reconcile is scoped to future bookings,
so once those dates elapsed the rows were skipped as history and four tests
failed. Derive the fixture year from the current date instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP8VHeDuMhdC6865MY5VfR
Non-legitimate bookings (throwaway names/addresses, keyboard-mash notes,
often booked then cancelled within a minute) were landing directly on the
calendar, notifying the owner and making the owner's account send invites to
arbitrary addresses. They looked human, so the honeypot and rate limits
didn't catch them.

createBooking now stores a pending row and emails the visitor a confirm link
(confirm.html). Nothing reaches the calendar, the invitee or the owner until
the visitor clicks 'Confirm booking'; confirmBooking then re-checks
availability under the lock, creates the event and sends the normal
confirmation emails. Pending rows don't hold the slot (first to verify wins),
so unverified requests can't squat times. Links expire after
VERIFY_LINK_MINUTES (60); REQUIRE_EMAIL_VERIFICATION=false restores
immediate booking. Confirmation requires a click so mail link-scanners can't
verify on the visitor's behalf.

Also: share event creation / response shaping between create, confirm and
reschedule; extract the token-page message renderer (PageMessage); fix
cancel/confirm pages rendering raw ISO dates (TimezoneUtil was never
initialised there) and misaligned action buttons.

Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP8VHeDuMhdC6865MY5VfR
- A link confirmed more than GHOST_GRACE_MINUTES after the request produced a
  confirmed row whose createdAt was already stale, so its just-created (not
  yet visible) event read as deleted: a second visitor could confirm the same
  slot, and reconcile could cancel the live booking. createdAt is now reset
  when the event is created.
- isRangeAvailable measured min-notice from the confirm click, so near-limit
  slots failed at confirm with a false 'someone else booked it'. It now takes
  the request time (start must still be in the future).
- A failed verification send (or exhausted MailApp quota) still told the
  visitor to check their email. createBooking now returns EMAIL_UNAVAILABLE.
- Persist the confirmed row before sending confirmation emails, so nobody gets
  cancel/reschedule links for a row that failed to save.

Each fix has a regression test that fails on the previous commit.

Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP8VHeDuMhdC6865MY5VfR

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Require email verification before a booking is finalized (mitigate non-legitimate bookings)

2 participants