Files
zwitschi 4cefd4e3ab
Some checks failed
CI / test (3.11) (push) Failing after 5m36s
CI / build-image (push) Has been skipped
v1
2025-10-22 16:48:55 +02:00

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