"""Contact submission routes.""" from __future__ import annotations import logging from flask import Blueprint, jsonify, request from .. import auth, settings from ..database import delete_contact, get_contacts from ..rate_limit import allow_request from ..services.contact import persist_submission, send_notification, validate_submission bp = Blueprint("contact", __name__, url_prefix="/api") @bp.route("/contact", methods=["POST"]) def receive_contact(): payload = request.form or request.get_json(silent=True) or {} if settings.STRICT_ORIGIN_CHECK: origin = request.headers.get("Origin") referer = request.headers.get("Referer") allowed = settings.ALLOWED_ORIGIN if allowed: if origin and origin != allowed and not (referer and referer.startswith(allowed)): logging.warning( "Origin/Referer mismatch (origin=%s, referer=%s)", origin, referer) return jsonify({"status": "error", "message": "Invalid request origin."}), 403 else: logging.warning( "STRICT_ORIGIN_CHECK enabled but ALLOWED_ORIGIN not set; skipping enforcement") client_ip_source = request.headers.get( "X-Forwarded-For", request.remote_addr or "unknown") client_ip = client_ip_source.split( ",")[0].strip() if client_ip_source else "unknown" if not allow_request(client_ip): logging.warning("Rate limit reached for %s", client_ip) return ( jsonify( {"status": "error", "message": "Too many submissions, please try later."}), 429, ) submission, errors = validate_submission(payload) if errors: return jsonify({"status": "error", "errors": errors}), 400 assert submission is not None try: record_id = persist_submission(submission) except Exception as exc: # pragma: no cover - logged for diagnostics logging.exception("Failed to persist submission: %s", exc) return ( jsonify({"status": "error", "message": "Could not store submission."}), 500, ) email_sent = send_notification(submission) status = 201 if email_sent else 202 body = { "status": "ok", "id": record_id, "email": "sent" if email_sent else "pending", } if not email_sent: body["message"] = "Submission stored but email dispatch is not configured." return jsonify(body), status @bp.route("/contact", methods=["GET"]) @auth.login_required def get_submissions(): """Retrieve contact form submissions with pagination, filtering, and sorting.""" try: # Parse query parameters page = int(request.args.get("page", 1)) per_page = min(int(request.args.get("per_page", 50)), 100) # Max 100 per page sort_by = request.args.get("sort_by", "created_at") sort_order = request.args.get("sort_order", "desc") email_filter = request.args.get("email") date_from = request.args.get("date_from") date_to = request.args.get("date_to") # Validate sort_by valid_sort_fields = ["id", "name", "email", "created_at"] if sort_by not in valid_sort_fields: sort_by = "created_at" # Get submissions submissions, total = get_contacts( page=page, per_page=per_page, sort_by=sort_by, sort_order=sort_order, email_filter=email_filter, date_from=date_from, date_to=date_to, ) return jsonify({ "status": "ok", "submissions": submissions, "pagination": { "page": page, "per_page": per_page, "total": total, "pages": (total + per_page - 1) // per_page, }, }) except Exception as exc: logging.exception("Failed to retrieve submissions: %s", exc) return jsonify({"status": "error", "message": "Failed to retrieve submissions."}), 500 @bp.route("/contact/", methods=["DELETE"]) @auth.login_required def delete_submission(contact_id: int): """Delete a contact submission by ID.""" try: deleted = delete_contact(contact_id) if not deleted: return jsonify({"status": "error", "message": "Submission not found."}), 404 return jsonify({"status": "ok", "message": "Submission deleted successfully."}) except Exception as exc: logging.exception("Failed to delete submission: %s", exc) return jsonify({"status": "error", "message": "Failed to delete submission."}), 500