""" Participatory Planning Application Copyright (c) 2024-2025 Marcos Thadeu Queiroz Magalhães (thadillo@gmail.com) Licensed under MIT License - See LICENSE file for details """ from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_limiter import Limiter from flask_limiter.util import get_remote_address from dotenv import load_dotenv import os db = SQLAlchemy() limiter = Limiter( key_func=get_remote_address, default_limits=["200 per day", "50 per hour"], storage_uri="memory://" ) def create_app(): load_dotenv() app = Flask(__name__) # Secret key validation with fail-fast in production flask_secret_key = os.getenv('FLASK_SECRET_KEY') flask_env = os.getenv('FLASK_ENV', 'production') if not flask_secret_key: if flask_env == 'production': raise RuntimeError( "FLASK_SECRET_KEY must be set in production! " "Generate one with: python -c \"import secrets; print(secrets.token_hex(32))\"" ) else: # Development: Generate random secret (not persistent) import secrets flask_secret_key = secrets.token_hex(32) app.logger.warning("⚠️ No FLASK_SECRET_KEY set - using random key for development") app.logger.warning("⚠️ Sessions will be invalidated on restart!") elif flask_secret_key == 'dev-secret-key-change-in-production': raise RuntimeError( "FLASK_SECRET_KEY is using the default insecure value! " "Change it in .env file to a secure random value." ) app.config['SECRET_KEY'] = flask_secret_key # Session configuration for iframe embedding (HF Spaces) app.config['SESSION_COOKIE_SECURE'] = True # Required for HTTPS app.config['SESSION_COOKIE_HTTPONLY'] = True # Security app.config['SESSION_COOKIE_SAMESITE'] = 'None' # Allow in iframes app.config['SESSION_COOKIE_PARTITIONED'] = True # Safari compatibility app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 hours # Use custom database path if set (for HF Spaces), otherwise use instance folder db_path = os.getenv('DATABASE_PATH') if db_path: # Absolute path for Hugging Face Spaces app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' else: # Relative path for local development app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///participatory_planner.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # SQLite-specific settings to reduce locking issues app.config['SQLALCHEMY_ENGINE_OPTIONS'] = { 'connect_args': { 'timeout': 60, # Increase timeout to 60 seconds for HuggingFace 'check_same_thread': False # Allow multi-threaded access }, 'pool_pre_ping': True, # Verify connections before using 'pool_recycle': 3600, # Recycle connections every hour } db.init_app(app) limiter.init_app(app) # Enable WAL mode for SQLite to reduce locking with app.app_context(): from sqlalchemy import event from sqlalchemy.engine import Engine @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_conn, connection_record): cursor = dbapi_conn.cursor() cursor.execute("PRAGMA journal_mode=WAL") # Write-Ahead Logging cursor.execute("PRAGMA synchronous=NORMAL") # Balance safety/performance cursor.execute("PRAGMA busy_timeout=60000") # 60 second timeout for HuggingFace cursor.close() # Import models from app.models import models # Import and register blueprints from app.routes import auth, submissions, admin app.register_blueprint(auth.bp) app.register_blueprint(submissions.bp) app.register_blueprint(admin.bp) # Add Partitioned attribute to session cookies for Safari compatibility @app.after_request def add_partitioned_cookie(response): """Add Partitioned attribute to cookies for Safari in iframes""" # Get the Set-Cookie headers set_cookie = response.headers.get('Set-Cookie') if set_cookie and 'session=' in set_cookie: # Add Partitioned attribute if SameSite=None is present if 'SameSite=None' in set_cookie and 'Partitioned' not in set_cookie: response.headers['Set-Cookie'] = set_cookie + '; Partitioned' return response # Create tables with app.app_context(): db.create_all() # Initialize with admin token if not exists (SECURE VERSION) from app.models.models import Token import secrets # Check if any admin token exists existing_admin = Token.query.filter_by(type='admin').first() if not existing_admin: # Get admin token from environment or generate secure random token admin_token_value = os.getenv('ADMIN_TOKEN') if not admin_token_value: # Generate secure random token admin_token_value = secrets.token_urlsafe(16) app.logger.warning("=" * 80) app.logger.warning("🔐 ADMIN TOKEN GENERATED (SAVE THIS - SHOWN ONLY ONCE):") app.logger.warning(f" {admin_token_value}") app.logger.warning("=" * 80) print("\n" + "=" * 80) print("🔐 ADMIN TOKEN GENERATED (SAVE THIS - SHOWN ONLY ONCE):") print(f" {admin_token_value}") print("=" * 80 + "\n") else: app.logger.info("Using ADMIN_TOKEN from environment variable") admin_token = Token( token=admin_token_value, type='admin', name='Administrator' ) db.session.add(admin_token) db.session.commit() app.logger.info(f"Admin token created: {admin_token_value[:4]}...{admin_token_value[-4:]}") return app