"""App-Factory für die CheckPoint-Ehrenamt-App. Baut die Flask-App zusammen, initialisiert die Datenbank (SQLite in instance/) und registriert die Blueprints. Mandantenfähigkeit wird über das Team-Scoping (app/team_scope.py) vorbereitet – die echte Logik folgt in späteren Sessions. """ import os from datetime import timezone from zoneinfo import ZoneInfo from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy import event from sqlalchemy.engine import Engine _BERLIN = ZoneInfo("Europe/Berlin") @event.listens_for(Engine, "connect") def _sqlite_pragmas(dbapi_connection, connection_record): """Nur für SQLite: WAL + busy_timeout machen parallele gunicorn-Worker verträglich (statt „database is locked"), Fremdschlüssel werden erzwungen. Bei anderen Engines (z. B. späteres PostgreSQL) ein No-op.""" import sqlite3 if isinstance(dbapi_connection, sqlite3.Connection): cur = dbapi_connection.cursor() cur.execute("PRAGMA journal_mode=WAL") cur.execute("PRAGMA busy_timeout=5000") cur.execute("PRAGMA foreign_keys=ON") cur.close() def _lokalzeit(dt, fmt="%d.%m.%Y %H:%M"): """Jinja-Filter: UTC-Zeitstempel (in SQLite naiv) → Europe/Berlin formatiert.""" if dt is None: return "" if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(_BERLIN).strftime(fmt) # Eine zentrale DB-Instanz, die die Modelle (Session 2) importieren. db = SQLAlchemy() def create_app(test_config=None): # instance_relative_config: Pfade wie die SQLite-DB liegen in instance/. app = Flask(__name__, instance_relative_config=True) # CP_ENV=production schaltet die Härtung scharf (sichere Cookies, SECRET_KEY Pflicht). ist_produktion = os.environ.get("CP_ENV") == "production" secret = os.environ.get("SECRET_KEY") if ist_produktion and not secret: raise RuntimeError( "SECRET_KEY muss in Produktion gesetzt sein (CP_ENV=production)." ) app.config.from_mapping( # In Produktion über Umgebungsvariable setzen, nie hart im Code. SECRET_KEY=secret or "dev-only-change-me", SQLALCHEMY_DATABASE_URI="sqlite:///" + os.path.join(app.instance_path, "checkpoint.sqlite"), SQLALCHEMY_TRACK_MODIFICATIONS=False, # Dokument-Uploads (Session 10): außerhalb von static/, max. 10 MB. UPLOAD_FOLDER=os.path.join(app.instance_path, "uploads"), MAX_CONTENT_LENGTH=10 * 1024 * 1024, # Cookie-Härtung. Secure nur in Produktion (sonst bricht http-Login lokal). SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax", SESSION_COOKIE_SECURE=ist_produktion, ) if test_config is not None: app.config.from_mapping(test_config) # instance/ + Upload-Ordner sicherstellen (für DB + Dokumente). os.makedirs(app.instance_path, exist_ok=True) os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True) # Hinter nginx: X-Forwarded-Proto/-For/-Host auswerten (korrektes https-Schema). if ist_produktion: from werkzeug.middleware.proxy_fix import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) db.init_app(app) # CSRF-Schutz für alle POST-Formulare. from .csrf import init_csrf init_csrf(app) # Lokalzeit-Filter (einheitliche Anzeige in Europe/Berlin). app.jinja_env.filters["lokalzeit"] = _lokalzeit # Modelle importieren, damit sie bei create_all() bekannt sind. # (Noch leer – die Tabellen kommen in Session 2.) from . import models # noqa: F401 with app.app_context(): db.create_all() # Blueprints registrieren. from .routes import main_bp from .routes.planung import bp as planung_bp from .routes.dienste import bp as dienste_bp from .routes.dokumente import bp as dokumente_bp from .routes.chat import bp as chat_bp from .routes.profil import bp as profil_bp from .auth import bp as auth_bp, register_cli app.register_blueprint(main_bp) app.register_blueprint(auth_bp) app.register_blueprint(planung_bp) app.register_blueprint(dienste_bp) app.register_blueprint(dokumente_bp) app.register_blueprint(chat_bp) app.register_blueprint(profil_bp) # CLI-Befehle (z. B. flask create-admin). register_cli(app) return app