Spaces:
				
			
			
	
			
			
		Sleeping
		
	
	
	
			
			
	
	
	
	
		
		
		Sleeping
		
	| """ | |
| Participatory Planning Application | |
| Copyright (c) 2024-2025 Marcos Thadeu Queiroz Magalhães ([email protected]) | |
| 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 | |
| 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 | |
| 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 | |