feat(api): add contact form endpoint for website enquiries
Add POST /api/v1/contact endpoint to handle contact form submissions from the marketing website. Includes: - ContactSchema for validation with HTML sanitization - Honeypot field for spam protection - Rate limiting (5 per hour) - Email notification to info@secuird.tech via NotificationService
This commit is contained in:
@@ -5,7 +5,9 @@ from flask import Blueprint
|
||||
api_v1_bp = Blueprint("api_v1", __name__)
|
||||
|
||||
# Import route modules to register them
|
||||
from gatehouse_app.api.v1 import auth, users, organizations, policies, external_auth, departments, principals, ssh, zerotier, sudo, oidc
|
||||
from gatehouse_app.api.v1 import auth, users, organizations, policies, external_auth, departments, principals, ssh, zerotier, sudo, oidc, contact
|
||||
from gatehouse_app.api.v1 import superadmin
|
||||
|
||||
api_v1_bp.register_blueprint(ssh.ssh_bp)
|
||||
api_v1_bp.register_blueprint(superadmin.superadmin_bp)
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Contact form endpoint for website enquiries."""
|
||||
import logging
|
||||
|
||||
from flask import request, current_app
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from gatehouse_app.api.v1 import api_v1_bp
|
||||
from gatehouse_app.extensions import limiter
|
||||
from gatehouse_app.utils.response import api_response
|
||||
from gatehouse_app.schemas.contact_schema import ContactSchema
|
||||
from gatehouse_app.services.notification_service import NotificationService
|
||||
from gatehouse_app.services.email_templates import build_contact_enquiry_html
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Hardcoded destination for all contact submissions
|
||||
CONTACT_DESTINATION = "info@secuird.tech"
|
||||
|
||||
|
||||
@api_v1_bp.route("/contact", methods=["POST"])
|
||||
@limiter.limit("5 per hour")
|
||||
def contact():
|
||||
"""Handle contact form submissions from the marketing website.
|
||||
|
||||
Accepts: email, name, company, enquiry_type, message, interest_area, _hp.
|
||||
Sends an email to info@secuird.tech with the enquiry details.
|
||||
Silently discards submissions where the honeypot field (_hp) is filled.
|
||||
"""
|
||||
try:
|
||||
schema = ContactSchema()
|
||||
data = schema.load(request.get_json() or {})
|
||||
except ValidationError as err:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Invalid request data",
|
||||
status=400,
|
||||
error_type="VALIDATION_ERROR",
|
||||
error_details=err.messages,
|
||||
)
|
||||
|
||||
# Honeypot check — silently succeed without sending
|
||||
if data.get("_hp"):
|
||||
logger.info(f"[Contact] Honeypot triggered, ip={request.remote_addr}")
|
||||
return api_response(message="Thank you for your message!")
|
||||
|
||||
enquiry_type = data.get("enquiry_type") or "general"
|
||||
email = data.get("email") or ""
|
||||
|
||||
# Build and send email
|
||||
html_body = build_contact_enquiry_html(
|
||||
enquiry_type=enquiry_type,
|
||||
submitter_email=email,
|
||||
name=data.get("name"),
|
||||
company=data.get("company"),
|
||||
interest_area=data.get("interest_area"),
|
||||
message=data.get("message"),
|
||||
)
|
||||
|
||||
NotificationService._send_email_async(
|
||||
to_address=CONTACT_DESTINATION,
|
||||
subject=f"Secuird Website: {enquiry_type.replace('_', ' ').title()} from {email}",
|
||||
body=f"New contact enquiry ({enquiry_type}) from {email}",
|
||||
html_body=html_body,
|
||||
)
|
||||
|
||||
logger.info(f"[Contact] enquiry_type={enquiry_type} ip={request.remote_addr}")
|
||||
|
||||
return api_response(message="Thank you for your message!")
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Contact form validation schemas."""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from marshmallow import Schema, fields, validate, validates_schema, ValidationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContactSchema(Schema):
|
||||
"""Schema for contact form submissions."""
|
||||
|
||||
email = fields.Email(required=True)
|
||||
name = fields.Str(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
validate=validate.Length(max=255),
|
||||
)
|
||||
company = fields.Str(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
validate=validate.Length(max=255),
|
||||
)
|
||||
enquiry_type = fields.Str(
|
||||
required=True,
|
||||
validate=validate.OneOf(["demo_request", "sales_enquiry", "general", "support"]),
|
||||
)
|
||||
message = fields.Str(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
validate=validate.Length(max=2000),
|
||||
)
|
||||
interest_area = fields.Str(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
validate=validate.Length(max=100),
|
||||
)
|
||||
_hp = fields.Str(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
load_from="_hp",
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def sanitize_html(self, data, **kwargs):
|
||||
"""Strip HTML tags from all text fields to prevent XSS."""
|
||||
text_fields = ["name", "company", "message", "interest_area"]
|
||||
for field in text_fields:
|
||||
value = data.get(field)
|
||||
if value and isinstance(value, str):
|
||||
data[field] = re.sub(r"<[^>]*>", "", value)
|
||||
@@ -496,3 +496,69 @@ def build_email_verification_resend_html(
|
||||
{get_alert_box("If you didn't request this, you can safely ignore this email.", "info", "🔒")}
|
||||
'''
|
||||
return get_base_html(content, "Verify your Secuird email address", "Please verify your email address")
|
||||
|
||||
|
||||
def build_contact_enquiry_html(
|
||||
enquiry_type: str,
|
||||
submitter_email: str,
|
||||
name: Optional[str],
|
||||
company: Optional[str],
|
||||
interest_area: Optional[str],
|
||||
message: Optional[str],
|
||||
) -> str:
|
||||
"""Build a contact enquiry notification email.
|
||||
|
||||
Args:
|
||||
enquiry_type: One of demo_request, sales_enquiry, general, support
|
||||
submitter_email: Email address of the person submitting the enquiry
|
||||
name: Full name of the submitter (optional)
|
||||
company: Company name (optional)
|
||||
interest_area: Area of interest (optional)
|
||||
message: Free-text message (optional)
|
||||
|
||||
Returns:
|
||||
HTML email string
|
||||
"""
|
||||
# Map enquiry types to display labels and colors
|
||||
type_labels = {
|
||||
"demo_request": ("Demo Request", "info"),
|
||||
"sales_enquiry": ("Sales Enquiry", "success"),
|
||||
"general": ("General Enquiry", "info"),
|
||||
"support": ("Support Request", "warning"),
|
||||
}
|
||||
type_label, alert_type = type_labels.get(enquiry_type, ("Enquiry", "info"))
|
||||
|
||||
name_display = name if name else "Not provided"
|
||||
company_display = company if company else "Not provided"
|
||||
interest_display = interest_area if interest_area else "Not provided"
|
||||
message_display = message if message else "No message provided"
|
||||
|
||||
# Build details table
|
||||
details_rows = f"""
|
||||
{get_detail_row("Enquiry Type", type_label)}
|
||||
{get_detail_row("Submitter Email", submitter_email)}
|
||||
{get_detail_row("Name", name_display)}
|
||||
{get_detail_row("Company", company_display)}
|
||||
{get_detail_row("Interest Area", interest_display)}
|
||||
"""
|
||||
|
||||
content = f'''
|
||||
<h2 style="margin: 0 0 20px 0; color: {TEXT_COLOR}; font-size: 20px; font-weight: 600;">New {type_label}</h2>
|
||||
<p style="margin: 0 0 20px 0; color: {TEXT_COLOR}; font-size: 15px; line-height: 1.6;">
|
||||
A new {type_label.lower()} has been submitted through the Secuird website.
|
||||
</p>
|
||||
{get_alert_box(f"Enquiry type: <strong>{type_label}</strong>", alert_type, "📬")}
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin: 20px 0; background-color: {BACKGROUND_COLOR}; border-radius: 8px;">
|
||||
<tr>
|
||||
<td style="padding: 20px;">
|
||||
<h3 style="margin: 0 0 16px 0; color: {TEXT_COLOR}; font-size: 14px; font-weight: 600;">Enquiry Details</h3>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
{details_rows}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3 style="margin: 20px 0 12px 0; color: {TEXT_COLOR}; font-size: 14px; font-weight: 600;">Message</h3>
|
||||
<p style="margin: 0; color: {TEXT_COLOR}; font-size: 14px; line-height: 1.6; white-space: pre-wrap;">{message_display}</p>
|
||||
'''
|
||||
return get_base_html(content, f"Secuird Website: {type_label}", f"New {type_label} from {submitter_email}")
|
||||
|
||||
Reference in New Issue
Block a user