diff --git a/gatehouse_app/api/v1/__init__.py b/gatehouse_app/api/v1/__init__.py index c14e63f..7a45c53 100644 --- a/gatehouse_app/api/v1/__init__.py +++ b/gatehouse_app/api/v1/__init__.py @@ -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) diff --git a/gatehouse_app/api/v1/contact.py b/gatehouse_app/api/v1/contact.py new file mode 100644 index 0000000..313dfcb --- /dev/null +++ b/gatehouse_app/api/v1/contact.py @@ -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!") diff --git a/gatehouse_app/schemas/contact_schema.py b/gatehouse_app/schemas/contact_schema.py new file mode 100644 index 0000000..44df861 --- /dev/null +++ b/gatehouse_app/schemas/contact_schema.py @@ -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) diff --git a/gatehouse_app/services/email_templates.py b/gatehouse_app/services/email_templates.py index c4ede14..ec4d81d 100644 --- a/gatehouse_app/services/email_templates.py +++ b/gatehouse_app/services/email_templates.py @@ -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''' +

New {type_label}

+

+ A new {type_label.lower()} has been submitted through the Secuird website. +

+ {get_alert_box(f"Enquiry type: {type_label}", alert_type, "📬")} + + + + +
+

Enquiry Details

+ + {details_rows} +
+
+

Message

+

{message_display}

+ ''' + return get_base_html(content, f"Secuird Website: {type_label}", f"New {type_label} from {submitter_email}")