135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
"""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/<int:contact_id>", 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
|