thadillo
Add copyright and attribution for Marcos Thadeu Queiroz Magalhães
67d3f72
"""
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
@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