124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
"""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
|