# ============================================================================== # Formeleon - Multi-tenant Form Generator # Author: Björn Runden # Module: services/submission_service.py # Description: Business logic for processing and handling form submissions. # ============================================================================== import uuid import logging from typing import Dict, Any, Tuple from flask import current_app from werkzeug.exceptions import BadRequest, Unauthorized, Forbidden, NotFound from models import db, Tenant, FormConfig, Submission, SubmissionStatus from services.recaptcha import RecaptchaService from utils.helpers import hash_ip logger = logging.getLogger(__name__) class SubmissionService: """Service layer for handling form submissions.""" @staticmethod def process_submission(form_id: str, tenant: Tenant, origin: str, payload: dict, ip_address: str, user_agent: str, recaptcha_token: str = None, content_type: str = 'application/json') -> Tuple[Dict[str, Any], int]: """ Process a form submission with validation and reCAPTCHA checking. :param form_id: ID of the form being submitted :param tenant: Validated tenant object :param origin: Validated origin string :param payload: Submitted form data :param ip_address: Submitter IP address :param user_agent: Submitter user agent :param recaptcha_token: Optional reCAPTCHA token :param content_type: Request content type :return: Tuple of (response_dict, http_status_code) """ if not payload: raise BadRequest('No form data provided') # Get form form = FormConfig.query.filter_by( id=form_id, tenant_id=tenant.id ).first() if not form: raise NotFound('Form not found') if not form.is_active: raise Forbidden('Form is not active') # Validate reCAPTCHA if enabled recaptcha_score = None recaptcha_passed = None if form.settings.get('recaptcha_enabled', True) if form.settings else True: if recaptcha_token: recaptcha_service = RecaptchaService() is_valid, score = recaptcha_service.verify( recaptcha_token, ip_address ) recaptcha_score = score recaptcha_passed = is_valid if not is_valid: logger.warning(f'Submission rejected: reCAPTCHA failed with score {score}') else: # If no token provided, use dev mode score if in dev mode if current_app.config.get('DEV_MODE', False): recaptcha_score = current_app.config.get('MOCK_RECAPTCHA_SCORE', 0.9) recaptcha_passed = True # Create submission submission_id = f'sub_{form_id}_{str(uuid.uuid4())[:8]}' submission = Submission( id=submission_id, form_config_id=form.id, tenant_id=tenant.id, payload=payload, ip_hash=hash_ip(ip_address), user_agent=user_agent, referer=origin, recaptcha_score=recaptcha_score, recaptcha_passed=recaptcha_passed, submission_metadata={ 'origin': origin, 'user_agent': user_agent, 'content_type': content_type } ) # Set status based on reCAPTCHA if recaptcha_passed is False: submission.status = SubmissionStatus.SPAM else: submission.status = SubmissionStatus.PENDING db.session.add(submission) db.session.commit() logger.info(f'Form submission: {submission_id} for form {form_id}, tenant {tenant.id}') # Prepare response response = { 'status': 'success', 'submission_id': submission.id, 'message': form.settings.get('success_message', 'Form submitted successfully!') if form.settings else 'Form submitted successfully!' } # If reCAPTCHA failed, indicate in response if submission.status == SubmissionStatus.SPAM: response['status'] = 'spam' response['message'] = 'Submission flagged as spam' logger.warning(f'Spam submission: {submission_id}') return response, 200