mail drin
fehlende routen drin
prod-reife erreicht.
TODOS:
Deine aktuellen offenen TODOs
1. Rate-Limiting (Spam-Schutz VOR dem reCAPTCHA)
Das reCAPTCHA hält zwar Bots ab, aber wenn jemand böswillig ein Script schreibt, das 1.000 POST-Requests pro Sekunde an /api/v1/submissions sendet, muss dein Backend jedes Mal Google kontaktieren und die Datenbank befragen. Das frisst Server-Ressourcen.
Todo: Einbinden von Flask-Limiter und Absichern der API-Route (z.B. max. 10 Submissions pro Minute pro IP-Adresse oder Tenant).
2. Billing & Feature-Enforcement (Limits pro Tenant durchsetzen)
Die Mandanten-Architektur und das config-Feld im JSON existieren bereits. Aber wir erzwingen diese Limits aktuell nicht im Backend.
Todo: Im Dashboard (templates/admin/tenants/view.html) UI-Controls einbauen, um Feature-Flags für einen Tenant festzulegen:
max_forms (z.B. 1 für Free-User, 10 für Pro-User).
allow_instant_mail (Dürfen E-Mails an den Tenant gesendet werden?).
allow_autoresponder (Darf der Autoresponder für Endkunden genutzt werden?).
Todo: Diese Limits hart im Backend (beim Erstellen von Forms und beim Mailversand) kontrollieren. (Der Mailer prüft das zwar schon halbwegs, das UI zum Pflegen fehlt aber noch komplett).
3. E-Mail Templates aufwerten (HTML statt Plain-Text)
Bisher versendet der Service E-Mails als reinen Text (plain). Für eine professionelle SaaS-Lösung sollten diese Mails ansprechend aussehen.
Todo: Im services/mailer.py den MIMEMultipart so erweitern, dass er saubere HTML-Mails aus einem Jinja2-Template (templates/emails/...) generiert, inklusive Branding/Logo deines Services.
4. Frontend-Snippet "Graceful Degradation"
Das embed.js arbeitet aktuell sehr gut. Aber was ist, wenn das Formeleon-Backend mal für 5 Minuten down ist?
Todo: Einen sauberen Timeout im fetch einbauen und dem Endkunden auf der Webseite eine schöne, fallback HTML-Meldung anzeigen ("Entschuldigung, das Formular ist momentan nicht erreichbar"), anstatt dass sich das Script lautlos in der Console mit einem CORS/Network-Error verabschiedet.
5. CI/CD Pipeline (Deployment)
Wenn der Code fertig ist, willst du ihn sicher nicht immer händisch per git pull auf deinen Produktionsserver schieben und dort docker compose up tippen.
Todo: Eine simple Pipeline (Github Actions / Gitea Actions) anlegen, die den Code testet und automatisch auf dem Zielserver ausrollt.
219 lines
8.6 KiB
Python
219 lines
8.6 KiB
Python
# ==============================================================================
|
|
# Formeleon - Multi-tenant Form Generator
|
|
# Author: Björn Runden <br@runden.net>
|
|
# Module: config.py
|
|
# Description: Application configuration with environment variable support.
|
|
# Handles development mode, reCAPTCHA, Zitadel OIDC, and database settings.
|
|
# ==============================================================================
|
|
|
|
"""
|
|
Application configuration module for Formeleon.
|
|
|
|
This module centralizes all configuration settings for the Flask application,
|
|
loading from environment variables with sensible defaults for development.
|
|
|
|
Environment Variables:
|
|
- DEV_MODE: Enable development mode (default: False)
|
|
- SECRET_KEY: Flask secret key for session management
|
|
- SQLALCHEMY_DATABASE_URI: Database connection string
|
|
- SQLALCHEMY_TRACK_MODIFICATIONS: Track modifications (default: False)
|
|
|
|
- MOCK_AUTH: Enable mock authentication mode (default: False)
|
|
Requires DEV_MODE=true. When enabled, uses local mock users instead of Zitadel OIDC.
|
|
- MOCK_USER_ID: User ID for mock auth (default: 'mock_user_001')
|
|
- MOCK_USER_ROLE: Role for mock user (default: 'admin')
|
|
- MOCK_USER_NAME: Name for mock user (default: 'Mock Admin')
|
|
- MOCK_USER_EMAIL: Email for mock user (default: 'mock@formeleon.local')
|
|
- MOCK_TENANT_ID: Tenant ID for mock auth (default: 'mock_tenant_001')
|
|
|
|
- ZITADEL_ISSUER: OIDC issuer URL for Zitadel
|
|
- ZITADEL_CLIENT_ID: OIDC client ID
|
|
- ZITADEL_CLIENT_SECRET: OIDC client secret
|
|
- ZITADEL_REDIRECT_URI: OIDC redirect URI
|
|
|
|
- RECAPTCHA_SECRET_KEY: reCAPTCHA v3 secret key
|
|
- RECAPTCHA_SITE_KEY: reCAPTCHA v3 site key
|
|
- RECAPTCHA_THRESHOLD: Minimum score threshold (default: 0.5)
|
|
- MOCK_RECAPTCHA_SCORE: Mock score for dev mode (default: 0.9)
|
|
|
|
- DEV_USER_ROLE: Role for dev mode user (default: 'admin')
|
|
- DEV_TENANT_ID: Tenant ID for dev mode (default: 'tenant_dev_123')
|
|
- DEV_USER_NAME: Username for dev mode (default: 'Dev Admin')
|
|
- DEV_USER_EMAIL: Email for dev mode (default: 'dev@formeleon.local')
|
|
"""
|
|
|
|
import os
|
|
from typing import Optional
|
|
|
|
|
|
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
class Config:
|
|
"""
|
|
Base configuration class with default values.
|
|
|
|
All configuration values can be overridden via environment variables.
|
|
"""
|
|
|
|
# Flask Core Settings
|
|
SECRET_KEY: str = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
|
|
DEBUG: bool = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
|
|
TESTING: bool = os.environ.get('FLASK_TESTING', 'false').lower() == 'true'
|
|
|
|
# i18n / Localization Settings
|
|
LANGUAGES = ['en', 'de']
|
|
BABEL_DEFAULT_LOCALE = 'en'
|
|
BABEL_DEFAULT_TIMEZONE = 'UTC'
|
|
|
|
# Development Mode
|
|
DEV_MODE: bool = os.environ.get('DEV_MODE', 'false').lower() == 'true'
|
|
|
|
# Mock Authentication Mode
|
|
# When MOCK_AUTH=true and DEV_MODE=true, use local mock users instead of Zitadel OIDC
|
|
MOCK_AUTH: bool = os.environ.get('MOCK_AUTH', 'false').lower() == 'true'
|
|
|
|
# Dev User Settings
|
|
DEV_USER_ROLE: str = os.environ.get('DEV_USER_ROLE', 'admin')
|
|
DEV_TENANT_ID: str = os.environ.get('DEV_TENANT_ID', 'tenant_dev_123')
|
|
DEV_USER_NAME: str = os.environ.get('DEV_USER_NAME', 'Dev Admin')
|
|
DEV_USER_EMAIL: str = os.environ.get('DEV_USER_EMAIL', 'dev@formeleon.local')
|
|
|
|
# Mock User Settings (used when MOCK_AUTH is enabled)
|
|
MOCK_USER_ID: str = os.environ.get('MOCK_USER_ID', 'mock_user_001')
|
|
MOCK_USER_ROLE: str = os.environ.get('MOCK_USER_ROLE', 'admin')
|
|
MOCK_USER_NAME: str = os.environ.get('MOCK_USER_NAME', 'Mock Admin')
|
|
MOCK_USER_EMAIL: str = os.environ.get('MOCK_USER_EMAIL', 'mock@formeleon.local')
|
|
MOCK_TENANT_ID: str = os.environ.get('MOCK_TENANT_ID', 'mock_tenant_001')
|
|
|
|
# Database Configuration
|
|
if 'SQLALCHEMY_DATABASE_URI' in os.environ:
|
|
SQLALCHEMY_DATABASE_URI: str = os.environ['SQLALCHEMY_DATABASE_URI']
|
|
else:
|
|
SQLALCHEMY_DATABASE_URI: str = f'sqlite:///{os.path.join(BASE_DIR, "instance", "formeleon.db")}'
|
|
|
|
SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
|
|
SQLALCHEMY_ECHO: bool = os.environ.get('SQLALCHEMY_ECHO', 'false').lower() == 'true'
|
|
|
|
# Zitadel OIDC Configuration
|
|
ZITADEL_ISSUER: Optional[str] = os.environ.get('ZITADEL_ISSUER')
|
|
ZITADEL_CLIENT_ID: Optional[str] = os.environ.get('ZITADEL_CLIENT_ID')
|
|
ZITADEL_CLIENT_SECRET: Optional[str] = os.environ.get('ZITADEL_CLIENT_SECRET')
|
|
ZITADEL_REDIRECT_URI: Optional[str] = os.environ.get(
|
|
'ZITADEL_REDIRECT_URI',
|
|
'http://localhost:5000/auth/callback'
|
|
)
|
|
ZITADEL_SCOPES: list = ['openid', 'profile', 'email']
|
|
|
|
# reCAPTCHA v3 Configuration
|
|
RECAPTCHA_SECRET_KEY: Optional[str] = os.environ.get('RECAPTCHA_SECRET_KEY')
|
|
RECAPTCHA_SITE_KEY: Optional[str] = os.environ.get('RECAPTCHA_SITE_KEY')
|
|
RECAPTCHA_THRESHOLD: float = float(os.environ.get('RECAPTCHA_THRESHOLD', '0.5'))
|
|
MOCK_RECAPTCHA_SCORE: float = float(os.environ.get('MOCK_RECAPTCHA_SCORE', '0.9'))
|
|
RECAPTCHA_VERIFY_URL: str = 'https://www.google.com/recaptcha/api/siteverify'
|
|
|
|
# Application Settings
|
|
DEFAULT_PAGE_SIZE: int = 20
|
|
MAX_PAGE_SIZE: int = 100
|
|
|
|
# Form Submission Settings
|
|
ALLOWED_ORIGINS: list = os.environ.get('ALLOWED_ORIGINS', '*').split(',')
|
|
ENABLE_CORS: bool = os.environ.get('ENABLE_CORS', 'true').lower() == 'true'
|
|
|
|
# File Upload Settings
|
|
MAX_CONTENT_LENGTH: int = 16 * 1024 * 1024 # 16MB
|
|
UPLOAD_FOLDER: str = os.environ.get('UPLOAD_FOLDER', '/tmp/formeleon_uploads')
|
|
ALLOWED_EXTENSIONS: set = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'txt'}
|
|
|
|
# Session Configuration
|
|
SESSION_COOKIE_SECURE: bool = os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
|
SESSION_COOKIE_HTTPONLY: bool = True
|
|
SESSION_COOKIE_SAMESITE: str = os.environ.get('SESSION_COOKIE_SAMESITE', 'Lax')
|
|
PERMANENT_SESSION_LIFETIME: int = 3600 # 1 hour in seconds
|
|
|
|
# SMTP Settings
|
|
SMTP_SERVER: Optional[str] = os.environ.get('SMTP_SERVER')
|
|
SMTP_PORT: int = int(os.environ.get('SMTP_PORT', '587'))
|
|
SMTP_USERNAME: Optional[str] = os.environ.get('SMTP_USERNAME')
|
|
SMTP_PASSWORD: Optional[str] = os.environ.get('SMTP_PASSWORD')
|
|
SMTP_USE_TLS: bool = os.environ.get('SMTP_USE_TLS', 'true').lower() == 'true'
|
|
EMAIL_FROM: str = os.environ.get('EMAIL_FROM', 'noreply@formeleon.local')
|
|
|
|
def is_dev_mode(self) -> bool:
|
|
"""
|
|
Check if application is running in development mode.
|
|
|
|
:return: True if DEV_MODE is enabled, False otherwise.
|
|
"""
|
|
return self.DEV_MODE
|
|
|
|
def is_recaptcha_enabled(self) -> bool:
|
|
"""
|
|
Check if reCAPTCHA is properly configured.
|
|
|
|
:return: True if both secret and site keys are configured, False otherwise.
|
|
"""
|
|
return (self.RECAPTCHA_SECRET_KEY is not None and
|
|
self.RECAPTCHA_SITE_KEY is not None)
|
|
|
|
def is_zitadel_enabled(self) -> bool:
|
|
"""
|
|
Check if Zitadel OIDC is properly configured.
|
|
|
|
:return: True if all required Zitadel settings are present, False otherwise.
|
|
"""
|
|
return (self.ZITADEL_ISSUER is not None and
|
|
self.ZITADEL_CLIENT_ID is not None )
|
|
|
|
def is_mock_auth_enabled(self) -> bool:
|
|
"""
|
|
Check if mock authentication mode is enabled.
|
|
|
|
:return: True if both DEV_MODE and MOCK_AUTH are enabled, False otherwise.
|
|
"""
|
|
return self.DEV_MODE and self.MOCK_AUTH
|
|
|
|
|
|
class DevelopmentConfig(Config):
|
|
"""
|
|
Development-specific configuration.
|
|
|
|
Overrides base configuration with development-friendly settings.
|
|
"""
|
|
# DEBUG: bool = True
|
|
# DEV_MODE: bool = True
|
|
# SQLALCHEMY_ECHO: bool = True
|
|
# SESSION_COOKIE_SECURE: bool = False
|
|
|
|
|
|
class ProductionConfig(Config):
|
|
"""
|
|
Production-specific configuration.
|
|
|
|
Overrides base configuration with production-safe settings.
|
|
"""
|
|
DEBUG: bool = False
|
|
DEV_MODE: bool = False
|
|
SQLALCHEMY_ECHO: bool = False
|
|
SESSION_COOKIE_SECURE: bool = True
|
|
SESSION_COOKIE_SAMESITE: str = 'Strict'
|
|
|
|
|
|
class TestingConfig(Config):
|
|
"""
|
|
Testing-specific configuration.
|
|
|
|
Overrides base configuration with test-friendly settings.
|
|
"""
|
|
TESTING: bool = True
|
|
SQLALCHEMY_DATABASE_URI: str = 'sqlite:///:memory:'
|
|
WTF_CSRF_ENABLED: bool = False
|
|
|
|
|
|
# Configuration mapping for easy access
|
|
config_map: dict = {
|
|
'development': DevelopmentConfig,
|
|
'production': ProductionConfig,
|
|
'testing': TestingConfig,
|
|
'default': DevelopmentConfig if os.environ.get('FLASK_ENV', 'development') == 'development' else ProductionConfig
|
|
}
|