gemini (kostenintensives) app-aufbauen

This commit is contained in:
br committed 2026-08-01 16:42:30 +02:00
1 parent c74bca1e13
commit 8232f4d8f7
11 files changed
+659 -519

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
# ==============================================================================
# Formeleon - Multi-tenant Form Generator
# Author: Björn Runden <br@runden.net>
# Module: services/auth_service.py
# Description: Business logic for handling user authentication and session management.
# ==============================================================================
import uuid
import logging
from typing import Dict, Any, Tuple, Optional
from flask import current_app, session
from models import db, User, UserRole, Tenant, AuditLog
logger = logging.getLogger(__name__)
class AuthService:
"""Service layer for authentication and user management."""
@staticmethod
def is_dev_mode() -> bool:
"""Check if application is in development mode."""
return current_app.config.get('DEV_MODE', False)
@staticmethod
def ensure_dev_user_exists() -> User:
"""
Ensure the development user exists in the database.
:return: User object from database
"""
config = current_app.config
user_id = config.get('DEV_USER_ID', 'user_dev_admin_001')
user = User.query.get(user_id)
if not user:
role = UserRole.ADMIN if config.get('DEV_USER_ROLE', 'admin') == 'admin' else UserRole.USER
user = User(
id=user_id,
email=config.get('DEV_USER_EMAIL', 'dev@formeleon.local'),
name=config.get('DEV_USER_NAME', 'Dev Admin'),
role=role,
sub=f'dev-{uuid.uuid4()}'
)
db.session.add(user)
db.session.commit()
logger.info(f'Created dev user: {user.id}')
return user
@staticmethod
def ensure_dev_tenant_exists() -> Tenant:
"""
Ensure the development tenant exists in the database.
:return: Tenant object from database
"""
config = current_app.config
tenant_id = config.get('DEV_TENANT_ID', 'tenant_dev_123')
tenant = Tenant.query.get(tenant_id)
if not tenant:
tenant = Tenant(
id=tenant_id,
name='Development Tenant',
public_key=f'dev-key-{uuid.uuid4().hex[:16]}',
allowed_origins='*'
)
db.session.add(tenant)
db.session.commit()
logger.info(f'Created dev tenant: {tenant.id}')
return tenant
@staticmethod
def create_dev_user_session() -> dict:
"""
Create a development mode user session with fake Zitadel user.
:return: Dictionary with user session data
"""
config = current_app.config
# Use configured dev user settings or defaults
dev_user = {
'id': config.get('DEV_USER_ID', 'user_dev_admin_001'),
'email': config.get('DEV_USER_EMAIL', 'dev@formeleon.local'),
'name': config.get('DEV_USER_NAME', 'Dev Admin'),
'role': config.get('DEV_USER_ROLE', 'admin'),
'sub': f'dev-{uuid.uuid4()}',
'tenant_id': config.get('DEV_TENANT_ID', 'tenant_dev_123'),
'dev_mode': True,
'authenticated': True
}
return dev_user
@staticmethod
def process_oidc_callback(user_info: dict) -> Tuple[Optional[User], Optional[str]]:
"""
Process user information from OIDC callback and create/update user.
:param user_info: User information dictionary from OIDC provider
:return: Tuple of (User object or None, error message or None)
"""
sub = user_info.get('sub')
email = user_info.get('email')
name = user_info.get('name', user_info.get('preferred_username', 'Unknown'))
if not email:
return None, 'No email in user info'
# Create or update user
user = User.query.filter_by(sub=sub).first()
if not user:
user_id = f'user_{email.split("@")[0]}_{str(uuid.uuid4())[:8]}'
user = User(
id=user_id,
email=email,
name=name,
role=UserRole.USER, # Default role for new OIDC users
sub=sub
)
db.session.add(user)
db.session.commit()
logger.info(f'Created new user from OIDC: {user.id}')
else:
# Update existing user
user.name = name
user.email = email
db.session.commit()
return user, None
+121
View File
@@ -0,0 +1,121 @@
# ==============================================================================
# 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
+59
View File
@@ -0,0 +1,59 @@
# ==============================================================================
# Formeleon - Multi-tenant Form Generator
# Author: Björn Runden <br@runden.net>
# Module: services/tenant_service.py
# Description: Business logic for tenant management and validation.
# ==============================================================================
from typing import Optional
from models import Tenant
class TenantService:
"""Service layer for handling tenant-related business logic."""
@staticmethod
def get_tenant_by_public_key(public_key: str) -> Optional[Tenant]:
"""
Get tenant by public key.
:param public_key: Public API key
:return: Tenant object or None
"""
if not public_key:
return None
return Tenant.query.filter_by(public_key=public_key).first()
@staticmethod
def validate_origin(origin: str, tenant: Tenant) -> bool:
"""
Validate that the request origin matches allowed origins for the tenant.
:param origin: Request origin/referer
:param tenant: Tenant object to check against
:return: True if origin is allowed, False otherwise
"""
if not origin:
return False
# Remove trailing slash and protocol
origin = origin.rstrip('/')
allowed_origins = tenant.get_allowed_origins_list()
# Check for wildcard
if '*' in allowed_origins:
return True
# Exact match
if origin in allowed_origins:
return True
# Check if origin matches any allowed domain (with subdomains)
for allowed in allowed_origins:
if allowed.startswith('*'):
# Wildcard subdomain match
domain = allowed[1:] # Remove leading *
if origin.endswith(domain):
return True
return False