c3e520aabb
Test backoffice (smb-crm) / test (push) Successful in 1m43s
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>
57 lines
2.4 KiB
Python
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
|