122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
# ==============================================================================
|
|
# Formeleon - Multi-tenant Form Generator
|
|
# Author: Björn Runden <br@runden.net>
|
|
# 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
|