Files
mivanchenko c3e520aabb
Test backoffice (smb-crm) / test (push) Successful in 1m43s
Rate-limit public bookings per contact; add direct owner contact endpoint
Public booking API now rejects a 6th active booking from the same
customer_contact within 24h (429), stopping one contact from filling
every slot on every resource, while owner-entered manual bookings stay
unaffected.

Add POST /api/contact: client sites can reach their own owner's inbox
directly (via their existing login email) for general inquiries,
separate from the agency's leads/Telegram pipeline (n8n/lead-intake.json),
which stays reserved for actual prospects contacting the agency itself.
Paris Barber Shop's contact form and Rückruf widget now point here; the
Rückruf floating widget itself has been removed from the site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 04:47:06 +02:00

57 lines
2.4 KiB
Python

"""Public "contact the owner" API: a client's own site (a top-level page on
its own domain, e.g. barbershop.mivanchenko.de -- not an iframe of this app,
unlike the booking widget) posts here directly, cross-origin. See
contact_mail.py for why this is a separate path from n8n/lead-intake.json.
"""
from flask import Blueprint, jsonify, request
import booking_db as bdb
import contact_mail
bp = Blueprint("contact_api", __name__, url_prefix="/api/contact")
@bp.after_request
def _add_cors_headers(resp):
# Client sites live on their own domains (barbershop.mivanchenko.de, a
# future client's own domain, ...), never this app's own origin -- a
# fixed allowlist would mean editing this file for every new client, so
# this mirrors n8n/lead-intake.json's existing allowedOrigins: "*" for
# the same public, unauthenticated, abuse-limited-by-content form.
# Runs on every response from this blueprint, including Flask's
# automatic OPTIONS response to the browser's CORS preflight (a JSON
# POST isn't a CORS-simple request) -- no separate OPTIONS route needed.
resp.headers["Access-Control-Allow-Origin"] = "*"
resp.headers["Access-Control-Allow-Methods"] = "POST, OPTIONS"
resp.headers["Access-Control-Allow-Headers"] = "Content-Type"
return resp
@bp.post("")
def create_contact():
body = request.get_json(force=True, silent=True) or {}
if (body.get("website") or "").strip():
# Honeypot field, same convention as booking_api.py's create_booking:
# real visitors never see or fill it, so a filled value means a bot.
# Fake a normal-looking success so a scripted client has no signal.
return jsonify({"sent": True}), 201
client_id = body.get("client_id")
name = (body.get("name") or "").strip()
contact = (body.get("contact") or "").strip()
if not (client_id and name and contact):
return jsonify({"error": "client_id, name, contact are required"}), 400
if bdb.get_client(client_id) is None:
return jsonify({"error": "not found"}), 404
payload = {
"name": name,
"contact": contact,
"service_interest": (body.get("service_interest") or body.get("service") or "").strip(),
"message": (body.get("message") or "").strip(),
}
if not contact_mail.notify_owner_of_contact(client_id, payload):
return jsonify({"error": "not found"}), 404
return jsonify({"sent": True}), 201