import os import logging from flask import Flask, jsonify, request from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager from flask_migrate import Migrate from flask_limiter import Limiter from flask_limiter.util import get_remote_address from flask_talisman import Talisman from werkzeug.middleware.proxy_fix import ProxyFix logger = logging.getLogger(__name__) db = SQLAlchemy() login_manager = LoginManager() migrate = Migrate() limiter = Limiter(key_func=get_remote_address, default_limits=[]) def _make_limiter(redis_url: str) -> Limiter: """ Maak een nieuwe Limiter instantie met de correcte storage_uri. In flask-limiter 3.x hoort storage_uri in __init__, NIET in init_app(). """ return Limiter( key_func=get_remote_address, default_limits=[], storage_uri=redis_url, strategy='moving-window', ) def create_app(): app = Flask(__name__, template_folder='templates', static_folder='static') # ── Config ──────────────────────────────────────────────────────────────── app.config['SECRET_KEY'] = os.environ['SECRET_KEY'] app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DATABASE_URL'] app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['BASE_URL'] = os.environ.get('BASE_URL', 'http://localhost') app.config['ORG_NAME'] = os.environ.get('ORG_NAME', 'GO! Scholengroep') # OAuth2 app.config['MICROSOFT_CLIENT_ID'] = os.environ.get('MICROSOFT_CLIENT_ID') app.config['MICROSOFT_CLIENT_SECRET'] = os.environ.get('MICROSOFT_CLIENT_SECRET') app.config['MICROSOFT_TENANT_ID'] = os.environ.get('MICROSOFT_TENANT_ID', 'common') # Session cookie beveiliging is_https = app.config['BASE_URL'].startswith('https') app.config['SESSION_COOKIE_SECURE'] = is_https app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Lax ipv Strict: compatibel met OAuth redirect app.config['REMEMBER_COOKIE_SECURE'] = is_https app.config['REMEMBER_COOKIE_HTTPONLY'] = True app.config['REMEMBER_COOKIE_SAMESITE'] = 'Lax' app.config['REMEMBER_COOKIE_DURATION'] = 86400 * 8 # 8 dagen max (was: onbeperkt) # ── Rate limit handler ─────────────────────────────────────────────────── def _rate_limit_handler(e): logger.warning( f"Rate limit overschreden: {request.method} {request.path} " f"van {request.remote_addr}" ) return jsonify({ 'error': 'Te veel verzoeken. Probeer later opnieuw.', 'retry_after': e.retry_after, }), 429 # ── ProxyFix (Flask zit achter nginx) ──────────────────────────────────── app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) # ── Rate limiter ────────────────────────────────────────────────────────── # In flask-limiter 3.x hoort storage_uri in de constructor, niet in init_app(). # We vervangen de module-level limiter instantie zodat @limiter.limit decorators # ook de juiste storage gebruiken. global limiter redis_url = os.environ.get('REDIS_URL', '') if not redis_url: logger.warning( "REDIS_URL niet ingesteld — rate limiter gebruikt in-memory storage. " "Dit werkt NIET correct met meerdere gunicorn workers! " "Stel REDIS_URL in voor productie." ) redis_url = 'memory://' limiter = _make_limiter(redis_url) limiter.init_app(app) # ── Security headers via Talisman ───────────────────────────────────────── # CSP: strikte whitelist — geen inline scripts, geen externe resources buiten cdnjs # CSP: nonce-based voor scripts (Talisman injecteert {{ csp_nonce() }} in templates) # unsafe-inline is uitgeschakeld voor scripts — gebruik {{ csp_nonce() }} in