# ============================================================================== # Formeleon - Multi-tenant Form Generator # Author: Björn Runden # Module: routes/api.py # Description: Public API Blueprint for handling form handshakes and submissions. # ============================================================================== import logging from flask import Blueprint, request, jsonify, current_app from models import db, Tenant, FormConfig, Submission, SubmissionStatus from services.mailer import MailerService from services.recaptcha import get_recaptcha_service import uuid logger = logging.getLogger(__name__) # Create public API blueprint api_bp = Blueprint('api', __name__, url_prefix='/api/v1') def _get_tenant_from_auth(): """Helper to validate Bearer token and return Tenant.""" auth_header = request.headers.get('Authorization', '') if not auth_header.startswith('Bearer '): return None public_key = auth_header.replace('Bearer ', '').strip() tenant = Tenant.query.filter_by(public_key=public_key).first() return tenant @api_bp.route('/forms/', methods=['GET']) def get_form_config(form_id): """ Handshake Endpoint. Client snippet requests form configuration using its public key. """ tenant = _get_tenant_from_auth() if not tenant: return jsonify({'error': 'Unauthorized. Invalid public key.'}), 401 if not tenant.is_active(): return jsonify({'error': 'Tenant is inactive.'}), 403 # Check origin (CORS / allowed domains) origin = request.headers.get('Origin') if origin and not tenant.is_origin_allowed(origin): logger.warning(f"Origin rejected: {origin} for tenant {tenant.id}") return jsonify({'error': 'Origin not allowed'}), 403 form = FormConfig.query.filter_by(id=form_id, tenant_id=tenant.id).first() if not form: return jsonify({'error': 'Form not found or access denied.'}), 404 if not form.is_active: return jsonify({'error': 'Form is inactive.'}), 403 recaptcha_service = get_recaptcha_service() return jsonify({ 'id': form.id, 'name': form.name, 'description': form.description, 'form_structure': form.form_structure, 'recaptcha': { 'enabled': recaptcha_service.is_enabled() and not recaptcha_service.is_dev_mode(), 'site_key': recaptcha_service.get_site_key() } }), 200 @api_bp.route('/submissions', methods=['POST']) def submit_form(): """ Submission Endpoint. Receives JSON payload, validates reCAPTCHA (mocked in dev), and saves to DB. """ tenant = _get_tenant_from_auth() if not tenant: return jsonify({'error': 'Unauthorized. Invalid public key.'}), 401 data = request.get_json() if not data: return jsonify({'error': 'Invalid JSON payload.'}), 400 form_id = data.get('form_id') payload = data.get('payload') recaptcha_token = data.get('recaptcha_token') if not form_id or not payload: return jsonify({'error': 'Missing form_id or payload.'}), 400 form = FormConfig.query.filter_by(id=form_id, tenant_id=tenant.id).first() if not form: return jsonify({'error': 'Form not found.'}), 404 # --- reCAPTCHA Validation --- recaptcha_service = get_recaptcha_service() # Get client IP for additional reCAPTCHA validation client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) if client_ip and ',' in client_ip: client_ip = client_ip.split(',')[0].strip() recaptcha_passed, recaptcha_score = recaptcha_service.verify(recaptcha_token, client_ip) status = SubmissionStatus.PENDING if recaptcha_passed else SubmissionStatus.SPAM # Save Submission sub_id = f"sub_{uuid.uuid4().hex[:12]}" new_submission = Submission( id=sub_id, form_config_id=form.id, tenant_id=tenant.id, payload=payload, recaptcha_score=recaptcha_score, recaptcha_passed=recaptcha_passed, status=status, user_agent=request.headers.get('User-Agent'), referer=request.headers.get('Referer') ) db.session.add(new_submission) db.session.commit() # --- Trigger Email Notifications (if not spam) --- if recaptcha_passed: # Check Tenant Level Permissions allow_instant_mail = tenant.config.get('features', {}).get('instant_notifications', True) # Default to true for now allow_autoresponder = tenant.config.get('features', {}).get('submitter_autoresponder', True) if form.settings: # 1. Admin Notification notif_settings = form.settings.get('notifications', {}) if allow_instant_mail and notif_settings.get('enabled'): send_to = notif_settings.get('send_to', []) if send_to: # In a production app, you might want to queue this with Celery instead of blocking the request try: MailerService.send_admin_notification(form.name, send_to, payload) except Exception as e: logger.error(f"Failed to send admin notification: {e}") # 2. Submitter Autoresponder auto_settings = form.settings.get('autoresponder', {}) if allow_autoresponder and auto_settings.get('enabled'): target_field = auto_settings.get('email_field_name') target_email = payload.get(target_field) if target_field else None if target_email and '@' in str(target_email): try: MailerService.send_autoresponder( to_address=str(target_email), subject_template=auto_settings.get('subject', 'Thank you!'), body_template=auto_settings.get('body_template', ''), payload=payload ) except Exception as e: logger.error(f"Failed to send autoresponder to {target_email}: {e}") if not recaptcha_passed: return jsonify({'error': 'Spam detected. Submission rejected.'}), 400 return jsonify({ 'message': 'Submission received successfully', 'submission_id': sub_id }), 201