Public booking page + iframe embed (#17)

Adds the customer-facing /book/<slug> page: service/slot picker, booking
form, and confirmation screen, built on #16's existing booking JSON API.
Includes iframe auto-fit height reporting (mirroring
deploy/booking/booking_layout.js's eaBookingHeight message), brand-color
theming via a ?color= query param, a honeypot field with a fake-success
response indistinguishable from a real booking, and a clear "just taken"
message on slot-conflict. Caddy per-IP rate limiting is documented in
deploy/booking/RATE_LIMIT.md for manual application (no Caddyfile is
tracked in this repo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 15:15:29 +02:00
parent 2f6e0c1459
commit b895663c3a
7 changed files with 556 additions and 5 deletions
+13
View File
@@ -117,6 +117,19 @@ def slots():
@bp.post("")
def create_booking():
body = request.get_json(force=True, silent=True) or {}
if (body.get("website") or "").strip():
# Honeypot field: real customers never see or fill it (hidden from
# sighted users and screen readers alike), so a filled value means a
# scripted bot filled every field it could find. Fake a normal-looking
# success instead of a 4xx so a scripted client has no signal it was
# caught -- no booking is created, but the id/token are shaped exactly
# like a real create_booking() response (same id format, same client_id
# in the token's claims) so nothing about this response is
# distinguishable from a genuine one by a client inspecting it.
fake_booking_id = bdb.new_id("BK")
return jsonify({"booking_id": fake_booking_id, "status": "confirmed",
"token": _mint_manage_token(body.get("client_id") or "",
fake_booking_id)}), 201
client_id = body.get("client_id")
resource_id = body.get("resource_id")
service_id = body.get("service_id")